Browse Source

Refactor OIDC authentication flow and update dependencies

pull/5981/head
Germán Martín 1 week ago
committed by Thomas Kaul
parent
commit
ba21ad07e0
  1. 2
      apps/api/src/app/auth/auth.controller.ts
  2. 4
      apps/api/src/app/auth/auth.module.ts
  3. 42
      apps/api/src/app/auth/oidc-state.store.ts
  4. 10
      apps/api/src/app/auth/oidc.strategy.ts
  5. 14
      apps/api/src/services/configuration/configuration.service.ts
  6. 4
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  7. 4
      package-lock.json
  8. 4
      package.json

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

@ -84,7 +84,6 @@ export class AuthController {
@Req() request: Request, @Req() request: Request,
@Res() response: Response @Res() response: Response
) { ) {
// Handles the Google OAuth2 callback
const jwt: string = (request.user as any).jwt; const jwt: string = (request.user as any).jwt;
if (jwt) { if (jwt) {
@ -120,7 +119,6 @@ export class AuthController {
@UseGuards(AuthGuard('oidc')) @UseGuards(AuthGuard('oidc'))
@Version(VERSION_NEUTRAL) @Version(VERSION_NEUTRAL)
public oidcLoginCallback(@Req() request: Request, @Res() response: Response) { public oidcLoginCallback(@Req() request: Request, @Res() response: Response) {
// Handles the OIDC callback
const jwt: string = (request.user as any).jwt; const jwt: string = (request.user as any).jwt;
if (jwt) { if (jwt) {

4
apps/api/src/app/auth/auth.module.ts

@ -40,6 +40,7 @@ import { OidcStrategy } from './oidc.strategy';
GoogleStrategy, GoogleStrategy,
JwtStrategy, JwtStrategy,
{ {
inject: [AuthService, ConfigurationService],
provide: OidcStrategy, provide: OidcStrategy,
useFactory: async ( useFactory: async (
authService: AuthService, authService: AuthService,
@ -95,8 +96,7 @@ import { OidcStrategy } from './oidc.strategy';
} }
return new OidcStrategy(authService, options); return new OidcStrategy(authService, options);
}, }
inject: [AuthService, ConfigurationService]
}, },
WebAuthService WebAuthService
] ]

42
apps/api/src/app/auth/oidc-state.store.ts

@ -1,3 +1,5 @@
import ms from 'ms';
/** /**
* Custom state store for OIDC authentication that doesn't rely on express-session. * Custom state store for OIDC authentication that doesn't rely on express-session.
* This store manages OAuth2 state parameters in memory with automatic cleanup. * This store manages OAuth2 state parameters in memory with automatic cleanup.
@ -7,15 +9,17 @@ export class OidcStateStore {
string, string,
{ {
appState?: unknown; appState?: unknown;
ctx: { maxAge?: number; nonce?: string; issued?: Date }; ctx: { issued?: Date; maxAge?: number; nonce?: string };
meta?: unknown; meta?: unknown;
timestamp: number; timestamp: number;
} }
>(); >();
private readonly STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes private readonly STATE_EXPIRY_MS = ms('10 minutes');
// Store request state. /**
// Signature matches passport-openidconnect SessionStore * Store request state.
* Signature matches passport-openidconnect SessionStore
*/
public store( public store(
_req: unknown, _req: unknown,
_meta: unknown, _meta: unknown,
@ -43,8 +47,10 @@ export class OidcStateStore {
} }
} }
// Verify request state. /**
// Signature matches passport-openidconnect SessionStore * Verify request state.
* Signature matches passport-openidconnect SessionStore
*/
public verify( public verify(
_req: unknown, _req: unknown,
handle: string, handle: string,
@ -76,16 +82,9 @@ export class OidcStateStore {
} }
} }
// Generate a cryptographically secure random handle /**
private generateHandle(): string { * Clean up expired states
return ( */
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15) +
Date.now().toString(36)
);
}
// Clean up expired states
private cleanup(): void { private cleanup(): void {
const now = Date.now(); const now = Date.now();
const expiredKeys: string[] = []; const expiredKeys: string[] = [];
@ -100,4 +99,15 @@ export class OidcStateStore {
this.stateMap.delete(key); this.stateMap.delete(key);
} }
} }
/**
* Generate a cryptographically secure random handle
*/
private generateHandle(): string {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15) +
Date.now().toString(36)
);
}
} }

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

@ -59,6 +59,11 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
params?.sub ?? params?.sub ??
context?.claims?.sub; context?.claims?.sub;
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.OIDC,
thirdPartyId
});
if (!thirdPartyId) { if (!thirdPartyId) {
Logger.error( Logger.error(
`Missing subject identifier in OIDC response from ${issuer}`, `Missing subject identifier in OIDC response from ${issuer}`,
@ -67,11 +72,6 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
throw new Error('Missing subject identifier in OIDC response'); throw new Error('Missing subject identifier in OIDC response');
} }
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.OIDC,
thirdPartyId
});
return { jwt }; return { jwt };
} catch (error) { } catch (error) {
Logger.error(error, 'OidcStrategy'); Logger.error(error, 'OidcStrategy');

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

@ -58,14 +58,14 @@ export class ConfigurationService {
JWT_SECRET_KEY: str({}), JWT_SECRET_KEY: str({}),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_CHART_ITEMS: num({ default: 365 }), MAX_CHART_ITEMS: num({ default: 365 }),
OIDC_AUTHORIZATION_URL: str({ default: '' }), OIDC_AUTHORIZATION_URL: str({ default: undefined }),
OIDC_CALLBACK_URL: str({ default: '' }), OIDC_CALLBACK_URL: str({ default: undefined }),
OIDC_CLIENT_ID: str({ default: '' }), OIDC_CLIENT_ID: str({ default: undefined }),
OIDC_CLIENT_SECRET: str({ default: '' }), OIDC_CLIENT_SECRET: str({ default: undefined }),
OIDC_ISSUER: str({ default: '' }), OIDC_ISSUER: str({ default: undefined }),
OIDC_SCOPE: json({ default: ['openid'] }), OIDC_SCOPE: json({ default: ['openid'] }),
OIDC_TOKEN_URL: str({ default: '' }), OIDC_TOKEN_URL: str({ default: undefined }),
OIDC_USER_INFO_URL: str({ default: '' }), OIDC_USER_INFO_URL: str({ default: undefined }),
PORT: port({ default: DEFAULT_PORT }), PORT: port({ default: DEFAULT_PORT }),
PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({
default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY

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

@ -41,7 +41,7 @@
class="mr-2" class="mr-2"
src="../assets/icons/google.svg" src="../assets/icons/google.svg"
style="height: 1rem" style="height: 1rem"
/><span i18n>Sign in with Google</span></a /><ng-container i18n>Sign in with Google</ng-container></a
> >
</div> </div>
} }
@ -52,7 +52,7 @@
class="px-4 rounded-pill" class="px-4 rounded-pill"
href="../api/auth/oidc" href="../api/auth/oidc"
mat-stroked-button mat-stroked-button
><span i18n>Sign in with OIDC</span></a ><ng-container i18n>Sign in with OIDC</ng-container></a
> >
</div> </div>
} }

4
package-lock.json

@ -83,7 +83,7 @@
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2", "passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"passport-openidconnect": "^0.1.2", "passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "18.5.0", "stripe": "18.5.0",
@ -130,7 +130,7 @@
"@types/node": "22.15.17", "@types/node": "22.15.17",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16", "@types/passport-google-oauth20": "2.0.16",
"@types/passport-openidconnect": "^0.1.3", "@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0", "@typescript-eslint/parser": "8.43.0",
"eslint": "9.35.0", "eslint": "9.35.0",

4
package.json

@ -127,7 +127,7 @@
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2", "passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"passport-openidconnect": "^0.1.2", "passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "18.5.0", "stripe": "18.5.0",
@ -174,7 +174,7 @@
"@types/node": "22.15.17", "@types/node": "22.15.17",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16", "@types/passport-google-oauth20": "2.0.16",
"@types/passport-openidconnect": "^0.1.3", "@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0", "@typescript-eslint/parser": "8.43.0",
"eslint": "9.35.0", "eslint": "9.35.0",

Loading…
Cancel
Save