Browse Source

Feature/add custom OIDC state store and remove express-session dependency

pull/5981/head
Germán Martín 3 weeks ago
parent
commit
06a933b3ac
  1. 111
      apps/api/src/app/auth/oidc-state.store.ts
  2. 26
      apps/api/src/app/auth/oidc.strategy.ts
  3. 9
      apps/api/src/main.ts
  4. 74
      package-lock.json
  5. 2
      package.json

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

@ -0,0 +1,111 @@
/**
* Custom state store for OIDC authentication that doesn't rely on express-session.
* This store manages OAuth2 state parameters in memory with automatic cleanup.
*/
export class OidcStateStore {
private stateMap = new Map<
string,
{
ctx: { maxAge?: number; nonce?: string; issued?: Date };
appState?: unknown;
meta?: unknown;
timestamp: number;
}
>();
private readonly STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes
/**
* Store request state.
* Signature matches passport-openidconnect SessionStore
*/
public store(
_req: unknown,
ctx: { maxAge?: number; nonce?: string; issued?: Date },
appState: unknown,
_meta: unknown,
callback: (err: Error | null, handle?: string) => void
): void {
try {
// Generate a unique handle for this state
const handle = this.generateHandle();
this.stateMap.set(handle, {
appState,
ctx,
meta: _meta,
timestamp: Date.now()
});
// Clean up expired states
this.cleanup();
callback(null, handle);
} catch (error) {
callback(error as Error);
}
}
/**
* Verify request state.
* Signature matches passport-openidconnect SessionStore
*/
public verify(
_req: unknown,
handle: string,
callback: (
err: Error | null,
ctx?: { maxAge?: number; nonce?: string; issued?: Date },
appState?: unknown
) => void
): void {
try {
const data = this.stateMap.get(handle);
if (!data) {
return callback(null, undefined, undefined);
}
// Check if state has expired
if (Date.now() - data.timestamp > this.STATE_EXPIRY_MS) {
this.stateMap.delete(handle);
return callback(null, undefined, undefined);
}
// Remove state after verification (one-time use)
this.stateMap.delete(handle);
callback(null, data.ctx, data.appState);
} catch (error) {
callback(error as Error);
}
}
/**
* 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)
);
}
/**
* Clean up expired states
*/
private cleanup(): void {
const now = Date.now();
const expiredKeys: string[] = [];
for (const [key, value] of this.stateMap.entries()) {
if (now - value.timestamp > this.STATE_EXPIRY_MS) {
expiredKeys.push(key);
}
}
for (const key of expiredKeys) {
this.stateMap.delete(key);
}
}
}

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

@ -1,13 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { DoneCallback } from 'passport';
import { Strategy } from 'passport-openidconnect';
import { AuthService } from './auth.service';
import { OidcStateStore } from './oidc-state.store';
@Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private static readonly stateStore = new OidcStateStore();
public constructor(
private readonly authService: AuthService,
options: any
@ -15,7 +17,8 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
super({
...options,
passReqToCallback: true,
scope: ['openid', 'profile', 'email']
scope: ['openid', 'profile', 'email'],
store: OidcStrategy.stateStore
});
}
@ -23,18 +26,29 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
_request: any,
_issuer: string,
profile: any,
done: DoneCallback
context: any,
idToken: any,
_accessToken: any,
_refreshToken: any,
params: any
) {
try {
const thirdPartyId =
params?.sub || idToken?.sub || context?.claims?.sub || profile?.id;
if (!thirdPartyId) {
throw new Error('Missing subject identifier in OIDC response');
}
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.OIDC,
thirdPartyId: profile.id
thirdPartyId
});
done(null, { jwt });
return { jwt };
} catch (error) {
Logger.error(error, 'OidcStrategy');
done(error, false);
throw error;
}
}
}

9
apps/api/src/main.ts

@ -15,7 +15,6 @@ import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { NextFunction, Request, Response } from 'express';
import * as session from 'express-session';
import helmet from 'helmet';
import { AppModule } from './app/app.module';
@ -62,14 +61,6 @@ async function bootstrap() {
})
);
app.use(
session({
resave: false,
saveUninitialized: false,
secret: configService.get<string>('JWT_SECRET_KEY')
})
);
// Support 10mb csv/json files for importing activities
app.useBodyParser('json', { limit: '10mb' });

74
package-lock.json

@ -63,7 +63,6 @@
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.0",
"express-session": "^1.18.2",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
@ -127,7 +126,6 @@
"@storybook/angular": "9.1.5",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2",
"@types/express-session": "^1.18.2",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.13",
"@types/lodash": "4.17.20",
@ -14204,16 +14202,6 @@
"@types/send": "*"
}
},
"node_modules/@types/express-session": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz",
"integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/express/node_modules/@types/express-serve-static-core": {
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
@ -22427,46 +22415,6 @@
"express": ">= 4.11"
}
},
"node_modules/express-session": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.1.0",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/express-session/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express-session/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/express/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@ -33795,6 +33743,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@ -35944,15 +35893,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -40311,18 +40251,6 @@
"node": ">=8"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"license": "MIT",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",

2
package.json

@ -109,7 +109,6 @@
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.0",
"express-session": "^1.18.2",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
@ -173,7 +172,6 @@
"@storybook/angular": "9.1.5",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2",
"@types/express-session": "^1.18.2",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.13",
"@types/lodash": "4.17.20",

Loading…
Cancel
Save