Browse Source

OIDC flow fixes

pull/5981/head
Germán Martín 1 week ago
committed by Thomas Kaul
parent
commit
7485cfe6f3
  1. 2
      CHANGELOG.md
  2. 20
      apps/api/src/app/auth/auth.controller.ts
  3. 71
      apps/api/src/app/auth/auth.module.ts
  4. 15
      apps/api/src/app/auth/oidc.strategy.ts
  5. 2
      apps/api/src/services/configuration/configuration.service.ts
  6. 2
      apps/api/src/services/interfaces/environment.interface.ts
  7. 2
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

2
CHANGELOG.md

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added _OpenID Connect_ (`OIDC`) as a new login provider - Added OIDC (_OpenID Connect_) as a login auth provider
### Changed ### Changed

20
apps/api/src/app/auth/auth.controller.ts

@ -104,7 +104,15 @@ export class AuthController {
@Get('oidc') @Get('oidc')
@UseGuards(AuthGuard('oidc')) @UseGuards(AuthGuard('oidc'))
@Version(VERSION_NEUTRAL)
public oidcLogin() { public oidcLogin() {
if (!this.configurationService.get('ENABLE_FEATURE_AUTH_OIDC')) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
// Initiates the OIDC login flow // 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') @Post('webauthn/generate-authentication-options')
public async generateAuthenticationOptions( public async generateAuthenticationOptions(
@Body() body: { deviceId: string } @Body() body: { deviceId: string }
@ -143,6 +145,12 @@ export class AuthController {
return this.webAuthService.generateAuthenticationOptions(body.deviceId); 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') @Post('webauthn/verify-attestation')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async verifyAttestation( public async verifyAttestation(

71
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 { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.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 { JwtModule } from '@nestjs/jwt';
import type { StrategyOptions } from 'passport-openidconnect';
import { ApiKeyStrategy } from './api-key.strategy'; import { ApiKeyStrategy } from './api-key.strategy';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
@ -45,31 +46,13 @@ import { OidcStrategy } from './oidc.strategy';
configurationService: ConfigurationService configurationService: ConfigurationService
) => { ) => {
const issuer = configurationService.get('OIDC_ISSUER'); const issuer = configurationService.get('OIDC_ISSUER');
const scopeString = configurationService.get('OIDC_SCOPE'); const scope = configurationService.get('OIDC_SCOPE');
const scope = scopeString
.split(' ')
.map((s) => s.trim())
.filter((s) => s.length > 0);
const callbackUrl = const callbackUrl =
configurationService.get('OIDC_CALLBACK_URL') || configurationService.get('OIDC_CALLBACK_URL') ||
`${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`;
const options: { let options: StrategyOptions;
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
};
if (issuer) { if (issuer) {
try { try {
@ -82,36 +65,36 @@ import { OidcStrategy } from './oidc.strategy';
userinfo_endpoint: string; userinfo_endpoint: string;
}; };
options.authorizationURL = config.authorization_endpoint; options = {
options.issuer = issuer; authorizationURL: config.authorization_endpoint,
options.tokenURL = config.token_endpoint; callbackURL: callbackUrl,
options.userInfoURL = config.userinfo_endpoint; clientID: configurationService.get('OIDC_CLIENT_ID'),
clientSecret: configurationService.get('OIDC_CLIENT_SECRET'),
issuer,
scope,
tokenURL: config.token_endpoint,
userInfoURL: config.userinfo_endpoint
};
} catch (error) { } catch (error) {
Logger.error(error, 'OidcStrategy'); Logger.error(error, 'OidcStrategy');
throw new Error('Failed to fetch OIDC configuration from issuer'); throw new Error('Failed to fetch OIDC configuration from issuer');
} }
} else { } else {
options.authorizationURL = configurationService.get( options = {
'OIDC_AUTHORIZATION_URL' authorizationURL: configurationService.get(
); 'OIDC_AUTHORIZATION_URL'
options.issuer = configurationService.get('OIDC_ISSUER'); ),
options.tokenURL = configurationService.get('OIDC_TOKEN_URL'); callbackURL: callbackUrl,
options.userInfoURL = configurationService.get('OIDC_USER_INFO_URL'); 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( return new OidcStrategy(authService, options);
authService,
options as {
authorizationURL: string;
callbackURL: string;
clientID: string;
clientSecret: string;
issuer: string;
scope: string[];
tokenURL: string;
userInfoURL: string;
}
);
}, },
inject: [AuthService, ConfigurationService] inject: [AuthService, ConfigurationService]
}, },

15
apps/api/src/app/auth/oidc.strategy.ts

@ -2,22 +2,11 @@ import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client'; import { Provider } from '@prisma/client';
import { Request } from 'express'; import { Request } from 'express';
import { Strategy } from 'passport-openidconnect'; import { Strategy, type StrategyOptions } from 'passport-openidconnect';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { OidcStateStore } from './oidc-state.store'; 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 { interface OidcProfile {
id?: string; id?: string;
sub?: string; sub?: string;
@ -43,7 +32,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
public constructor( public constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
options: OidcStrategyOptions options: StrategyOptions
) { ) {
super({ super({
...options, ...options,

2
apps/api/src/services/configuration/configuration.service.ts

@ -63,7 +63,7 @@ export class ConfigurationService {
OIDC_CLIENT_ID: str({ default: '' }), OIDC_CLIENT_ID: str({ default: '' }),
OIDC_CLIENT_SECRET: str({ default: '' }), OIDC_CLIENT_SECRET: str({ default: '' }),
OIDC_ISSUER: str({ default: '' }), OIDC_ISSUER: str({ default: '' }),
OIDC_SCOPE: str({ default: 'profile' }), OIDC_SCOPE: json({ default: ['openid'] }),
OIDC_TOKEN_URL: str({ default: '' }), OIDC_TOKEN_URL: str({ default: '' }),
OIDC_USER_INFO_URL: str({ default: '' }), OIDC_USER_INFO_URL: str({ default: '' }),
PORT: port({ default: DEFAULT_PORT }), PORT: port({ default: DEFAULT_PORT }),

2
apps/api/src/services/interfaces/environment.interface.ts

@ -38,7 +38,7 @@ export interface Environment extends CleanedEnvAccessors {
OIDC_CLIENT_ID: string; OIDC_CLIENT_ID: string;
OIDC_CLIENT_SECRET: string; OIDC_CLIENT_SECRET: string;
OIDC_ISSUER: string; OIDC_ISSUER: string;
OIDC_SCOPE: string; OIDC_SCOPE: string[];
OIDC_TOKEN_URL: string; OIDC_TOKEN_URL: string;
OIDC_USER_INFO_URL: string; OIDC_USER_INFO_URL: string;
PORT: number; PORT: number;

2
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -50,7 +50,7 @@
<div class="d-flex flex-column mt-2"> <div class="d-flex flex-column mt-2">
<a <a
class="px-4 rounded-pill" class="px-4 rounded-pill"
href="../api/v1/auth/oidc" href="../api/auth/oidc"
mat-stroked-button mat-stroked-button
><span i18n>Sign in with OIDC</span></a ><span i18n>Sign in with OIDC</span></a
> >

Loading…
Cancel
Save