-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Feature: Add generic SSO support via trusted reverse proxy headers #1047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a80abde
b42b7ce
c21b675
2922458
7383d3a
0b6cf91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| FROM node:22-alpine | ||
| ARG NEXT_PUBLIC_VERSION | ||
| ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION | ||
| RUN apk add --no-cache g++ make py3-pip bash nginx | ||
| RUN adduser -D -g 'www' www | ||
| RUN mkdir /www | ||
| RUN chown -R www:www /var/lib/nginx | ||
| RUN chown -R www:www /www | ||
|
|
||
|
|
||
| RUN npm --no-update-notifier --no-fund --global install [email protected] pm2 | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY . /app | ||
| COPY var/docker/nginx.conf /etc/nginx/nginx.conf | ||
|
|
||
| RUN pnpm install | ||
| RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm run build | ||
|
|
||
| CMD ["sh", "-c", "nginx && pnpm run pm2"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { Controller, Get, Req } from '@nestjs/common'; | ||
| import { Request } from 'express'; | ||
|
|
||
| /** | ||
| * Debug endpoints for testing SSO middleware behavior | ||
| * IMPORTANT: Only enable in development/test environments | ||
| */ | ||
| @Controller('debug') | ||
| export class DebugController { | ||
| @Get('auth') | ||
| debugAuth(@Req() req: Request) { | ||
| // Only allow in non-production environments | ||
| if (process.env.NODE_ENV === 'production') { | ||
| return { error: 'Debug endpoints disabled in production' }; | ||
| } | ||
|
|
||
| return { | ||
| user: (req as any).user ? { | ||
| id: (req as any).user.id, | ||
| email: (req as any).user.email, | ||
| name: (req as any).user.name, | ||
| activated: (req as any).user.activated, | ||
| } : null, | ||
| org: (req as any).org ? { | ||
| id: (req as any).org.id, | ||
| name: (req as any).org.name, | ||
| } : null, | ||
| headers: { | ||
| 'remote-email': req.headers['remote-email'], | ||
| 'remote-user': req.headers['remote-user'], | ||
| 'remote-name': req.headers['remote-name'], | ||
| 'remote-groups': req.headers['remote-groups'], | ||
| 'cookie': req.headers.cookie ? '[present]' : '[absent]', | ||
| 'authorization': req.headers.authorization ? '[present]' : '[absent]', | ||
| }, | ||
| sso: { | ||
| enabled: process.env.ENABLE_SSO === 'true', | ||
| trustProxy: process.env.SSO_TRUST_PROXY === 'true', | ||
| mode: process.env.SSO_MODE, | ||
| sharedSecretConfigured: !!process.env.SSO_SHARED_SECRET, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| @Get('health') | ||
| health() { | ||
| return { | ||
| status: 'ok', | ||
| timestamp: new Date().toISOString(), | ||
| env: process.env.NODE_ENV, | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,10 +31,159 @@ export class AuthMiddleware implements NestMiddleware { | |||||||||||||||||||||||||||||||||
| private _userService: UsersService | ||||||||||||||||||||||||||||||||||
| ) {} | ||||||||||||||||||||||||||||||||||
| async use(req: Request, res: Response, next: NextFunction) { | ||||||||||||||||||||||||||||||||||
| // Check if user already has valid auth cookie first | ||||||||||||||||||||||||||||||||||
| const existingAuth = req.headers.auth || req.cookies.auth; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // TRUSTED REVERSE PROXY SSO | ||||||||||||||||||||||||||||||||||
| // Supports any reverse proxy that can set trusted headers (Traefik, Nginx, Caddy, oauth2-proxy, etc.) | ||||||||||||||||||||||||||||||||||
| const enableSSO = process.env.ENABLE_SSO === 'true'; | ||||||||||||||||||||||||||||||||||
| const trustProxy = process.env.SSO_TRUST_PROXY === 'true'; | ||||||||||||||||||||||||||||||||||
| const ssoMode = process.env.SSO_MODE || 'trusted-headers'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Only process SSO if explicitly enabled AND proxy is trusted | ||||||||||||||||||||||||||||||||||
| if (enableSSO && trustProxy && ssoMode === 'trusted-headers' && !existingAuth) { | ||||||||||||||||||||||||||||||||||
| // Configurable header names (default to Authelia/ForwardAuth standard) | ||||||||||||||||||||||||||||||||||
| const emailHeader = (process.env.SSO_HEADER_EMAIL || 'remote-email').toLowerCase(); | ||||||||||||||||||||||||||||||||||
| const nameHeader = (process.env.SSO_HEADER_NAME || 'remote-name').toLowerCase(); | ||||||||||||||||||||||||||||||||||
| const userHeader = (process.env.SSO_HEADER_USER || 'remote-user').toLowerCase(); | ||||||||||||||||||||||||||||||||||
| const groupsHeader = (process.env.SSO_HEADER_GROUPS || 'remote-groups').toLowerCase(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Optional shared secret validation | ||||||||||||||||||||||||||||||||||
| const sharedSecret = process.env.SSO_SHARED_SECRET; | ||||||||||||||||||||||||||||||||||
| const secretHeader = (process.env.SSO_SECRET_HEADER || 'x-sso-secret').toLowerCase(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Security: validate shared secret if configured | ||||||||||||||||||||||||||||||||||
| if (sharedSecret && req.headers[secretHeader] !== sharedSecret) { | ||||||||||||||||||||||||||||||||||
| if (process.env.NODE_ENV !== 'production') { | ||||||||||||||||||||||||||||||||||
| console.warn('[SSO] Invalid or missing shared secret header, falling back to normal auth'); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| // Fall through to normal auth | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| // Extract SSO headers | ||||||||||||||||||||||||||||||||||
| const ssoEmail = req.headers[emailHeader] as string | undefined; | ||||||||||||||||||||||||||||||||||
| const ssoName = req.headers[nameHeader] as string | undefined; | ||||||||||||||||||||||||||||||||||
| const ssoUser = req.headers[userHeader] as string | undefined; | ||||||||||||||||||||||||||||||||||
| const ssoGroups = req.headers[groupsHeader] as string | undefined; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (process.env.NODE_ENV !== 'production') { | ||||||||||||||||||||||||||||||||||
| console.log('[SSO] Trusted headers detected:', { | ||||||||||||||||||||||||||||||||||
| email: ssoEmail, | ||||||||||||||||||||||||||||||||||
| name: ssoName, | ||||||||||||||||||||||||||||||||||
| user: ssoUser, | ||||||||||||||||||||||||||||||||||
| groups: ssoGroups, | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+73
|
||||||||||||||||||||||||||||||||||
| const ssoName = req.headers[nameHeader] as string | undefined; | |
| const ssoUser = req.headers[userHeader] as string | undefined; | |
| const ssoGroups = req.headers[groupsHeader] as string | undefined; | |
| if (process.env.NODE_ENV !== 'production') { | |
| console.log('[SSO] Trusted headers detected:', { | |
| email: ssoEmail, | |
| name: ssoName, | |
| user: ssoUser, | |
| groups: ssoGroups, | |
| const ssoUser = req.headers[userHeader] as string | undefined; | |
| if (process.env.NODE_ENV !== 'production') { | |
| console.log('[SSO] Trusted headers detected:', { | |
| email: ssoEmail, | |
| user: ssoUser, |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses console.log for logging. According to the project's coding guidelines, logging should use Sentry's logger. Replace this with logger.debug(logger.fmtSSO trusted headers detected for: ${ssoEmail}, { ssoName, ssoUser, ssoGroups }).
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses console.error for logging. According to the project's coding guidelines, logging should use Sentry's logger. Replace this with logger.error('Failed to process SSO authentication', { lookupEmail, reason: 'No organization found' }).
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses console.log for logging. According to the project's coding guidelines, logging should use Sentry's logger. Replace this with logger.info('SSO auth cookie set', { lookupEmail, orgId: selectedOrg.id }).
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SSO cookie uses sameSite: 'lax' while all other auth cookies in the codebase use sameSite: 'none' (see auth.controller.ts and removeAuth function in this same file). This inconsistency could cause issues with cross-site authentication. For consistency and to prevent potential authentication problems, change sameSite: 'lax' to sameSite: 'none' to match the existing pattern.
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses console.log for logging. According to the project's coding guidelines, logging should use Sentry's logger. Replace this with logger.info('SSO request authenticated', { orgId: selectedOrg.id }).
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses console.error for logging. According to the project's coding guidelines, logging should use Sentry's logger. Replace this with logger.error('Failed to process SSO authentication', { error: err }).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new SSO-related environment variables (ENABLE_SSO, SSO_TRUST_PROXY, SSO_MODE, SSO_HEADER_EMAIL, SSO_HEADER_NAME, SSO_HEADER_USER, SSO_HEADER_GROUPS, SSO_SHARED_SECRET, SSO_SECRET_HEADER, SSO_DEFAULT_ORG_STRATEGY, SSO_FORCE_ORG_ID) are not documented in the
.env.examplefile. Per the coding guidelines, the.env.examplefile should be kept updated with new environment variables.