fix: dropping the legacy surface

master
Guillermo Pages 3 months ago
parent 9f2176cd4b
commit d7b3f2a4df

@ -22,14 +22,14 @@ export { swissoidMergeAppConfigMap } from './utils/swissoidMergeAppConfigMap';
export const swissoidAppConfigMapKey = 'swissoidAppConfigMapNamespace';
// Classes and functions for direct use
export { createOIDCRoutes } from './oidc/OIDCRoutes';
export { createOidcStandardRoutes } from './oidc/OIDCStandardRoutes';
export { buildOidcConfig } from './oidc/oidcConfigBuilder';
export { SwissOIDSessionService } from './session/SwissOIDSessionService';
export { CookieManager } from './cookies/CookieManager';
export { SwissOIDJWTVerifier } from './jwt/SwissOIDJWTVerifier';
// Types
export type {
OIDCConfig,
CookieConfig,
SessionData,
DATClaims,
@ -38,4 +38,4 @@ export type {
// Re-export useful types from dependencies
export type { Request, Response, Router } from 'express';
export type { LoadDictElement } from 'di-why/build/src/DiContainer';
export type { LoadDictElement } from 'di-why/build/src/DiContainer';

@ -1,7 +1,7 @@
import { LoadDictElement } from 'di-why';
import { Express } from 'express';
import { createOidcStandardRoutes } from '../oidc/OIDCStandardRoutes';
import { buildOidcConfig } from './oidcConfigBuilder';
import { buildOidcConfig } from '../oidc/oidcConfigBuilder';
/**
* OIDC Standard Routes as a Middleware Attacher

@ -1,391 +0,0 @@
import { Router, Request, Response } from 'express';
import express from 'express';
import cookieParser from 'cookie-parser';
import * as crypto from 'crypto';
import { jwtVerify, createRemoteJWKSet, SignJWT } from 'jose';
import { OIDCConfig, SessionData } from '../types/auth.types';
export interface OIDCRoutesConfig extends OIDCConfig {
logger: any;
sessionService: any;
redisClient: any;
}
export function createOIDCRoutes(config: OIDCRoutesConfig): Router {
const router = Router();
// Add cookie parser middleware
router.use(cookieParser());
const {
logger,
sessionService,
redisClient,
issuer,
clientId,
clientSecret,
tokenEndpoint,
jwksUri,
authorizeEndpoint,
callbackUrl,
cookieDomain,
frontendUrl,
sessionCookieName,
stateSigningSecret
} = config;
// Create the secret key for signing
const secretKey = new TextEncoder().encode(stateSigningSecret);
// Cookie options
const getCookieOptions = () => ({
domain: cookieDomain,
httpOnly: true,
secure: true,
sameSite: 'lax' as const,
path: '/'
});
/**
* GET /auth/ping - Health check endpoint
*/
router.get('/auth/ping', (req: Request, res: Response) => {
res.json({
status: 'ok',
service: 'swissoid-back',
timestamp: new Date().toISOString()
});
});
/**
* GET /login - Initiates OIDC flow
*/
router.get('/login', async (req: Request, res: Response) => {
try {
const jti = crypto.randomBytes(16).toString('base64url');
const nonce = crypto.randomBytes(16).toString('base64url');
// Create signed state token
const state = await new SignJWT({
jti,
nonce,
client_id: clientId,
redirect_uri: callbackUrl
})
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('10m')
.sign(secretKey);
// Store JTI for replay prevention
await redisClient.set(
`oidc_jti:${jti}`,
'1',
'NX',
'EX',
600
);
// Set nonce cookie for backward compatibility
res.cookie('rp_nonce', nonce, {
...getCookieOptions(),
maxAge: 10 * 60 * 1000
});
logger.log('Initiating OIDC flow', {
client_id: clientId,
jti_hash: crypto.createHash('sha256').update(jti).digest('hex').substring(0, 8)
});
// Build authorization URL
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: callbackUrl,
response_type: 'code',
response_mode: 'form_post',
scope: 'openid',
state: state,
nonce: nonce
});
const authorizationUrl = `${authorizeEndpoint}?${params.toString()}`;
return res.redirect(302, authorizationUrl);
} catch (error) {
logger.error('Error initiating login', error);
return res.status(500).send('Internal server error');
}
});
/**
* POST /oidc/callback - Handles callback from SwissOID
*/
router.post('/oidc/callback',
express.urlencoded({ extended: false }),
async (req: Request, res: Response) => {
try {
const { code, state } = req.body;
if (!code || !state) {
logger.error('Missing code or state in callback');
return res.status(400).send('Missing required parameters');
}
// Verify signed state token
let statePayload: any;
try {
const { payload } = await jwtVerify(state, secretKey, {
algorithms: ['HS256'],
clockTolerance: 5
});
statePayload = payload;
} catch (verifyError) {
logger.error('State token verification failed', verifyError);
return res.status(400).send('Invalid state parameter');
}
// Enforce single-use via JTI
const jti = statePayload.jti;
const jtiKey = `oidc_jti:${jti}`;
const deleted = await redisClient.del(jtiKey);
if (deleted === 0) {
logger.error('JTI already used or expired', { jti });
return res.status(400).send('State already used or expired');
}
// Validate embedded parameters
if (statePayload.client_id !== clientId || statePayload.redirect_uri !== callbackUrl) {
logger.error('State parameter mismatch');
return res.status(400).send('Invalid state parameter');
}
// Exchange code for tokens
const tokenParams = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: callbackUrl
});
const authHeader = clientSecret
? 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
: undefined;
const tokenResponse = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(authHeader && { 'Authorization': authHeader })
},
body: tokenParams.toString()
});
if (!tokenResponse.ok) {
logger.error('Token exchange failed', { status: tokenResponse.status });
return res.status(401).send('Token exchange failed');
}
const tokenData = await tokenResponse.json() as any;
const { id_token } = tokenData;
if (!id_token) {
logger.error('No id_token in response');
return res.status(401).send('No id_token received');
}
// Verify id_token
const JWKS = createRemoteJWKSet(new URL(jwksUri));
let payload: any;
try {
const result = await jwtVerify(id_token, JWKS, {
issuer: issuer,
audience: clientId,
algorithms: ['RS256'],
clockTolerance: 5
});
payload = result.payload;
} catch (verifyError) {
logger.error('ID token verification failed', verifyError);
return res.status(401).send('Invalid id_token');
}
// Verify nonce
if (!payload.nonce || payload.nonce !== statePayload.nonce) {
logger.error('Nonce mismatch');
return res.status(401).send('Nonce mismatch');
}
// Clear nonce cookie
res.clearCookie('rp_nonce', getCookieOptions());
// Create session
const sessionId = crypto.randomBytes(24).toString('base64url');
await sessionService.createSession(sessionId, {
sub: payload.sub,
iat: payload.iat,
exp: payload.exp,
email: payload.email,
name: payload.name
});
// Create transit token for finalize endpoint
const transitToken = crypto.randomBytes(32).toString('base64url');
await redisClient.set(
`login_tx:${transitToken}`,
sessionId,
'EX',
60
);
// Redirect to finalize endpoint
const finalizeUrl = `${callbackUrl.replace('/oidc/callback', '/oidc/finalize')}?tx=${transitToken}`;
return res.redirect(303, finalizeUrl);
} catch (error) {
logger.error('Error processing OIDC callback', error);
return res.status(500).send('Internal server error');
}
});
/**
* GET /oidc/finalize - Sets session cookie in first-party context
*/
router.get('/oidc/finalize', async (req: Request, res: Response) => {
try {
const { tx } = req.query;
if (!tx || typeof tx !== 'string') {
logger.error('Missing transit token');
return res.status(400).send('Missing transit token');
}
// Retrieve and delete session ID from Redis
const transitKey = `login_tx:${tx}`;
const sessionId = await redisClient.get(transitKey);
if (!sessionId) {
logger.error('Transit token not found or expired');
return res.status(400).send('Transit token expired');
}
await redisClient.del(transitKey);
// Verify session exists
const sessionData = await sessionService.getSession(sessionId);
if (!sessionData) {
logger.error('Session not found');
return res.status(400).send('Session not found');
}
// Set session cookie
res.cookie(sessionCookieName, sessionId, {
...getCookieOptions(),
maxAge: 7 * 24 * 60 * 60 * 1000
});
// Redirect to app
const redirectUrl = `${frontendUrl}/workspace`;
return res.redirect(302, redirectUrl);
} catch (error) {
logger.error('Error in finalize endpoint', error);
return res.status(500).send('Internal server error');
}
});
/**
* GET /auth/status - Returns authentication status
*/
router.get('/auth/status', async (req: Request, res: Response) => {
try {
const sessionId = req.cookies?.[sessionCookieName];
if (!sessionId) {
return res.json({ authenticated: false });
}
const session = await sessionService.getSession(sessionId);
if (!session) {
return res.json({ authenticated: false });
}
return res.json({
authenticated: true,
user: {
id: session.sub,
UUID: session.sub,
email: session.email,
username: session.email,
name: session.name,
...session
}
});
} catch (error) {
logger.error('Error checking auth status', error);
return res.json({ authenticated: false });
}
});
/**
* GET/POST /auth/logout - Destroys session and clears cookies
*/
const logoutHandler = async (req: Request, res: Response) => {
try {
const sessionId = req.cookies?.[sessionCookieName];
if (sessionId) {
await sessionService.destroySession(sessionId);
}
res.clearCookie(sessionCookieName, getCookieOptions());
if (req.method === 'GET') {
return res.redirect('/login');
} else {
return res.json({ success: true });
}
} catch (error) {
logger.error('Error during logout', error);
if (req.method === 'GET') {
return res.redirect('/login');
} else {
return res.status(500).json({ error: 'Logout failed' });
}
}
};
router.get('/auth/logout', logoutHandler);
router.post('/auth/logout', logoutHandler);
/**
* GET /auth/userinfo - Returns user information (if authenticated)
*/
router.get('/auth/userinfo', async (req: Request, res: Response) => {
try {
const sessionId = req.cookies?.[sessionCookieName];
if (!sessionId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const session = await sessionService.getSession(sessionId);
if (!session) {
return res.status(401).json({ error: 'Invalid session' });
}
return res.json({
sub: session.sub,
email: session.email,
name: session.name,
...session
});
} catch (error) {
logger.error('Error fetching user info', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
return router;
}

@ -1,34 +0,0 @@
import { LoadDictElement } from 'di-why/build/src/DiContainer';
import { Router } from 'express';
import { createOIDCRoutes } from './OIDCRoutes';
import { OIDCConfig } from '../types/auth.types';
export const oidcRoutesLDEGen = (config?: Partial<OIDCConfig>): LoadDictElement<Router> => ({
factory: ({ logger, sessionService, redisClient, appConfig }) => {
const finalConfig = {
logger,
sessionService,
redisClient,
issuer: config?.issuer || appConfig.swissoidIssuer,
clientId: config?.clientId || appConfig.swissoidClientId,
clientSecret: config?.clientSecret || appConfig.swissoidClientSecret,
tokenEndpoint: config?.tokenEndpoint || appConfig.swissoidTokenEndpoint,
jwksUri: config?.jwksUri || appConfig.swissoidJwksUri,
authorizeEndpoint: config?.authorizeEndpoint || appConfig.swissoidAuthorizeEndpoint,
callbackUrl: config?.callbackUrl || appConfig.rpCallbackUrl,
cookieDomain: config?.cookieDomain || appConfig.rpCookieDomain,
frontendUrl: config?.frontendUrl || appConfig.rpFrontendUrl,
sessionCookieName: config?.sessionCookieName || appConfig.sessionCookieName,
sessionSecret: config?.sessionSecret || appConfig.sessionSecret,
stateSigningSecret: config?.stateSigningSecret || appConfig.stateSigningSecret
};
return createOIDCRoutes(finalConfig);
},
locateDeps: {
logger: 'logger',
sessionService: 'sessionService',
redisClient: 'redisClient',
appConfig: 'appConfig'
}
});

@ -1,46 +0,0 @@
import { LoadDictElement } from 'di-why/build/src/DiContainer';
import { Express } from 'express';
import { createOIDCRoutes } from './OIDCRoutes';
import { OIDCConfig } from '../types/auth.types';
/**
* Loader that creates OIDC routes and mounts them on Express app
* Returns a string to indicate completion (cronide-user pattern)
*/
export const oidcRoutesMounterLDEGen = (config?: Partial<OIDCConfig>): LoadDictElement<string> => ({
factory: ({ app, logger, sessionService, redisClient, appConfig }) => {
const finalConfig = {
logger,
sessionService,
redisClient,
issuer: config?.issuer || appConfig.swissoidIssuer,
clientId: config?.clientId || appConfig.swissoidClientId,
clientSecret: config?.clientSecret || appConfig.swissoidClientSecret,
tokenEndpoint: config?.tokenEndpoint || appConfig.swissoidTokenEndpoint,
jwksUri: config?.jwksUri || appConfig.swissoidJwksUri,
authorizeEndpoint: config?.authorizeEndpoint || appConfig.swissoidAuthorizeEndpoint,
callbackUrl: config?.callbackUrl || appConfig.rpCallbackUrl,
cookieDomain: config?.cookieDomain || appConfig.rpCookieDomain || appConfig.cookieDomain,
frontendUrl: config?.frontendUrl || appConfig.rpFrontendUrl,
sessionCookieName: config?.sessionCookieName || appConfig.sessionCookieName,
sessionSecret: config?.sessionSecret || appConfig.sessionSecret,
stateSigningSecret: config?.stateSigningSecret || appConfig.stateSigningSecret
};
const router = createOIDCRoutes(finalConfig);
// Mount the routes on the Express app
(app as Express).use(router);
logger.log('Standard OIDC routes mounted - /login, POST /oidc/callback, /auth/status, /auth/userinfo, /auth/logout');
return 'oidcStandardRoutes-loaded';
},
locateDeps: {
app: 'app',
logger: 'logger',
sessionService: 'sessionService',
appConfig: 'appConfig',
redisClient: 'redisClient'
}
});

@ -1,23 +1,3 @@
export interface OIDCConfig {
// SwissOID settings
issuer: string; // https://api.swissoid.com
clientId: string; // Application client ID
clientSecret?: string; // For confidential clients
tokenEndpoint: string; // https://api.swissoid.com/token
jwksUri: string; // https://api.swissoid.com/.well-known/jwks.json
authorizeEndpoint: string; // https://api.swissoid.com/authorize
// RP settings
callbackUrl: string; // Where SwissOID redirects back to
cookieDomain: string; // Domain for cookies (e.g., .example.com)
frontendUrl: string; // Frontend application URL
// Session settings
sessionCookieName: string; // Name for session cookie
sessionSecret: string; // Secret for session encryption
stateSigningSecret: string; // Secret for signing state parameter
}
export interface CookieConfig {
domain: string;
sessionName: string;
@ -57,4 +37,4 @@ export interface SwissOIDSessionConfig {
sameSite?: 'strict' | 'lax' | 'none';
sessionTTL?: number; // In seconds
refreshTTL?: number; // In seconds
}
}

Loading…
Cancel
Save