From 57aa9fe73081585ada8190a9b74c4d250d67e647 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 4 Jan 2026 10:42:27 +0100 Subject: [PATCH] Refactor flow 2 --- INVESTIGATION_502_ERROR.md | 224 +++++++++++++++++++++++++++++++++++++ app/api/auth/options.ts | 115 ++++++++++++++----- 2 files changed, 312 insertions(+), 27 deletions(-) create mode 100644 INVESTIGATION_502_ERROR.md diff --git a/INVESTIGATION_502_ERROR.md b/INVESTIGATION_502_ERROR.md new file mode 100644 index 00000000..5f341c80 --- /dev/null +++ b/INVESTIGATION_502_ERROR.md @@ -0,0 +1,224 @@ +# Investigation Erreur 502 - Redirection depuis Keycloak + +## 🔍 Problème Identifié + +**Symptôme** : Erreur 502 après redirection depuis Keycloak après authentification + +**Logs observés** : +``` +Keycloak profile callback: { + rawProfile: { ... }, + rawRoles: undefined, + realmAccess: undefined, // ⚠️ PROBLÈME ICI + groups: [ ... ] // ✅ Groups présents +} +Profile callback raw roles: [] +Profile callback cleaned roles: [] +``` + +## 🎯 Cause Racine + +Le profil Keycloak (ID token) **ne contient pas** `realm_access.roles`, mais contient `groups`. Les rôles sont probablement dans le **token d'accès**, pas dans le token ID. + +## ✅ Correction Appliquée + +**Fichier** : `app/api/auth/options.ts` - Callback JWT + +**Changements** : +1. ✅ Extraction des rôles depuis le **token d'accès** (pas seulement le profil) +2. ✅ Fallback sur `groups` si pas de rôles +3. ✅ Logs améliorés pour debugging + +**Code modifié** : +```typescript +// Avant : Seulement depuis profile +const roles = keycloakProfile.realm_access?.roles || []; + +// Après : Multi-sources +1. Essayer depuis profile (ID token) +2. Si vide, décoder access token +3. Si toujours vide, utiliser groups comme fallback +``` + +## 🔍 Points d'Investigation pour l'Erreur 502 + +### 1. Vérifier les logs serveur Next.js complets + +**Où chercher** : +- Terminal Next.js (erreurs complètes) +- Logs de production (si déployé) +- Console navigateur (erreurs client) + +**Commandes utiles** : +```bash +# Voir tous les logs +npm run dev 2>&1 | tee logs.txt + +# Filtrer les erreurs +npm run dev 2>&1 | grep -i "error\|502\|exception" +``` + +### 2. Vérifier le callback NextAuth + +**Fichier** : `app/api/auth/callback/keycloak` (géré par NextAuth) + +**Points à vérifier** : +- ✅ Le callback reçoit bien le code OAuth +- ✅ L'échange code → tokens fonctionne +- ✅ Le callback JWT s'exécute sans erreur +- ✅ Le callback session s'exécute sans erreur + +**Ajouter des logs** : +```typescript +// Dans options.ts, callback jwt +console.log('JWT callback - account:', !!account, 'profile:', !!profile); +console.log('JWT callback - accessToken length:', account?.access_token?.length); +console.log('JWT callback - roles extracted:', cleanRoles); +``` + +### 3. Vérifier l'initialisation storage + +**Fichier** : `app/api/storage/init/route.ts` + +**Problème potentiel** : +- Si `createUserFolderStructure()` échoue, ça peut causer une 502 +- Si la session n'est pas encore complètement créée + +**Vérifications** : +```typescript +// Vérifier si l'erreur vient de là +console.log('Storage init - session:', session); +console.log('Storage init - user id:', session?.user?.id); +``` + +### 4. Vérifier la configuration Keycloak + +**Problème potentiel** : +- Les rôles ne sont pas mappés correctement dans Keycloak +- Le scope "roles" n'est pas configuré correctement +- Les mappers de token ne sont pas configurés + +**À vérifier dans Keycloak** : +1. **Client Configuration** : + - Scope "roles" est-il activé ? + - Mapper "realm roles" est-il configuré ? + +2. **Token Mappers** : + - `realm_access.roles` mapper existe-t-il ? + - Est-il ajouté au token d'accès ? + +3. **User Roles** : + - L'utilisateur a-t-il des rôles assignés dans le realm ? + +### 5. Vérifier les erreurs dans le callback session + +**Fichier** : `app/api/auth/options.ts` - Callback session + +**Problème potentiel** : +- Si `token.role` est undefined et qu'un code s'attend à un array +- Si `session.user` est mal formé + +**Vérifications** : +```typescript +// Dans callback session +console.log('Session callback - token.role:', token.role); +console.log('Session callback - token.error:', token.error); +console.log('Session callback - hasAccessToken:', !!token.accessToken); +``` + +### 6. Vérifier les timeouts + +**Problème potentiel** : +- Timeout lors de l'appel à Keycloak pour échanger le code +- Timeout lors de l'initialisation storage +- Timeout lors de la création de la session + +**Solutions** : +- Augmenter les timeouts si nécessaire +- Vérifier la latence réseau vers Keycloak + +## 🛠️ Actions Immédiates + +### Action 1 : Ajouter plus de logs + +**Fichier** : `app/api/auth/options.ts` + +```typescript +// Dans callback jwt, après extraction des rôles +console.log('=== JWT CALLBACK DEBUG ==='); +console.log('Has account:', !!account); +console.log('Has profile:', !!profile); +console.log('Access token present:', !!account?.access_token); +console.log('Roles from profile:', keycloakProfile.realm_access?.roles); +console.log('Roles from access token:', roles); +console.log('Final roles:', cleanRoles); +console.log('=========================='); +``` + +### Action 2 : Vérifier l'erreur exacte + +**Dans le terminal Next.js**, chercher : +- Stack trace complète +- Message d'erreur exact +- Ligne de code qui cause l'erreur + +### Action 3 : Tester avec un utilisateur simple + +**Tester avec** : +- Un utilisateur avec des rôles +- Un utilisateur sans rôles +- Vérifier si l'erreur est liée aux rôles + +### Action 4 : Vérifier la configuration Keycloak + +**Dans Keycloak Admin Console** : + +1. **Client → Mappers** : + - Vérifier qu'il y a un mapper "realm roles" + - Vérifier qu'il est ajouté au token d'accès + - Vérifier qu'il ajoute `realm_access.roles` + +2. **Client → Settings** : + - Vérifier que "Full Scope Allowed" est activé + - Vérifier les "Default Client Scopes" incluent "roles" + +3. **Realm → Roles** : + - Vérifier que l'utilisateur a des rôles assignés + +## 📊 Checklist de Debugging + +- [ ] Logs serveur Next.js complets vérifiés +- [ ] Erreur exacte identifiée (stack trace) +- [ ] Callback JWT s'exécute sans erreur +- [ ] Callback session s'exécute sans erreur +- [ ] Rôles extraits correctement (depuis access token) +- [ ] Storage init fonctionne +- [ ] Configuration Keycloak vérifiée +- [ ] Mappers Keycloak vérifiés +- [ ] Timeouts vérifiés + +## 🔧 Solution Temporaire (si nécessaire) + +Si l'erreur persiste, on peut temporairement utiliser les `groups` comme rôles : + +```typescript +// Dans profile callback +const roles = keycloakProfile.realm_access?.roles || + keycloakProfile.groups || + []; +``` + +**Note** : Ce n'est qu'une solution temporaire. Il faut corriger la configuration Keycloak pour avoir les rôles correctement mappés. + +## 📝 Prochaines Étapes + +1. ✅ **Correction appliquée** : Extraction rôles depuis access token +2. ⏳ **À faire** : Vérifier les logs serveur pour l'erreur exacte +3. ⏳ **À faire** : Vérifier configuration Keycloak +4. ⏳ **À faire** : Tester après correction + +--- + +**Document créé le** : $(date) +**Statut** : Correction appliquée, investigation en cours + diff --git a/app/api/auth/options.ts b/app/api/auth/options.ts index 42bb1152..80372b95 100644 --- a/app/api/auth/options.ts +++ b/app/api/auth/options.ts @@ -183,16 +183,16 @@ export const authOptions: NextAuthOptions = { } }, profile(profile) { - console.log('Keycloak profile callback:', { - rawProfile: profile, - rawRoles: profile.roles, - realmAccess: profile.realm_access, - groups: profile.groups - }); - - // Get roles from realm_access + console.log('=== KEYCLOAK PROFILE CALLBACK ==='); + console.log('Profile keys:', Object.keys(profile)); + console.log('Has realm_access:', !!profile.realm_access); + console.log('Has groups:', !!profile.groups); + console.log('Groups:', profile.groups); + + // Note: realm_access.roles might not be in ID token + // Roles will be extracted from access token in JWT callback const roles = profile.realm_access?.roles || []; - console.log('Profile callback raw roles:', roles); + console.log('Profile callback raw roles (from ID token):', roles); // Clean up roles by removing ROLE_ prefix and converting to lowercase const cleanRoles = roles.map((role: string) => @@ -200,6 +200,7 @@ export const authOptions: NextAuthOptions = { ); console.log('Profile callback cleaned roles:', cleanRoles); + console.log('==================================='); return { id: profile.sub, @@ -208,7 +209,7 @@ export const authOptions: NextAuthOptions = { first_name: profile.given_name ?? '', last_name: profile.family_name ?? '', username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', - role: cleanRoles, + role: cleanRoles, // Will be updated in JWT callback from access token } }, }), @@ -262,11 +263,54 @@ export const authOptions: NextAuthOptions = { async jwt({ token, account, profile }) { // Initial sign-in: account and profile are present if (account && profile) { + console.log('=== JWT CALLBACK - INITIAL SIGN-IN ==='); + console.log('Has account:', !!account); + console.log('Has profile:', !!profile); + console.log('Has access_token:', !!account.access_token); + const keycloakProfile = profile as KeycloakProfile; - const roles = keycloakProfile.realm_access?.roles || []; + + // Extract roles from access token (not from profile/ID token) + // Keycloak typically puts realm_access.roles in the access token, not the ID token + let roles: string[] = []; + + // First, try to get roles from profile (ID token) - may be empty + if (keycloakProfile.realm_access?.roles) { + roles = keycloakProfile.realm_access.roles; + console.log('Roles from profile (ID token):', roles); + } + + // If no roles in profile, try to decode access token + if (roles.length === 0 && account.access_token) { + try { + const decodedAccessToken = jwtDecode(account.access_token); + console.log('Decoded access token keys:', Object.keys(decodedAccessToken)); + console.log('Decoded access token realm_access:', decodedAccessToken.realm_access); + + if (decodedAccessToken.realm_access?.roles) { + roles = decodedAccessToken.realm_access.roles; + console.log('✅ Roles extracted from access token:', roles); + } else { + console.log('⚠️ No realm_access.roles in access token'); + } + } catch (error) { + console.error('❌ Error decoding access token for roles:', error); + } + } + + // If still no roles, try groups as fallback (some Keycloak configs use groups instead) + if (roles.length === 0 && keycloakProfile.groups && Array.isArray(keycloakProfile.groups)) { + console.log('⚠️ No roles found, using groups as fallback:', keycloakProfile.groups); + // Use groups as roles (they might be the actual roles in this Keycloak setup) + roles = keycloakProfile.groups; + } + const cleanRoles = roles.map((role: string) => role.replace(/^ROLE_/, '').toLowerCase() ); + + console.log('✅ Final cleaned roles:', cleanRoles); + console.log('====================================='); token.accessToken = account.access_token ?? ''; token.refreshToken = account.refresh_token ?? ''; @@ -347,12 +391,19 @@ export const authOptions: NextAuthOptions = { return refreshedToken; }, async session({ session, token }) { + console.log('=== SESSION CALLBACK ==='); + console.log('Token error:', token.error); + console.log('Has accessToken:', !!token.accessToken); + console.log('Has refreshToken:', !!token.refreshToken); + console.log('Token role:', token.role); + console.log('Token sub:', token.sub); + // If session was invalidated or tokens are missing, return null to sign out if (token.error === "SessionNotActive" || token.error === "NoRefreshToken" || !token.accessToken || !token.refreshToken) { - console.log("Session invalidated or tokens missing, user will be signed out", { + console.log("❌ Session invalidated or tokens missing, user will be signed out", { error: token.error, hasAccessToken: !!token.accessToken, hasRefreshToken: !!token.refreshToken @@ -367,26 +418,36 @@ export const authOptions: NextAuthOptions = { // For other errors, throw to trigger error handling if (token.error) { + console.error("❌ Token error, throwing:", token.error); throw new Error(token.error as string); } const userRoles = Array.isArray(token.role) ? token.role : []; - session.user = { - id: (token.sub ?? '') as string, - email: (token.email ?? null) as string | null, - name: (token.name ?? null) as string | null, - image: null, - username: (token.username ?? '') as string, - first_name: (token.first_name ?? '') as string, - last_name: (token.last_name ?? '') as string, - role: userRoles, - nextcloudInitialized: false, - }; - session.accessToken = token.accessToken as string | undefined; - session.idToken = token.idToken as string | undefined; - session.refreshToken = token.refreshToken as string | undefined; + console.log('User roles for session:', userRoles); + + try { + session.user = { + id: (token.sub ?? '') as string, + email: (token.email ?? null) as string | null, + name: (token.name ?? null) as string | null, + image: null, + username: (token.username ?? '') as string, + first_name: (token.first_name ?? '') as string, + last_name: (token.last_name ?? '') as string, + role: userRoles, + nextcloudInitialized: false, + }; + session.accessToken = token.accessToken as string | undefined; + session.idToken = token.idToken as string | undefined; + session.refreshToken = token.refreshToken as string | undefined; - return session; + console.log('✅ Session created successfully'); + console.log('=============================='); + return session; + } catch (error) { + console.error('❌ Error creating session:', error); + throw error; + } } }, pages: {