Autenticazione e Autorizzazione
Emblema utilizza un sistema di autenticazione basato su JWT (JSON Web Tokens) integrato con Keycloak per la gestione centralizzata di identità e accessi.
Panoramica
Metodi di Autenticazione
- NextAuth.js - Per applicazioni web con sessioni
- JWT Bearer Tokens - Per API e integrazione servizi
- API Keys - Per applicazioni di sistema (opzionale)
Flusso di Autenticazione
Loading diagram...
Configurazione Keycloak
Endpoint di Autenticazione
# Base URL Keycloak
KEYCLOAK_BASE_URL=https://keycloak.your-domain.com
# Realm Emblema
KEYCLOAK_REALM=emblema
# Token endpoint
TOKEN_ENDPOINT=/auth/realms/emblema/protocol/openid-connect/token
# UserInfo endpoint
USERINFO_ENDPOINT=/auth/realms/emblema/protocol/openid-connect/userinfo
Client Configuration
// apps/www-emblema/lib/auth.ts
export const authConfig = {
providers: [
KeycloakProvider({
clientId: process.env.AUTH_KEYCLOAK_ID,
clientSecret: process.env.AUTH_KEYCLOAK_SECRET,
issuer: process.env.AUTH_KEYCLOAK_ISSUER,
authorization: {
params: { scope: "openid email profile" },
},
}),
],
callbacks: {
async jwt({ token, account }) {
if (account?.access_token) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
// Decodifica gruppi utente
const decoded = JSON.parse(
Buffer.from(account.access_token.split(".")[1], "base64").toString(),
);
token.groups = decoded.groups ?? [];
}
return token;
},
},
};
Ottenere un JWT Token
1. Password Grant (Sviluppo)
curl -X POST https://keycloak.your-domain.com/auth/realms/emblema/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=emblema-client" \
-d "client_secret=your-client-secret" \
-d "username=user@example.com" \
-d "password=user-password"
Risposta:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJr...",
"expires_in": 3600,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJh...",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "c4f5d9f1-8b2a-4c3d-9e1f-2a3b4c5d6e7f",
"scope": "openid email profile"
}
2. Authorization Code (Produzione)
Per applicazioni web, usa il flusso OAuth2 Authorization Code:
// Frontend redirect per login
window.location.href =
`https://keycloak.your-domain.com/auth/realms/emblema/protocol/openid-connect/auth?` +
`client_id=emblema-client&` +
`redirect_uri=${encodeURIComponent(window.location.origin + "/api/auth/callback/keycloak")}&` +
`response_type=code&` +
`scope=openid%20email%20profile`;
Utilizzo del Token
Headers Richiesti
Tutte le chiamate API devono includere:
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
Accept-Language: it # Opzionale per i18n
Esempio Completo
# Ottenimento token
TOKEN=$(curl -s -X POST https://keycloak.your-domain.com/auth/realms/emblema/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&client_id=emblema-client&username=user@example.com&password=password" \
| jq -r '.access_token')
# Utilizzo del token per chiamata API
curl -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept-Language: it" \
https://your-domain.com/api/v1/document
Autorizzazione Granulare
Sistema di Gruppi
Emblema utilizza gruppi Keycloak per controllare l'accesso:
// apps/www-emblema/lib/auth.ts
export const ALLOWED_GROUPS = [
'emblema-users',
'emblema-admins',
'emblema-developers'
];
// Verifica autorizzazione gruppo
export const hasAuthorizedGroups = (groups?: string[]): boolean => {
if (!ALLOWED_GROUPS.length) return true;
if (!groups?.length) return false;
return groups.some(group => ALLOWED_GROUPS.includes(group));
};
Permessi a Livello di Entità
Ogni entità ha permessi granulari:
// Controllo permessi per entità specifica
const hasPermission = await hasEntityTypePermission({
userId: await getUserId(req),
entityType: EntityType.Document,
read: true, // Lettura
create: true, // Creazione
update: true, // Modifica
delete: true, // Eliminazione
});
Record Permissions
Permessi a livello di singolo record:
{
"recordPermissions": {
"data": {
"userId": "user-uuid",
"read": true,
"create": true,
"update": true,
"delete": true,
"entityType": "Document"
}
}
}
Token Validation & Claims
Struttura JWT Token
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "keycloak-key-id"
},
"payload": {
"exp": 1640995200,
"iat": 1640991600,
"iss": "https://keycloak.your-domain.com/auth/realms/emblema",
"aud": "emblema-client",
"sub": "user-uuid",
"typ": "Bearer",
"azp": "emblema-client",
"session_state": "session-uuid",
"scope": "openid email profile",
"email_verified": true,
"preferred_username": "user@example.com",
"email": "user@example.com",
"groups": ["emblema-users"]
}
}
Validazione Server-Side
// apps/www-emblema/lib/keycloak.ts
import jwt from "jsonwebtoken";
export const verifyJWT = async (token: string) => {
try {
const decoded = jwt.verify(token, publicKey, {
issuer: process.env.AUTH_KEYCLOAK_ISSUER,
audience: process.env.AUTH_KEYCLOAK_ID,
});
return decoded;
} catch (error) {
throw new Error("Invalid token");
}
};
Gestione Errori di Autenticazione
Codici di Errore
| Codice | Descrizione | Azione |
|---|---|---|
401 | Token mancante o non valido | Ri-effettua il login |
403 | Permessi insufficienti | Contatta amministratore |
403 | Gruppo non autorizzato | Richiedi accesso |
Esempi di Risposta
401 Unauthorized
{
"error": "Unauthorized",
"code": "UNAUTHORIZED",
"message": "No valid token found",
"timestamp": "2024-01-15T10:00:00Z"
}
403 Forbidden - Gruppo
{
"error": "Forbidden",
"code": "NOT_AUTHORIZED",
"message": "User group not in allowed groups",
"details": {
"userGroups": ["public-users"],
"allowedGroups": ["emblema-users", "emblema-admins"]
}
}
403 Forbidden - Permessi Entità
{
"error": "Forbidden",
"code": "NOT_AUTHORIZED",
"message": "You are not authorized to create this entity",
"details": {
"entityType": "Document",
"requiredPermission": "create"
}
}
Refresh Token
Rinnovo Automatico
const refreshToken = async (refresh_token) => {
const response = await fetch(
"https://keycloak.your-domain.com/auth/realms/emblema/protocol/openid-connect/token",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: "emblema-client",
client_secret: "your-client-secret",
refresh_token: refresh_token,
}),
},
);
return response.json();
};
Interceptor per Auto-Refresh
// Axios interceptor per gestione automatica refresh
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
try {
const newToken = await refreshToken(storedRefreshToken);
error.config.headers.Authorization = `Bearer ${newToken.access_token}`;
return axios.request(error.config);
} catch (refreshError) {
// Redirect to login
window.location.href = "/auth/signin";
}
}
return Promise.reject(error);
},
);
Best Practices
Sicurezza
- Never log tokens - Non loggare mai token JWT nei log
- HTTPS only - Usa sempre HTTPS in produzione
- Short expiration - Configura token con breve durata (1-2 ore)
- Secure storage - Memorizza refresh token in modo sicuro
- Validate claims - Sempre validare iss, aud, exp
Performance
- Cache public keys - Cachea le chiavi pubbliche Keycloak
- Token pooling - Riusa token validi per richieste multiple
- Lazy validation - Valida solo quando necessario
Development
# .env.local per sviluppo
AUTH_KEYCLOAK_ID=emblema-dev
AUTH_KEYCLOAK_SECRET=dev-secret
AUTH_KEYCLOAK_ISSUER=http://localhost:8080/auth/realms/emblema
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=dev-secret-key
Production
# .env.production
AUTH_KEYCLOAK_ID=emblema-prod
AUTH_KEYCLOAK_SECRET=secure-production-secret
AUTH_KEYCLOAK_ISSUER=https://keycloak.your-domain.com/auth/realms/emblema
NEXTAUTH_URL=https://your-domain.com
NEXTAUTH_SECRET=secure-production-nextauth-secret