From 7485cfe6f3c79d493c9095a8ed0d42b60462314f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Fri, 28 Nov 2025 22:53:27 +0100 Subject: [PATCH] OIDC flow fixes --- CHANGELOG.md | 2 +- apps/api/src/app/auth/auth.controller.ts | 20 ++++-- apps/api/src/app/auth/auth.module.ts | 71 +++++++------------ apps/api/src/app/auth/oidc.strategy.ts | 15 +--- .../configuration/configuration.service.ts | 2 +- .../interfaces/environment.interface.ts | 2 +- .../login-with-access-token-dialog.html | 2 +- 7 files changed, 47 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abe1702c2..897cb577a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added _OpenID Connect_ (`OIDC`) as a new login provider +- Added OIDC (_OpenID Connect_) as a login auth provider ### Changed diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index d5a5e4bea..cd51e2954 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -104,7 +104,15 @@ export class AuthController { @Get('oidc') @UseGuards(AuthGuard('oidc')) + @Version(VERSION_NEUTRAL) public oidcLogin() { + if (!this.configurationService.get('ENABLE_FEATURE_AUTH_OIDC')) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + // Initiates the OIDC login flow } @@ -130,12 +138,6 @@ export class AuthController { } } - @Get('webauthn/generate-registration-options') - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async generateRegistrationOptions() { - return this.webAuthService.generateRegistrationOptions(); - } - @Post('webauthn/generate-authentication-options') public async generateAuthenticationOptions( @Body() body: { deviceId: string } @@ -143,6 +145,12 @@ export class AuthController { return this.webAuthService.generateAuthenticationOptions(body.deviceId); } + @Get('webauthn/generate-registration-options') + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async generateRegistrationOptions() { + return this.webAuthService.generateRegistrationOptions(); + } + @Post('webauthn/verify-attestation') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async verifyAttestation( diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index 9c535f947..60a3a64d1 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -8,8 +8,9 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; -import { Module, Logger } from '@nestjs/common'; +import { Logger, Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import type { StrategyOptions } from 'passport-openidconnect'; import { ApiKeyStrategy } from './api-key.strategy'; import { AuthController } from './auth.controller'; @@ -45,31 +46,13 @@ import { OidcStrategy } from './oidc.strategy'; configurationService: ConfigurationService ) => { const issuer = configurationService.get('OIDC_ISSUER'); - const scopeString = configurationService.get('OIDC_SCOPE'); - const scope = scopeString - .split(' ') - .map((s) => s.trim()) - .filter((s) => s.length > 0); + const scope = configurationService.get('OIDC_SCOPE'); const callbackUrl = configurationService.get('OIDC_CALLBACK_URL') || `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; - const options: { - authorizationURL?: string; - callbackURL: string; - clientID: string; - clientSecret: string; - issuer?: string; - scope: string[]; - tokenURL?: string; - userInfoURL?: string; - } = { - callbackURL: callbackUrl, - clientID: configurationService.get('OIDC_CLIENT_ID'), - clientSecret: configurationService.get('OIDC_CLIENT_SECRET'), - scope - }; + let options: StrategyOptions; if (issuer) { try { @@ -82,36 +65,36 @@ import { OidcStrategy } from './oidc.strategy'; userinfo_endpoint: string; }; - options.authorizationURL = config.authorization_endpoint; - options.issuer = issuer; - options.tokenURL = config.token_endpoint; - options.userInfoURL = config.userinfo_endpoint; + options = { + 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 + }; } catch (error) { Logger.error(error, 'OidcStrategy'); throw new Error('Failed to fetch OIDC configuration from issuer'); } } else { - options.authorizationURL = configurationService.get( - 'OIDC_AUTHORIZATION_URL' - ); - options.issuer = configurationService.get('OIDC_ISSUER'); - options.tokenURL = configurationService.get('OIDC_TOKEN_URL'); - options.userInfoURL = configurationService.get('OIDC_USER_INFO_URL'); + options = { + authorizationURL: configurationService.get( + 'OIDC_AUTHORIZATION_URL' + ), + callbackURL: callbackUrl, + 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') + }; } - return new OidcStrategy( - authService, - options as { - authorizationURL: string; - callbackURL: string; - clientID: string; - clientSecret: string; - issuer: string; - scope: string[]; - tokenURL: string; - userInfoURL: string; - } - ); + return new OidcStrategy(authService, options); }, inject: [AuthService, ConfigurationService] }, diff --git a/apps/api/src/app/auth/oidc.strategy.ts b/apps/api/src/app/auth/oidc.strategy.ts index 2ac1e0473..8366c58bc 100644 --- a/apps/api/src/app/auth/oidc.strategy.ts +++ b/apps/api/src/app/auth/oidc.strategy.ts @@ -2,22 +2,11 @@ import { Injectable, Logger } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Provider } from '@prisma/client'; import { Request } from 'express'; -import { Strategy } from 'passport-openidconnect'; +import { Strategy, type StrategyOptions } from 'passport-openidconnect'; import { AuthService } from './auth.service'; import { OidcStateStore } from './oidc-state.store'; -interface OidcStrategyOptions { - authorizationURL: string; - callbackURL: string; - clientID: string; - clientSecret: string; - issuer: string; - scope?: string[]; - tokenURL: string; - userInfoURL: string; -} - interface OidcProfile { id?: string; sub?: string; @@ -43,7 +32,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { public constructor( private readonly authService: AuthService, - options: OidcStrategyOptions + options: StrategyOptions ) { super({ ...options, diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 75b7ed4d1..59de60354 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -63,7 +63,7 @@ export class ConfigurationService { OIDC_CLIENT_ID: str({ default: '' }), OIDC_CLIENT_SECRET: str({ default: '' }), OIDC_ISSUER: str({ default: '' }), - OIDC_SCOPE: str({ default: 'profile' }), + OIDC_SCOPE: json({ default: ['openid'] }), OIDC_TOKEN_URL: str({ default: '' }), OIDC_USER_INFO_URL: str({ default: '' }), PORT: port({ default: DEFAULT_PORT }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index c37442688..3c03744f1 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -38,7 +38,7 @@ export interface Environment extends CleanedEnvAccessors { OIDC_CLIENT_ID: string; OIDC_CLIENT_SECRET: string; OIDC_ISSUER: string; - OIDC_SCOPE: string; + OIDC_SCOPE: string[]; OIDC_TOKEN_URL: string; OIDC_USER_INFO_URL: string; PORT: number; 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 0623a0f36..6570ab3d6 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 @@ -50,7 +50,7 @@
Sign in with OIDC