diff --git a/src/loaders/oidcStandardRoutesMiddleware.ts b/src/loaders/oidcStandardRoutesMiddleware.ts index dcae128..3ac7aa1 100644 --- a/src/loaders/oidcStandardRoutesMiddleware.ts +++ b/src/loaders/oidcStandardRoutesMiddleware.ts @@ -8,18 +8,28 @@ import { buildOidcConfig } from '../oidc/oidcConfigBuilder'; * Compatible with express-knifey's middleware system */ const loadDictElement: LoadDictElement<(path: string | '*') => void> = { + before: async ({ serviceLocator, deps }) => { + if (serviceLocator.couldLoad('oidcUserRegistrar')) { + const onUserAuthenticated = await serviceLocator.get('oidcUserRegistrar'); + return { ...deps, onUserAuthenticated }; + } + return deps; + }, factory: ({ app, logger, sessionService, appConfig, - redisClient + redisClient, + onUserAuthenticated }) => { // Build configuration using shared builder + const baseConfig = buildOidcConfig(appConfig, redisClient); const config = { logger, sessionService, - ...buildOidcConfig(appConfig, redisClient) + ...baseConfig, + ...(onUserAuthenticated ? { onUserAuthenticated } : {}) }; const router = createOidcStandardRoutes(config); @@ -42,4 +52,4 @@ const loadDictElement: LoadDictElement<(path: string | '*') => void> = { } }; -export default loadDictElement; \ No newline at end of file +export default loadDictElement; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..0c38b33 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,11 @@ +import type { MiddlewareHandle } from 'express-knifey'; + +export const SWISSOID_MIDDLEWARE = { + oidcStandardRoutes: { + name: 'oidcStandardRoutesMiddleware', + defaultPriority: 50, + defaultPath: '*', + } satisfies MiddlewareHandle, +} as const; + +export type SwissoidMiddlewareHandle = typeof SWISSOID_MIDDLEWARE[keyof typeof SWISSOID_MIDDLEWARE]; diff --git a/src/oidc/OIDCStandardRoutes.ts b/src/oidc/OIDCStandardRoutes.ts index 6c4f884..2a3dba5 100644 --- a/src/oidc/OIDCStandardRoutes.ts +++ b/src/oidc/OIDCStandardRoutes.ts @@ -11,6 +11,14 @@ import { jwtVerify, createRemoteJWKSet, SignJWT } from 'jose'; * Using stateless signed state to avoid third-party cookie issues */ +export interface OnUserAuthenticatedEvent { + claims: Record; + tokenResponse: Record; + request: Request; +} + +type OnUserAuthenticatedHandler = (event: OnUserAuthenticatedEvent) => Promise; + interface OidcStandardConfig { logger: any; sessionService: any; @@ -35,6 +43,9 @@ interface OidcStandardConfig { // State signing secret (should be different from session secret) stateSigningSecret: string; + + // Optional hook invoked after id_token verification succeeds + onUserAuthenticated?: OnUserAuthenticatedHandler; } export function createOidcStandardRoutes(config: OidcStandardConfig): Router { @@ -299,6 +310,19 @@ export function createOidcStandardRoutes(config: OidcStandardConfig): Router { return res.status(401).send('Nonce mismatch'); } + if (config.onUserAuthenticated) { + try { + await config.onUserAuthenticated({ + claims: payload, + tokenResponse: tokenData, + request: req, + }); + } catch (handlerError) { + logger.error('onUserAuthenticated handler failed', handlerError); + return res.status(500).send('Unable to process user registration'); + } + } + // Clear the optional nonce cookie if it was set res.clearCookie('rp_nonce', getCookieOptions()); @@ -631,4 +655,4 @@ export function createOidcStandardRoutes(config: OidcStandardConfig): Router { router.post('/auth/logout', logoutHandler); return router; -} \ No newline at end of file +} diff --git a/src/oidc/oidcConfigBuilder.ts b/src/oidc/oidcConfigBuilder.ts index 3f2ae0d..22b249c 100644 --- a/src/oidc/oidcConfigBuilder.ts +++ b/src/oidc/oidcConfigBuilder.ts @@ -29,4 +29,4 @@ export function buildOidcConfig(appConfig: SwissoidAppConfig, redisClient: Redis // State signing secret stateSigningSecret: appConfig.stateSigningSecret || appConfig.sessionSecret + '-state-signing' }; -} \ No newline at end of file +}