From 147ef01b2ec9b4855542cd73670e0895d129f6cd Mon Sep 17 00:00:00 2001 From: Fabio Date: Mon, 1 Dec 2025 09:27:16 +0100 Subject: [PATCH] nuovi files --- server/auth.ts | 116 ++++++++++++++++++++++---- server/auth.ts.old | 37 ++++++++ server/index.ts | 31 ++++++- server/index.ts.new | 81 ++++++++++++++++++ server/index.ts.old | 54 ++++++++++++ server/middleware/ensureFreshToken.ts | 32 +++++++ 6 files changed, 334 insertions(+), 17 deletions(-) create mode 100644 server/auth.ts.old create mode 100644 server/index.ts.new create mode 100644 server/index.ts.old create mode 100644 server/middleware/ensureFreshToken.ts diff --git a/server/auth.ts b/server/auth.ts index e1defdb..5228fd8 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -1,33 +1,119 @@ import type { Client } from 'openid-client'; -import { generators } 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) { +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: 600000 }); - const authUrl = client.authorizationUrl({ scope, code_challenge, code_challenge_method: 'S256', redirect_uri: redirectUri }); + + 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) => { - 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'); - res.cookie('access_token', tokenSet.access_token, { ...cookieOptionsBase }); - res.cookie('id_token', tokenSet.id_token, { ...cookieOptionsBase }); - res.redirect('/'); + 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) { + 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) => { - const access = req.signedCookies['access_token']; - if (!access) return res.status(401).json({ error: 'unauthorized' }); - const userinfo = await client.userinfo(access); - res.json(userinfo); + 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'); + 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', (_req: Request, res: Response) => { res.clearCookie('access_token'); res.clearCookie('id_token'); diff --git a/server/auth.ts.old b/server/auth.ts.old new file mode 100644 index 0000000..e1defdb --- /dev/null +++ b/server/auth.ts.old @@ -0,0 +1,37 @@ +import type { Client } from 'openid-client'; +import { generators } from 'openid-client'; +import { Request, Response } from 'express'; + +export function setupAuthRoutes(app: any, client: Client, redirectUri: string, scope: string, cookieOptionsBase: any) { + 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: 600000 }); + const authUrl = client.authorizationUrl({ scope, code_challenge, code_challenge_method: 'S256', redirect_uri: redirectUri }); + res.redirect(authUrl); + }); + + app.get('/auth/callback', async (req: Request, res: Response) => { + 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'); + res.cookie('access_token', tokenSet.access_token, { ...cookieOptionsBase }); + res.cookie('id_token', tokenSet.id_token, { ...cookieOptionsBase }); + res.redirect('/'); + }); + + app.get('/api/userinfo', async (req: Request, res: Response) => { + const access = req.signedCookies['access_token']; + if (!access) return res.status(401).json({ error: 'unauthorized' }); + const userinfo = await client.userinfo(access); + res.json(userinfo); + }); + + app.post('/auth/logout', (_req: Request, res: Response) => { + res.clearCookie('access_token'); + res.clearCookie('id_token'); + res.clearCookie('refresh_token'); + res.json({ ok: true }); + }); +} diff --git a/server/index.ts b/server/index.ts index ba8e3fe..17d7bb5 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2,7 +2,7 @@ import express from 'express'; import cookieParser from 'cookie-parser'; import path from 'path'; import { cfg } from './env'; -import { Issuer } from 'openid-client'; +import { Issuer, TokenSet } from 'openid-client'; import { setupAuthRoutes } from './auth'; async function bootstrap() { @@ -17,7 +17,7 @@ async function bootstrap() { client_secret: cfg.clientSecret, redirect_uris: [cfg.redirectUri], response_types: ['code'], - tokenendpointauthmethod: 'clientsecret_basic' + token_endpoint_auth_method: 'client_secret_basic' }); const cookieOptionsBase = { @@ -27,8 +27,35 @@ async function bootstrap() { secure: cfg.isProd }; + // setup delle route di login/callback → salva tokenSet nel cookie setupAuthRoutes(app, client, cfg.redirectUri, cfg.scope, cookieOptionsBase); + // 👉 Middleware per refresh automatico sulle rotte protette + app.use('/api', async (req, res, next) => { + const raw = req.signedCookies?.tokenSet; + if (!raw) return res.redirect('/login'); + + const tokenSet = new TokenSet(raw); + + if (tokenSet.expired()) { + try { + const refreshed = await client.refresh(tokenSet.refresh_token); + res.cookie('tokenSet', refreshed, { + ...cookieOptionsBase, + maxAge: 7 * 24 * 60 * 60 * 1000 // opzionale: persistenza 7 giorni + }); + console.log('[OIDC] Access token rinnovato automaticamente'); + } catch (err) { + console.error('[OIDC] Errore nel refresh:', err); + res.clearCookie('tokenSet'); + return res.redirect('/login'); + } + } + next(); + }, (req, res) => { + res.json({ message: 'Accesso con token valido!' }); + }); + // Vite middleware in dev, static in prod if (!cfg.isProd) { const vite = await (await import('vite')).createServer({ diff --git a/server/index.ts.new b/server/index.ts.new new file mode 100644 index 0000000..17d7bb5 --- /dev/null +++ b/server/index.ts.new @@ -0,0 +1,81 @@ +import express from 'express'; +import cookieParser from 'cookie-parser'; +import path from 'path'; +import { cfg } from './env'; +import { Issuer, TokenSet } from 'openid-client'; +import { setupAuthRoutes } from './auth'; + +async function bootstrap() { + const app = express(); + app.use(cookieParser(cfg.cookieSecret)); + app.use(express.json()); + + // OIDC discovery e client + const issuer = await Issuer.discover(cfg.issuerDiscoveryUrl); + const client = new issuer.Client({ + client_id: cfg.clientId, + client_secret: cfg.clientSecret, + redirect_uris: [cfg.redirectUri], + response_types: ['code'], + token_endpoint_auth_method: 'client_secret_basic' + }); + + const cookieOptionsBase = { + httpOnly: true, + signed: true, + sameSite: 'lax' as const, + secure: cfg.isProd + }; + + // setup delle route di login/callback → salva tokenSet nel cookie + setupAuthRoutes(app, client, cfg.redirectUri, cfg.scope, cookieOptionsBase); + + // 👉 Middleware per refresh automatico sulle rotte protette + app.use('/api', async (req, res, next) => { + const raw = req.signedCookies?.tokenSet; + if (!raw) return res.redirect('/login'); + + const tokenSet = new TokenSet(raw); + + if (tokenSet.expired()) { + try { + const refreshed = await client.refresh(tokenSet.refresh_token); + res.cookie('tokenSet', refreshed, { + ...cookieOptionsBase, + maxAge: 7 * 24 * 60 * 60 * 1000 // opzionale: persistenza 7 giorni + }); + console.log('[OIDC] Access token rinnovato automaticamente'); + } catch (err) { + console.error('[OIDC] Errore nel refresh:', err); + res.clearCookie('tokenSet'); + return res.redirect('/login'); + } + } + next(); + }, (req, res) => { + res.json({ message: 'Accesso con token valido!' }); + }); + + // Vite middleware in dev, static in prod + if (!cfg.isProd) { + const vite = await (await import('vite')).createServer({ + root: path.join(process.cwd(), 'client'), + server: { middlewareMode: true, hmr: false, host: '0.0.0.0' }, + plugins: [(await import('@vitejs/plugin-react')).default()] + }); + app.use(vite.middlewares); + } else { + const dist = path.join(process.cwd(), 'client', 'dist'); + app.use(express.static(dist)); + app.get('*', (_req, res) => res.sendFile(path.join(dist, 'index.html'))); + } + + app.listen(cfg.port, '192.168.1.3', () => + console.log(`Server running on http://192.168.1.3:${cfg.port}`) + ); +} + +bootstrap().catch((err) => { + console.error('Bootstrap failed:', err); + process.exit(1); +}); diff --git a/server/index.ts.old b/server/index.ts.old new file mode 100644 index 0000000..ba8e3fe --- /dev/null +++ b/server/index.ts.old @@ -0,0 +1,54 @@ +import express from 'express'; +import cookieParser from 'cookie-parser'; +import path from 'path'; +import { cfg } from './env'; +import { Issuer } from 'openid-client'; +import { setupAuthRoutes } from './auth'; + +async function bootstrap() { + const app = express(); + app.use(cookieParser(cfg.cookieSecret)); + app.use(express.json()); + + // OIDC discovery e client + const issuer = await Issuer.discover(cfg.issuerDiscoveryUrl); + const client = new issuer.Client({ + client_id: cfg.clientId, + client_secret: cfg.clientSecret, + redirect_uris: [cfg.redirectUri], + response_types: ['code'], + tokenendpointauthmethod: 'clientsecret_basic' + }); + + const cookieOptionsBase = { + httpOnly: true, + signed: true, + sameSite: 'lax' as const, + secure: cfg.isProd + }; + + setupAuthRoutes(app, client, cfg.redirectUri, cfg.scope, cookieOptionsBase); + + // Vite middleware in dev, static in prod + if (!cfg.isProd) { + const vite = await (await import('vite')).createServer({ + root: path.join(process.cwd(), 'client'), + server: { middlewareMode: true, hmr: false, host: '0.0.0.0' }, + plugins: [(await import('@vitejs/plugin-react')).default()] + }); + app.use(vite.middlewares); + } else { + const dist = path.join(process.cwd(), 'client', 'dist'); + app.use(express.static(dist)); + app.get('*', (_req, res) => res.sendFile(path.join(dist, 'index.html'))); + } + + app.listen(cfg.port, '192.168.1.3', () => + console.log(`Server running on http://192.168.1.3:${cfg.port}`) + ); +} + +bootstrap().catch((err) => { + console.error('Bootstrap failed:', err); + process.exit(1); +}); diff --git a/server/middleware/ensureFreshToken.ts b/server/middleware/ensureFreshToken.ts new file mode 100644 index 0000000..d5947a1 --- /dev/null +++ b/server/middleware/ensureFreshToken.ts @@ -0,0 +1,32 @@ +import { TokenSet } from "openid-client"; +import type { Client } from "openid-client"; + +export function makeEnsureFreshToken(client: Client) { + return async function ensureFreshToken(req, res, next) { + if (!req.signedCookies?.tokenSet) { + return next(); // nessun token → passa oltre + } + + const tokenSet = new TokenSet(req.signedCookies.tokenSet); + + if (tokenSet.expired()) { + try { + const refreshed = await client.refresh(tokenSet.refresh_token); + // aggiorna il cookie firmato + res.cookie("tokenSet", refreshed, { + httpOnly: true, + signed: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + }); + console.log("[OIDC] Access token rinnovato automaticamente"); + } catch (err) { + console.error("[OIDC] Errore nel refresh:", err); + res.clearCookie("tokenSet"); + return res.redirect("/login"); + } + } + + next(); + }; +}