diff --git a/server/auth.ts b/server/auth.ts new file mode 100644 index 0000000..18da15e --- /dev/null +++ b/server/auth.ts @@ -0,0 +1,144 @@ +import type { Client } from 'openid-client'; +import { generators, TokenSet } from 'openid-client'; +import { Request, Response } from 'express'; + +export function setupAuthRoutes( + app: any, + client: Client, + redirectUri: string, + scope: string, + cookieOptionsBase: any +) { + // Login: genera PKCE e reindirizza a Keycloak + app.get('/auth/login', (req: Request, res: Response) => { + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + + res.cookie('pkce_verifier', code_verifier, { + ...cookieOptionsBase, + maxAge: 10 * 60 * 1000, // 10 minuti + }); + + const authUrl = client.authorizationUrl({ + scope, + code_challenge, + code_challenge_method: 'S256', + redirect_uri: redirectUri, + }); + + res.redirect(authUrl); + }); + + // Callback: scambia il code per tokenSet e salva access+id+refresh + app.get('/auth/callback', async (req: Request, res: Response) => { + try { + const params = client.callbackParams(req); + const verifier = req.signedCookies['pkce_verifier']; + const tokenSet = await client.callback(redirectUri, params, { code_verifier: verifier }); + + res.clearCookie('pkce_verifier'); + + // Salva tutti i token necessari come cookie firmati e httpOnly + res.cookie('access_token', tokenSet.access_token, { ...cookieOptionsBase }); + res.cookie('id_token', tokenSet.id_token, { ...cookieOptionsBase }); + + // Importante: salva anche il refresh_token per il rinnovo automatico + if (tokenSet.refresh_token) { + res.cookie('refresh_token', tokenSet.refresh_token, { + ...cookieOptionsBase, + // opzionale: persistenza, deve essere <= refresh token lifetime configurato in Keycloak + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 giorni + }); + } + + res.redirect('/'); + } catch (err) { + const refresh = req.signedCookies['refresh_token']; + + if (refresh) { + await client.revoke(refresh, 'refresh_token'); + } + + res.clearCookie('access_token', { ...cookieOptionsBase }); + res.clearCookie('id_token', { ...cookieOptionsBase }); + res.clearCookie('refresh_token', { ...cookieOptionsBase }); + console.error('[OIDC] Errore callback:', err); + res.status(500).json({ error: 'login_failed' }); + } + }); + + // Userinfo: usa SEMPRE l'access token, facendo refresh se scaduto + app.get('/api/userinfo', async (req: Request, res: Response) => { + try { + const access = req.signedCookies['access_token']; + const refresh = req.signedCookies['refresh_token']; + + // Se non ho proprio token, è 401 + if (!access && !refresh) { + return res.status(401).json({ error: 'unauthorized' }); + } + + // Provo userinfo con l'access token corrente + try { + const userinfo = await client.userinfo(access); + return res.json(userinfo); + } catch (err) { + // Se fallisce, può essere scaduto: provo il refresh se disponibile + if (!refresh) { + return res.status(401).json({ error: 'invalid_or_expired_token' }); + } + try { + const refreshed = await client.refresh(refresh); + + // Aggiorna i cookie con i nuovi token + if (refreshed.access_token) { + res.cookie('access_token', refreshed.access_token, { ...cookieOptionsBase }); + } + if (refreshed.id_token) { + res.cookie('id_token', refreshed.id_token, { ...cookieOptionsBase }); + } + if (refreshed.refresh_token) { + // alcuni IdP ruotano il refresh_token: sempre sovrascrivere se presente + res.cookie('refresh_token', refreshed.refresh_token, { + ...cookieOptionsBase, + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + } + + const userinfo = await client.userinfo(refreshed.access_token); + return res.json(userinfo); + } catch (refreshErr) { + console.error('[OIDC] Errore nel refresh:', refreshErr); + // pulizia e 401 → sessione non più valida + res.clearCookie('access_token', { ...cookieOptionsBase }); + res.clearCookie('id_token', { ...cookieOptionsBase }); + res.clearCookie('refresh_token', { ...cookieOptionsBase }); + //res.clearCookie('access_token'); + //res.clearCookie('id_token'); + //res.clearCookie('refresh_token'); + return res.status(401).json({ error: 'refresh_failed' }); + } + } + } catch (outerErr) { + console.error('[OIDC] Errore userinfo:', outerErr); + return res.status(401).json({ error: 'invalid_token' }); + } + }); + + // Logout: pulisce tutti i cookie + app.post('/auth/logout', async (req: Request, res: Response) => { + const refresh = req.signedCookies['refresh_token']; + + if (refresh) { + await client.revoke(refresh, 'refresh_token'); + } + + res.clearCookie('access_token', { ...cookieOptionsBase }); + res.clearCookie('id_token', { ...cookieOptionsBase }); + res.clearCookie('refresh_token', { ...cookieOptionsBase }); + //res.clearCookie('access_token'); + //res.clearCookie('id_token'); + //res.clearCookie('refresh_token'); + res.json({ ok: true }); + }); +}