mirror of https://github.com/ghostfolio/ghostfolio
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.
114 lines
2.6 KiB
114 lines
2.6 KiB
import ms from 'ms';
|
|
|
|
/**
|
|
* 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 readonly STATE_EXPIRY_MS = ms('10 minutes');
|
|
|
|
private stateMap = new Map<
|
|
string,
|
|
{
|
|
appState?: unknown;
|
|
ctx: { issued?: Date; maxAge?: number; nonce?: string };
|
|
meta?: unknown;
|
|
timestamp: number;
|
|
}
|
|
>();
|
|
|
|
/**
|
|
* Store request state.
|
|
* Signature matches passport-openidconnect SessionStore
|
|
*/
|
|
public store(
|
|
_req: unknown,
|
|
_meta: unknown,
|
|
appState: unknown,
|
|
ctx: { maxAge?: number; nonce?: string; issued?: Date },
|
|
callback: (err: Error | null, handle?: string) => 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,
|
|
appState?: unknown,
|
|
ctx?: { maxAge?: number; nonce?: string; issued?: Date }
|
|
) => void
|
|
) {
|
|
try {
|
|
const data = this.stateMap.get(handle);
|
|
|
|
if (!data) {
|
|
return callback(null, undefined, undefined);
|
|
}
|
|
|
|
if (Date.now() - data.timestamp > this.STATE_EXPIRY_MS) {
|
|
// State has expired
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 (
|
|
Math.random().toString(36).substring(2, 15) +
|
|
Math.random().toString(36).substring(2, 15) +
|
|
Date.now().toString(36)
|
|
);
|
|
}
|
|
}
|
|
|