Browse Source

OIDC flow fixes

pull/5981/head
Germán Martín 1 week ago
parent
commit
80c98129b5
  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 OIDC (OpenID Connect) as a login auth provider
- Added OIDC (_OpenID Connect_) as a login auth provider
### Changed

20
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(

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 { 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]
},

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 { 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,

2
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 }),

2
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;

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">
<a
class="px-4 rounded-pill"
href="../api/v1/auth/oidc"
href="../api/auth/oidc"
mat-stroked-button
><span i18n>Sign in with OIDC</span></a
>

Loading…
Cancel
Save