first commit

This commit is contained in:
Fabio 2025-11-30 17:04:07 +01:00
commit 9b5f083aec
19 changed files with 3754 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
dist
.env
client/.vite
client/dist

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# Keycloak OIDC + Vite + Express
Progetto full-stack con login tramite OIDC/PKCE su Keycloak, usando `openid-client`.
Serve il frontend Vite attraverso Express (middleware in dev, static build in prod).
## Requisiti
- Node 18+
- Keycloak con realm e client configurati:
- Client type: Public (PKCE)
- Valid Redirect URIs: https://my.patachina2.casacam.net/auth/callback
- Web Origins: https://my.patachina2.casacam.net
## Setup
1. Copia `.env` e inserisci i tuoi valori (issuer discovery, client id, redirect).
2. Installa:

15
README1.md Normal file
View file

@ -0,0 +1,15 @@
├── @types/cookie-parser@1.4.10
├── @types/express@4.17.25
├── @types/react-dom@19.2.3
├── @types/react@19.2.7
├── @vitejs/plugin-react@4.7.0
├── cookie-parser@1.4.7
├── cross-env@7.0.3
├── dotenv@17.2.3
├── express@4.21.2
├── openid-client@5.7.1
├── react-dom@19.2.0
├── react@19.2.0
├── ts-node-dev@2.0.0
├── typescript@5.9.3
└── vite@5.4.21

12
client/index.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Keycloak OIDC Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

28
client/src/App.tsx Normal file
View file

@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';
import { getUserinfo } from './api';
export default function App() {
const [user, setUser] = useState<any>(null);
const login = () => window.location.href = '/auth/login';
const logout = () => fetch('/auth/logout', { method: 'POST' }).then(() => setUser(null));
useEffect(() => {
getUserinfo().then(setUser).catch(() => setUser(null));
}, []);
return (
<div style={{ padding: 24 }}>
<h1>Demo OIDC con Keycloak</h1>
{user ? (
<>
<p>Utente: {user.name || user.preferred_username}</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={login}>Login con Keycloak</button>
)}
</div>
);
}

5
client/src/api.ts Normal file
View file

@ -0,0 +1,5 @@
export async function getUserinfo() {
const r = await fetch('/api/userinfo', { credentials: 'include' });
if (!r.ok) throw new Error('userinfo failed');
return r.json();
}

9
client/src/main.tsx Normal file
View file

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

17
client/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Node",
"rootDir": ".",
"outDir": "dist",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"server/**/*.ts",
"client/src/**/*.ts",
"client/src/**/*.tsx"
]
}

13
client/vite.config.ts Normal file
View file

@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
hmr: false,
host: '192.168.1.3',
allowedHosts: [
'my.patachina2.casacam.net' // 👈 aggiungi qui il tuo host
]
}
});

13
client/vite.config.ts.ok Normal file
View file

@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
hmr: false,
host: '192.168.1.3',
allowedHosts: [
'my.patachina2.casacam.net' // 👈 aggiungi qui il tuo host
]
}
});

38
make-zip.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT="keycloak-oidc-vite-express"
ZIP="$PROJECT.zip"
echo "[+] Validazione file principali..."
for f in package.json tsconfig.json README.md .env \
server/index.ts server/auth.ts server/env.ts \
client/index.html client/vite.config.ts \
client/src/main.tsx client/src/App.tsx client/src/api.ts
do
if [[ ! -f "$f" ]]; then
echo "[-] Manca il file: $f" >&2
exit 1
fi
done
echo "[+] Pulizia dist..."
rm -rf dist client/dist
echo "[+] Installazione dipendenze (se necessario)..."
if [[ ! -d node_modules ]]; then
npm install
fi
echo "[+] Build client e server..."
npm run build
echo "[+] Creazione archivio $ZIP..."
cd ..
rm -f "$ZIP"
zip -r "$ZIP" "$PROJECT" \
-x "$PROJECT/node_modules/*" \
-x "$PROJECT/.env" \
-x "$PROJECT/.git/*"
echo "[+] Fatto. Trovi lo zip in: $(pwd)/$ZIP"

3391
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "keycloak-oidc-vite-express",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development tsx server/index.ts",
"build:client": "vite build --config client/vite.config.ts",
"build:server": "tsc",
"build": "npm run build:client && npm run build:server",
"start": "node dist/server/index.js"
},
"dependencies": {
"cookie-parser": "^1.4.6",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"glob": "^9.3.5",
"openid-client": "^5.7.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"rimraf": "^4.4.1"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.3.0",
"cross-env": "^7.0.3",
"tsx": "^4.20.6",
"typescript": "^5.6.3",
"vite": "^5.4.0"
}
}

33
package.json.old Normal file
View file

@ -0,0 +1,33 @@
{
"name": "keycloak-oidc-vite-express",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development ts-node-dev --respawn --transpile-only server/index.ts",
"build:client": "vite build --config client/vite.config.ts",
"build:server": "tsc",
"build": "npm run build:client && npm run build:server",
"start": "node dist/server/index.js"
},
"dependencies": {
"cookie-parser": "^1.4.6",
"dotenv": "^17.2.3",
"express": "^4.19.2",
"glob": "^9.3.5",
"openid-client": "^5.7.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"rimraf": "^4.4.1"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.3.0",
"cross-env": "^7.0.3",
"ts-node-dev": "^2.0.0",
"tsx": "^4.20.6",
"typescript": "^5.6.3",
"vite": "^5.4.0"
}
}

37
server/auth.ts 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 });
});
}

12
server/env.ts Normal file
View file

@ -0,0 +1,12 @@
import 'dotenv/config';
export const cfg = {
issuerDiscoveryUrl: process.env.KEYCLOAK_ISSUER_DISCOVERY!,
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
redirectUri: process.env.REDIRECT_URI!,
cookieSecret: process.env.COOKIE_SECRET || 'dev-secret',
scope: process.env.SCOPE || 'openid profile email',
isProd: process.env.NODE_ENV === 'production',
port: Number(process.env.PORT || 3000),
};

54
server/index.ts 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);
});

17
tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": ".",
"outDir": "dist",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"server/**/*.ts",
"client/src/**/*.ts",
"client/src/**/*.tsx"
]
}

8
tsconfig.node.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["client/vite.config.ts"]
}