diff --git a/backend/tabby/middleware.py b/backend/tabby/middleware.py index 5197501..44e0ed4 100644 --- a/backend/tabby/middleware.py +++ b/backend/tabby/middleware.py @@ -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 diff --git a/backend/tabby/settings.py b/backend/tabby/settings.py index 0714461..a3459e9 100644 --- a/backend/tabby/settings.py +++ b/backend/tabby/settings.py @@ -39,6 +39,7 @@ "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", @@ -46,6 +47,10 @@ "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 = [ diff --git a/docker-compose.yml b/docker-compose.yml index 6202d8e..2a7b5ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: