Refactor flow 2
This commit is contained in:
parent
7a1a9cc88a
commit
57aa9fe730
224
INVESTIGATION_502_ERROR.md
Normal file
224
INVESTIGATION_502_ERROR.md
Normal file
@ -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
|
||||||
|
|
||||||
@ -183,16 +183,16 @@ export const authOptions: NextAuthOptions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
profile(profile) {
|
profile(profile) {
|
||||||
console.log('Keycloak profile callback:', {
|
console.log('=== KEYCLOAK PROFILE CALLBACK ===');
|
||||||
rawProfile: profile,
|
console.log('Profile keys:', Object.keys(profile));
|
||||||
rawRoles: profile.roles,
|
console.log('Has realm_access:', !!profile.realm_access);
|
||||||
realmAccess: profile.realm_access,
|
console.log('Has groups:', !!profile.groups);
|
||||||
groups: profile.groups
|
console.log('Groups:', profile.groups);
|
||||||
});
|
|
||||||
|
|
||||||
// Get roles from realm_access
|
// 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 || [];
|
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
|
// Clean up roles by removing ROLE_ prefix and converting to lowercase
|
||||||
const cleanRoles = roles.map((role: string) =>
|
const cleanRoles = roles.map((role: string) =>
|
||||||
@ -200,6 +200,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
console.log('Profile callback cleaned roles:', cleanRoles);
|
console.log('Profile callback cleaned roles:', cleanRoles);
|
||||||
|
console.log('===================================');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: profile.sub,
|
id: profile.sub,
|
||||||
@ -208,7 +209,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
first_name: profile.given_name ?? '',
|
first_name: profile.given_name ?? '',
|
||||||
last_name: profile.family_name ?? '',
|
last_name: profile.family_name ?? '',
|
||||||
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
|
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
|
||||||
role: cleanRoles,
|
role: cleanRoles, // Will be updated in JWT callback from access token
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -262,12 +263,55 @@ export const authOptions: NextAuthOptions = {
|
|||||||
async jwt({ token, account, profile }) {
|
async jwt({ token, account, profile }) {
|
||||||
// Initial sign-in: account and profile are present
|
// Initial sign-in: account and profile are present
|
||||||
if (account && profile) {
|
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 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<DecodedToken>(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) =>
|
const cleanRoles = roles.map((role: string) =>
|
||||||
role.replace(/^ROLE_/, '').toLowerCase()
|
role.replace(/^ROLE_/, '').toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('✅ Final cleaned roles:', cleanRoles);
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
token.accessToken = account.access_token ?? '';
|
token.accessToken = account.access_token ?? '';
|
||||||
token.refreshToken = account.refresh_token ?? '';
|
token.refreshToken = account.refresh_token ?? '';
|
||||||
token.idToken = account.id_token ?? '';
|
token.idToken = account.id_token ?? '';
|
||||||
@ -347,12 +391,19 @@ export const authOptions: NextAuthOptions = {
|
|||||||
return refreshedToken;
|
return refreshedToken;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
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 session was invalidated or tokens are missing, return null to sign out
|
||||||
if (token.error === "SessionNotActive" ||
|
if (token.error === "SessionNotActive" ||
|
||||||
token.error === "NoRefreshToken" ||
|
token.error === "NoRefreshToken" ||
|
||||||
!token.accessToken ||
|
!token.accessToken ||
|
||||||
!token.refreshToken) {
|
!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,
|
error: token.error,
|
||||||
hasAccessToken: !!token.accessToken,
|
hasAccessToken: !!token.accessToken,
|
||||||
hasRefreshToken: !!token.refreshToken
|
hasRefreshToken: !!token.refreshToken
|
||||||
@ -367,26 +418,36 @@ export const authOptions: NextAuthOptions = {
|
|||||||
|
|
||||||
// For other errors, throw to trigger error handling
|
// For other errors, throw to trigger error handling
|
||||||
if (token.error) {
|
if (token.error) {
|
||||||
|
console.error("❌ Token error, throwing:", token.error);
|
||||||
throw new Error(token.error as string);
|
throw new Error(token.error as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRoles = Array.isArray(token.role) ? token.role : [];
|
const userRoles = Array.isArray(token.role) ? token.role : [];
|
||||||
session.user = {
|
console.log('User roles for session:', userRoles);
|
||||||
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;
|
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;
|
||||||
|
|
||||||
|
console.log('✅ Session created successfully');
|
||||||
|
console.log('==============================');
|
||||||
|
return session;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error creating session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user