You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

119 lines
2.5 KiB

import type { Request } from 'express';
import ms from 'ms';
import { randomBytes } from 'node:crypto';
import type {
SessionStore,
SessionStoreCallback,
SessionStoreContext,
SessionVerifyCallback
} from 'passport-openidconnect';
/**
* 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 implements SessionStore {
private readonly STATE_EXPIRY_MS = ms('10 minutes');
private stateMap = new Map<
string,
{
appState?: unknown;
ctx: SessionStoreContext;
meta?: unknown;
timestamp: number;
}
>();
/**
* Store request state.
*/
public store(
_req: Request,
ctx: SessionStoreContext,
appState: unknown,
meta: unknown,
callback: SessionStoreCallback
) {
try {
// Generate a unique handle for this state
const handle = this.generateHandle();
this.stateMap.set(handle, {
appState,
ctx,
meta,
timestamp: Date.now()
});
// Clean up expired states
this.cleanup();
callback(null, handle);
} catch (error) {
callback(error as Error);
}
}
/**
* Verify request state.
*/
public verify(
_req: Request,
handle: string,
callback: SessionVerifyCallback
) {
try {
const data = this.stateMap.get(handle);
if (!data) {
return callback(
new Error('Invalid OIDC state parameter'),
undefined,
undefined
);
}
if (Date.now() - data.timestamp > this.STATE_EXPIRY_MS) {
this.stateMap.delete(handle);
return callback(
new Error('OIDC state has expired, please try again'),
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);
}
}
/**
* Clean up expired states
*/
private cleanup() {
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);
}
}
/**
* Generate a cryptographically secure random handle
*/
private generateHandle() {
return randomBytes(32).toString('hex');
}
}