Compare commits

..

10 commits

Author SHA1 Message Date
8a389f301d Aggiorna README.md 2025-12-04 00:16:25 +08:00
bad69128cf Cari file su "/" 2025-12-04 00:14:56 +08:00
cf53bb4a53 Cari file su "server" 2025-12-04 00:13:41 +08:00
17c0040ca1 Elimina server/auth.ts 2025-12-04 00:13:03 +08:00
5a61a2a03e Cari file su "server" 2025-12-04 00:10:04 +08:00
fb8aeac116 Elimina auth.ts 2025-12-04 00:09:48 +08:00
26cf018f61 Cari file su "/" 2025-12-04 00:09:28 +08:00
a0c7629607 Aggiorna server/auth.ts.old2 2025-12-04 00:07:01 +08:00
29c821464a Aggiorna README.md 2025-12-04 00:06:13 +08:00
147ef01b2e nuovi files 2025-12-01 09:27:16 +01:00
9 changed files with 567 additions and 22 deletions

View file

@ -11,5 +11,81 @@ Serve il frontend Vite attraverso Express (middleware in dev, static build in pr
- Web Origins: https://my.patachina2.casacam.net
## Setup
1. Copia `.env` e inserisci i tuoi valori (issuer discovery, client id, redirect).
1. Copia `file.env` in '.env' e inserisci i tuoi valori (issuer discovery, client id, redirect).
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 Normal file
View file

@ -0,0 +1,8 @@
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

View file

@ -1,37 +1,144 @@
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) {
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) => {
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', { ...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' });
}
});
app.post('/auth/logout', (_req: Request, res: Response) => {
res.clearCookie('access_token');
res.clearCookie('id_token');
res.clearCookie('refresh_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 });
});
}

37
server/auth.ts.old Normal file
View file

@ -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 });
});
}

123
server/auth.ts.old2 Normal file
View file

@ -0,0 +1,123 @@
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 });
});
}

View file

@ -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({

81
server/index.ts.new Normal file
View file

@ -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);
});

54
server/index.ts.old Normal file
View file

@ -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);
});

View file

@ -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();
};
}