Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions backend/tabby/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,94 @@ def __init__(self, get_response):
self.get_response = get_response


class ProxyAuthMiddleware(BaseMiddleware):
"""
Middleware to authenticate users via auth proxy headers.

When running behind an authenticating proxy (like Authentik, Authelia,
or any ForwardAuth service), this middleware trusts the X-Auth-* headers
set by the proxy and automatically logs in users based on their email.

Headers:
X-Auth-User-Email: User's email address (required for auth)
X-Auth-User-Name: User's display name (optional)
X-Auth-User-Id: External user ID (optional)
X-Auth-Tenant-Id: Tenant ID (optional)

Enable by setting PROXY_AUTH_ENABLED=true in environment.
"""

def __call__(self, request):
# Only process if proxy auth is enabled
if not getattr(settings, "PROXY_AUTH_ENABLED", False):
return self.get_response(request)

# Skip if user is already authenticated
if request.user.is_authenticated:
return self.get_response(request)

# Check for proxy auth header (Django converts X-Auth-User-Email to HTTP_X_AUTH_USER_EMAIL)
user_email = request.META.get("HTTP_X_AUTH_USER_EMAIL")
if not user_email:
return self.get_response(request)

# Get optional headers
user_name = request.META.get("HTTP_X_AUTH_USER_NAME", "")
tenant_id = request.META.get("HTTP_X_AUTH_TENANT_ID", "")

# Get or create user by email
user, created = User.objects.get_or_create(
email=user_email,
defaults={
"username": self._generate_username(user_email, user_name),
"first_name": user_name.split()[0] if user_name else "",
"last_name": " ".join(user_name.split()[1:]) if user_name else "",
}
)

if created:
logging.info(
f"ProxyAuthMiddleware: Created new user {user.username} "
f"(email={user_email}, tenant={tenant_id})"
)
else:
# Update name if provided and changed
if user_name and user.first_name != user_name.split()[0]:
user.first_name = user_name.split()[0] if user_name else ""
user.last_name = " ".join(user_name.split()[1:]) if user_name else ""
user.save(update_fields=["first_name", "last_name"])

# Log in the user
setattr(user, "backend", "django.contrib.auth.backends.ModelBackend")
login(request, user)
setattr(request, "_dont_enforce_csrf_checks", True)

# Store tenant info on request for potential future use
if tenant_id:
setattr(request, "tenant_id", tenant_id)

return self.get_response(request)

def _generate_username(self, email: str, name: str) -> str:
"""Generate a unique username from email or name."""
# Try name first (without spaces)
if name:
base_username = name.replace(" ", "_").lower()[:30]
else:
# Use email prefix
base_username = email.split("@")[0].lower()[:30]

# Ensure uniqueness
username = base_username
counter = 1
while User.objects.filter(username=username).exists():
suffix = f"_{counter}"
username = base_username[: 30 - len(suffix)] + suffix
counter += 1

return username


class TokenMiddleware(BaseMiddleware):
def __call__(self, request):
token_value = None
Expand Down
5 changes: 5 additions & 0 deletions backend/tabby/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"tabby.middleware.ProxyAuthMiddleware", # Must come after AuthenticationMiddleware
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
"tabby.middleware.TokenMiddleware",
"tabby.middleware.GAMiddleware",
]

# Proxy authentication - enable when running behind an authenticating proxy
# (Authentik, Authelia, etc.) that sets X-Auth-User-Email headers
PROXY_AUTH_ENABLED = os.getenv("PROXY_AUTH_ENABLED", "").lower() in ("true", "1", "yes")

ROOT_URLCONF = "tabby.urls"

TEMPLATES = [
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ services:
- PORT=80
- DEBUG=False
- DOCKERIZE_ARGS="-wait tcp://db:3306 -timeout 60s"
#
# Authentication Options (choose one):
#
# Option 1: Proxy Authentication (Authentik, Authelia, etc.)
# Enable when running behind a reverse proxy that sets X-Auth-User-Email headers
# - PROXY_AUTH_ENABLED=true
#
# Option 2: OAuth Providers (GitHub, GitLab, Google, Microsoft)
# Uncomment and set credentials for providers you want to enable:
# - SOCIAL_AUTH_GITHUB_KEY=your_github_client_id
# - SOCIAL_AUTH_GITHUB_SECRET=your_github_client_secret
# - SOCIAL_AUTH_GITLAB_KEY=your_gitlab_client_id
# - SOCIAL_AUTH_GITLAB_SECRET=your_gitlab_client_secret
# - SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your_google_client_id
# - SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your_google_client_secret
# - SOCIAL_AUTH_MICROSOFT_GRAPH_KEY=your_microsoft_client_id
# - SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET=your_microsoft_client_secret
#
# - APP_DIST_STORAGE="file:///app-dist"

db:
Expand Down