Compare commits
No commits in common. "main" and "old-main" have entirely different histories.
9 changed files with 22 additions and 567 deletions
78
README.md
78
README.md
|
|
@ -11,81 +11,5 @@ Serve il frontend Vite attraverso Express (middleware in dev, static build in pr
|
||||||
- Web Origins: https://my.patachina2.casacam.net
|
- Web Origins: https://my.patachina2.casacam.net
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
1. Copia `file.env` in '.env' e inserisci i tuoi valori (issuer discovery, client id, redirect).
|
1. Copia `.env` e inserisci i tuoi valori (issuer discovery, client id, redirect).
|
||||||
2. Installa:
|
2. Installa:
|
||||||
npm install
|
|
||||||
3. Run dev
|
|
||||||
npm run dev
|
|
||||||
4. Run public
|
|
||||||
npm run build
|
|
||||||
npm run preview
|
|
||||||
|
|
||||||
# Keycloak setting
|
|
||||||
|
|
||||||
collegarsi con keycloak come amministratore
|
|
||||||
|
|
||||||
https://auth.patachina2.casacam.net/admin
|
|
||||||
|
|
||||||
create realm x esempio demo
|
|
||||||
|
|
||||||
sul menù a tendina di Admin pulsante create realm
|
|
||||||
|
|
||||||
inserire realm name demo e lasciare enabled on
|
|
||||||
|
|
||||||
se non va da solo andare su demo nello stesso menù a tendina e poi andate su Clients
|
|
||||||
|
|
||||||
create client "my-app"
|
|
||||||
|
|
||||||
Client type OpenID connect
|
|
||||||
Client_ID my-app
|
|
||||||
client authentication on
|
|
||||||
Authorization off
|
|
||||||
|
|
||||||
rimangono spuntati
|
|
||||||
|
|
||||||
Standard Flow
|
|
||||||
Direct access grant
|
|
||||||
|
|
||||||
Valid redirect URIs url della mia app
|
|
||||||
|
|
||||||
https://my.patachina2.casacam.net/*
|
|
||||||
|
|
||||||
* significa che abiliti il redirect di qualsiasi route altrimenti metti /callback che è quella standard
|
|
||||||
|
|
||||||
Web origins
|
|
||||||
|
|
||||||
https://my.patachina2.casacam.net
|
|
||||||
|
|
||||||
lascia
|
|
||||||
|
|
||||||
Front channel logout on
|
|
||||||
Backchannel logout on
|
|
||||||
|
|
||||||
vai in credentials e copia Client Secret
|
|
||||||
|
|
||||||
questo va inserito nel file .env
|
|
||||||
|
|
||||||
vai su users e add/create new user
|
|
||||||
|
|
||||||
metti le info
|
|
||||||
|
|
||||||
Username fabio
|
|
||||||
Email fabio.micheluz@gmail.com
|
|
||||||
Fisrt name Fabio
|
|
||||||
Last name Micheluz
|
|
||||||
|
|
||||||
se metti
|
|
||||||
|
|
||||||
Email verified yes
|
|
||||||
|
|
||||||
devi settare tutto (vedi dopo)
|
|
||||||
|
|
||||||
vai su credientials e inserisci la password x es master66
|
|
||||||
|
|
||||||
questa verrà utilizzata dallo user fabio per autorizzare la connessione
|
|
||||||
|
|
||||||
se lasci
|
|
||||||
|
|
||||||
Temporary on
|
|
||||||
|
|
||||||
al primo collegamento chiede di cambiare password
|
|
||||||
8
file.env
8
file.env
|
|
@ -1,8 +0,0 @@
|
||||||
NODE_ENV=development
|
|
||||||
PORT=3000
|
|
||||||
COOKIE_SECRET=super-secret-string
|
|
||||||
KEYCLOAK_ISSUER_DISCOVERY=https://auth.patachina2.casacam.net/realms/demo/.well-known/openid-configuration
|
|
||||||
KEYCLOAK_CLIENT_ID=my-app
|
|
||||||
REDIRECT_URI=https://my.patachina2.casacam.net/auth/callback
|
|
||||||
SCOPE=openid profile email
|
|
||||||
KEYCLOAK_CLIENT_SECRET=BVWRrIWHYGiUmXpZoh7bkAWp2GmJjzGg
|
|
||||||
145
server/auth.ts
145
server/auth.ts
|
|
@ -1,144 +1,37 @@
|
||||||
import type { Client } from 'openid-client';
|
import type { Client } from 'openid-client';
|
||||||
import { generators, TokenSet } from 'openid-client';
|
import { generators } from 'openid-client';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
export function setupAuthRoutes(
|
export function setupAuthRoutes(app: any, client: Client, redirectUri: string, scope: string, cookieOptionsBase: any) {
|
||||||
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) => {
|
app.get('/auth/login', (req: Request, res: Response) => {
|
||||||
const code_verifier = generators.codeVerifier();
|
const code_verifier = generators.codeVerifier();
|
||||||
const code_challenge = generators.codeChallenge(code_verifier);
|
const code_challenge = generators.codeChallenge(code_verifier);
|
||||||
|
res.cookie('pkce_verifier', code_verifier, { ...cookieOptionsBase, maxAge: 600000 });
|
||||||
res.cookie('pkce_verifier', code_verifier, {
|
const authUrl = client.authorizationUrl({ scope, code_challenge, code_challenge_method: 'S256', redirect_uri: redirectUri });
|
||||||
...cookieOptionsBase,
|
|
||||||
maxAge: 10 * 60 * 1000, // 10 minuti
|
|
||||||
});
|
|
||||||
|
|
||||||
const authUrl = client.authorizationUrl({
|
|
||||||
scope,
|
|
||||||
code_challenge,
|
|
||||||
code_challenge_method: 'S256',
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.redirect(authUrl);
|
res.redirect(authUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback: scambia il code per tokenSet e salva access+id+refresh
|
|
||||||
app.get('/auth/callback', async (req: Request, res: Response) => {
|
app.get('/auth/callback', async (req: Request, res: Response) => {
|
||||||
try {
|
const params = client.callbackParams(req);
|
||||||
const params = client.callbackParams(req);
|
const verifier = req.signedCookies['pkce_verifier'];
|
||||||
const verifier = req.signedCookies['pkce_verifier'];
|
const tokenSet = await client.callback(redirectUri, params, { code_verifier: verifier });
|
||||||
const tokenSet = await client.callback(redirectUri, params, { code_verifier: verifier });
|
res.clearCookie('pkce_verifier');
|
||||||
|
res.cookie('access_token', tokenSet.access_token, { ...cookieOptionsBase });
|
||||||
res.clearCookie('pkce_verifier');
|
res.cookie('id_token', tokenSet.id_token, { ...cookieOptionsBase });
|
||||||
|
res.redirect('/');
|
||||||
// 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) => {
|
app.get('/api/userinfo', async (req: Request, res: Response) => {
|
||||||
try {
|
const access = req.signedCookies['access_token'];
|
||||||
const access = req.signedCookies['access_token'];
|
if (!access) return res.status(401).json({ error: 'unauthorized' });
|
||||||
const refresh = req.signedCookies['refresh_token'];
|
const userinfo = await client.userinfo(access);
|
||||||
|
res.json(userinfo);
|
||||||
// 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', (_req: Request, res: Response) => {
|
||||||
app.post('/auth/logout', async (req: Request, res: Response) => {
|
res.clearCookie('access_token');
|
||||||
const refresh = req.signedCookies['refresh_token'];
|
res.clearCookie('id_token');
|
||||||
|
res.clearCookie('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 });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
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) {
|
|
||||||
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');
|
|
||||||
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');
|
|
||||||
res.clearCookie('refresh_token');
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ import express from 'express';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { cfg } from './env';
|
import { cfg } from './env';
|
||||||
import { Issuer, TokenSet } from 'openid-client';
|
import { Issuer } from 'openid-client';
|
||||||
import { setupAuthRoutes } from './auth';
|
import { setupAuthRoutes } from './auth';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
|
@ -17,7 +17,7 @@ async function bootstrap() {
|
||||||
client_secret: cfg.clientSecret,
|
client_secret: cfg.clientSecret,
|
||||||
redirect_uris: [cfg.redirectUri],
|
redirect_uris: [cfg.redirectUri],
|
||||||
response_types: ['code'],
|
response_types: ['code'],
|
||||||
token_endpoint_auth_method: 'client_secret_basic'
|
tokenendpointauthmethod: 'clientsecret_basic'
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookieOptionsBase = {
|
const cookieOptionsBase = {
|
||||||
|
|
@ -27,35 +27,8 @@ async function bootstrap() {
|
||||||
secure: cfg.isProd
|
secure: cfg.isProd
|
||||||
};
|
};
|
||||||
|
|
||||||
// setup delle route di login/callback → salva tokenSet nel cookie
|
|
||||||
setupAuthRoutes(app, client, cfg.redirectUri, cfg.scope, cookieOptionsBase);
|
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
|
// Vite middleware in dev, static in prod
|
||||||
if (!cfg.isProd) {
|
if (!cfg.isProd) {
|
||||||
const vite = await (await import('vite')).createServer({
|
const vite = await (await import('vite')).createServer({
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue