Skip to content
Open
Changes from 1 commit
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
141 changes: 141 additions & 0 deletions apps/backend/src/services/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,150 @@ 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';
Comment on lines +39 to +41
Copy link

Copilot AI Nov 3, 2025

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.example file. Per the coding guidelines, the .env.example file should be kept updated with new environment variables.

Copilot uses AI. Check for mistakes.

// 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');
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses console.warn for logging. According to the project's coding guidelines, logging should use Sentry's logger. Import Sentry with import * as Sentry from \"@sentry/nextjs\", enable logs with Sentry.init({ enableLogs: true }), and use const { logger } = Sentry. Replace this with logger.warn('Rate limit reached for endpoint', { endpoint: '/sso', ssoEnabled: true }).

Copilot generated this review using guidance from repository custom instructions.
}
// 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
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables ssoName and ssoGroups are extracted from headers but are never used in the SSO authentication logic (only in debug logging). Consider removing these extractions or documenting why they're reserved for future use to avoid confusion.

Suggested change
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 uses AI. Check for mistakes.
});
Comment on lines +69 to +74
Copy link

Copilot AI Nov 3, 2025

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 generated this review using guidance from repository custom instructions.
}

// Process SSO if we have at least email or username
if (ssoEmail || ssoUser) {
const lookupEmail = ssoEmail || `${ssoUser}@sso.local`;

try {
let user = await this._userService.getUserByEmail(lookupEmail);

if (user && user.activated) {
// Load organization context
delete user.password;
const orgHeader = req.cookies.showorg || req.headers.showorg;
const organizations = (
await this._organizationService.getOrgsByUserId(user.id)
).filter((f) => !f.users[0].disabled);

// Organization selection strategy
const orgStrategy = process.env.SSO_DEFAULT_ORG_STRATEGY || 'first-active';
const forceOrgId = process.env.SSO_FORCE_ORG_ID;

let selectedOrg;
if (forceOrgId) {
selectedOrg = organizations.find((org) => org.id === forceOrgId);
} else if (orgHeader) {
selectedOrg = organizations.find((org) => org.id === orgHeader);
} else if (orgStrategy === 'first-active') {
selectedOrg = organizations[0];
}

if (!organizations || !selectedOrg) {
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition !organizations will always evaluate to false because organizations is the result of filtering an array and will never be null/undefined (it will be an empty array at worst). The correct check should be if (!organizations.length || !selectedOrg) to properly detect when no organizations exist.

Suggested change
if (!organizations || !selectedOrg) {
if (!organizations.length || !selectedOrg) {

Copilot uses AI. Check for mistakes.
if (process.env.NODE_ENV !== 'production') {
console.error('[SSO] No organization found for user:', lookupEmail);
Copy link

Copilot AI Nov 3, 2025

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 generated this review using guidance from repository custom instructions.
}
throw new HttpForbiddenException();
}

// Ensure org has API key
if (!selectedOrg.apiKey) {
await this._organizationService.updateApiKey(selectedOrg.id);
}

// Enrich JWT payload with org context
const jwtPayload = { ...user, orgId: selectedOrg.id };
const jwt = AuthService.signJWT(jwtPayload);
const cookieDomain = getCookieUrlFromDomain(process.env.FRONTEND_URL!);

if (process.env.NODE_ENV !== 'production') {
console.log('[SSO] Setting auth cookie for user:', lookupEmail, 'org:', selectedOrg.id);
Copy link

Copilot AI Nov 3, 2025

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 generated this review using guidance from repository custom instructions.
}

// Set secure cookie
res.cookie('auth', jwt, {
path: '/',
domain: cookieDomain,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'lax',
}
: {}),
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
});
Comment on lines +128 to +139
Copy link

Copilot AI Nov 3, 2025

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 uses AI. Check for mistakes.

// Set request context
// @ts-ignore
req.user = user;
// @ts-ignore
req.org = selectedOrg;

// Standardize authorization header for downstream middleware
delete req.headers.authorization;
req.headers.authorization = `Bearer ${jwt}`;
req.headers.auth = jwt;
req.cookies.auth = jwt;

if (process.env.NODE_ENV !== 'production') {
console.log('[SSO] Request authenticated with org:', selectedOrg.id);
Copy link

Copilot AI Nov 3, 2025

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 generated this review using guidance from repository custom instructions.
}

return next();
} else {
if (process.env.NODE_ENV !== 'production') {
console.log('[SSO] User not found or not activated, continuing to normal auth');
Copy link

Copilot AI Nov 3, 2025

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 user lookup failed', { reason: 'User not found or not activated' }).

Copilot generated this review using guidance from repository custom instructions.
}
}
} catch (err) {
if (process.env.NODE_ENV !== 'production') {
console.error('[SSO] Error during SSO processing:', err);
Copy link

Copilot AI Nov 3, 2025

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 }).

Copilot generated this review using guidance from repository custom instructions.
}
// Graceful fallback: continue to normal auth flow
}
}
}
}

// Standard Postiz authentication flow
const auth = req.headers.auth || req.cookies.auth;
if (!auth) {
throw new HttpForbiddenException();
}

try {
let user = AuthService.verifyJWT(auth) as User | null;
const orgHeader = req.cookies.showorg || req.headers.showorg;
Expand All @@ -47,6 +187,7 @@ export class AuthMiddleware implements NestMiddleware {
throw new HttpForbiddenException();
}

// Handle impersonation (superadmin feature)
const impersonate = req.cookies.impersonate || req.headers.impersonate;
if (user?.isSuperAdmin && impersonate) {
const loadImpersonate = await this._organizationService.getUserOrg(
Expand Down