diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 29af3a3c2..388f1dbd3 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -111,8 +111,6 @@ export class AuthController { StatusCodes.FORBIDDEN ); } - - // Initiates the OIDC login flow } @Get('oidc/callback') diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index c31e66299..4404205ce 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -60,6 +60,7 @@ import { OidcStrategy } from './oidc.strategy'; const response = await fetch( `${issuer}/.well-known/openid-configuration` ); + const config = (await response.json()) as { authorization_endpoint: string; token_endpoint: string; @@ -67,12 +68,12 @@ import { OidcStrategy } from './oidc.strategy'; }; options = { + issuer, + scope, authorizationURL: config.authorization_endpoint, callbackURL: callbackUrl, clientID: configurationService.get('OIDC_CLIENT_ID'), clientSecret: configurationService.get('OIDC_CLIENT_SECRET'), - issuer, - scope, tokenURL: config.token_endpoint, userInfoURL: config.userinfo_endpoint }; @@ -82,6 +83,7 @@ import { OidcStrategy } from './oidc.strategy'; } } else { options = { + scope, authorizationURL: configurationService.get( 'OIDC_AUTHORIZATION_URL' ), @@ -89,7 +91,6 @@ import { OidcStrategy } from './oidc.strategy'; clientID: configurationService.get('OIDC_CLIENT_ID'), clientSecret: configurationService.get('OIDC_CLIENT_SECRET'), issuer: configurationService.get('OIDC_ISSUER'), - scope, tokenURL: configurationService.get('OIDC_TOKEN_URL'), userInfoURL: configurationService.get('OIDC_USER_INFO_URL') }; diff --git a/apps/api/src/app/auth/interfaces/interfaces.ts b/apps/api/src/app/auth/interfaces/interfaces.ts index 4fdcc25b5..7ddfe41d2 100644 --- a/apps/api/src/app/auth/interfaces/interfaces.ts +++ b/apps/api/src/app/auth/interfaces/interfaces.ts @@ -6,6 +6,25 @@ export interface AuthDeviceDialogParams { authDevice: AuthDeviceDto; } +export interface OidcContext { + claims?: { + sub?: string; + }; +} + +export interface OidcIdToken { + sub?: string; +} + +export interface OidcParams { + sub?: string; +} + +export interface OidcProfile { + id?: string; + sub?: string; +} + export interface ValidateOAuthLoginParams { provider: Provider; thirdPartyId: string; diff --git a/apps/api/src/app/auth/oidc-state.store.ts b/apps/api/src/app/auth/oidc-state.store.ts index 437846cf1..0d9bb5f0f 100644 --- a/apps/api/src/app/auth/oidc-state.store.ts +++ b/apps/api/src/app/auth/oidc-state.store.ts @@ -5,6 +5,8 @@ import ms from 'ms'; * This store manages OAuth2 state parameters in memory with automatic cleanup. */ export class OidcStateStore { + private readonly STATE_EXPIRY_MS = ms('10 minutes'); + private stateMap = new Map< string, { @@ -14,7 +16,6 @@ export class OidcStateStore { timestamp: number; } >(); - private readonly STATE_EXPIRY_MS = ms('10 minutes'); /** * Store request state. @@ -26,7 +27,7 @@ export class OidcStateStore { appState: unknown, ctx: { maxAge?: number; nonce?: string; issued?: Date }, callback: (err: Error | null, handle?: string) => void - ): void { + ) { try { // Generate a unique handle for this state const handle = this.generateHandle(); @@ -59,7 +60,7 @@ export class OidcStateStore { appState?: unknown, ctx?: { maxAge?: number; nonce?: string; issued?: Date } ) => void - ): void { + ) { try { const data = this.stateMap.get(handle); @@ -85,7 +86,7 @@ export class OidcStateStore { /** * Clean up expired states */ - private cleanup(): void { + private cleanup() { const now = Date.now(); const expiredKeys: string[] = []; @@ -103,7 +104,7 @@ export class OidcStateStore { /** * Generate a cryptographically secure random handle */ - private generateHandle(): string { + private generateHandle() { return ( Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + diff --git a/apps/api/src/app/auth/oidc.strategy.ts b/apps/api/src/app/auth/oidc.strategy.ts index 58fd7bd87..6ed03b5a8 100644 --- a/apps/api/src/app/auth/oidc.strategy.ts +++ b/apps/api/src/app/auth/oidc.strategy.ts @@ -5,27 +5,14 @@ import { Request } from 'express'; import { Strategy, type StrategyOptions } from 'passport-openidconnect'; import { AuthService } from './auth.service'; +import { + OidcContext, + OidcIdToken, + OidcParams, + OidcProfile +} from './interfaces/interfaces'; import { OidcStateStore } from './oidc-state.store'; -interface OidcProfile { - id?: string; - sub?: string; -} - -interface OidcContext { - claims?: { - sub?: string; - }; -} - -interface OidcIdToken { - sub?: string; -} - -interface OidcParams { - sub?: string; -} - @Injectable() export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { private static readonly stateStore = new OidcStateStore(); diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 6f139b305..2a0546961 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -55,17 +55,17 @@ export class ConfigurationService { GOOGLE_SHEETS_ID: str({ default: '' }), GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }), HOST: host({ default: DEFAULT_HOST }), - JWT_SECRET_KEY: str({}), + JWT_SECRET_KEY: str(), MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_CHART_ITEMS: num({ default: 365 }), - OIDC_AUTHORIZATION_URL: str({ default: undefined }), - OIDC_CALLBACK_URL: str({ default: undefined }), - OIDC_CLIENT_ID: str({ default: undefined }), - OIDC_CLIENT_SECRET: str({ default: undefined }), - OIDC_ISSUER: str({ default: undefined }), + OIDC_AUTHORIZATION_URL: str(), + OIDC_CALLBACK_URL: str(), + OIDC_CLIENT_ID: str(), + OIDC_CLIENT_SECRET: str(), + OIDC_ISSUER: str(), OIDC_SCOPE: json({ default: ['openid'] }), - OIDC_TOKEN_URL: str({ default: undefined }), - OIDC_USER_INFO_URL: str({ default: undefined }), + OIDC_TOKEN_URL: str(), + OIDC_USER_INFO_URL: str(), PORT: port({ default: DEFAULT_PORT }), PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html index d345c4df5..cf5611ef7 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -41,7 +41,7 @@ class="mr-2" src="../assets/icons/google.svg" style="height: 1rem" - />Sign in with GoogleSign in with Google } diff --git a/prisma/migrations/20251103162035_add_oidc_provider/migration.sql b/prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql similarity index 100% rename from prisma/migrations/20251103162035_add_oidc_provider/migration.sql rename to prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql