302 lines
9.5 KiB
Markdown
302 lines
9.5 KiB
Markdown
# Solution: Propagation du Token JWT depuis JSF vers Backend
|
|
|
|
**Date**: 2025-12-05
|
|
**Problème**: 401 Unauthorized lors des appels frontend → backend malgré authentification OIDC réussie
|
|
|
|
---
|
|
|
|
## 🔍 Analyse du Problème
|
|
|
|
### Symptômes Observés
|
|
1. **Frontend** (Port 8080):
|
|
- ✅ Authentification OIDC réussie avec PKCE
|
|
- ✅ Token JWT reçu avec tous les rôles dans `realm_access.roles`
|
|
- ❌ Erreur: `Received: 'Unauthorized, status code 401'` lors des appels API
|
|
|
|
2. **Backend** (Port 8081):
|
|
- ✅ Démarre sans erreur
|
|
- ❌ Logs: `Bearer access token is not available`
|
|
- ❌ Rejette les requêtes avec 401 Unauthorized
|
|
|
|
### Configuration Initiale (Insuffisante)
|
|
```properties
|
|
# application.properties:56
|
|
quarkus.rest-client."lions-user-manager-api".bearer-token-propagation=true
|
|
```
|
|
|
|
### Pourquoi ça ne fonctionnait pas ?
|
|
La propriété `bearer-token-propagation=true` ne fonctionne QUE pour:
|
|
- ✅ Appels **backend → backend** (service-to-service)
|
|
- ❌ Appels **JSF managed bean → backend** (notre cas)
|
|
|
|
**Raison technique**: Les managed beans JSF s'exécutent dans un contexte serveur différent où le token OIDC n'est pas automatiquement injecté dans les appels REST Client.
|
|
|
|
---
|
|
|
|
## ✅ Solution Implémentée
|
|
|
|
### 1. Création de `AuthHeaderFactory`
|
|
Factory personnalisé qui intercepte TOUS les appels REST Client et ajoute automatiquement le header Authorization avec le token JWT.
|
|
|
|
**Fichier**: `lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/filter/AuthHeaderFactory.java`
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class AuthHeaderFactory implements ClientHeadersFactory {
|
|
|
|
private static final Logger LOGGER = Logger.getLogger(AuthHeaderFactory.class.getName());
|
|
|
|
@Inject
|
|
JsonWebToken jwt;
|
|
|
|
@Override
|
|
public MultivaluedMap<String, String> update(
|
|
MultivaluedMap<String, String> incomingHeaders,
|
|
MultivaluedMap<String, String> clientOutgoingHeaders) {
|
|
|
|
MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
|
|
|
|
try {
|
|
// Vérifier si le JWT est disponible et non expiré
|
|
if (jwt != null && jwt.getRawToken() != null && !jwt.getRawToken().isEmpty()) {
|
|
String token = jwt.getRawToken();
|
|
result.add("Authorization", "Bearer " + token);
|
|
LOGGER.fine("Token Bearer ajouté au header Authorization");
|
|
} else {
|
|
LOGGER.warning("Token JWT non disponible ou vide");
|
|
}
|
|
} catch (Exception e) {
|
|
LOGGER.severe("Erreur lors de l'ajout du token Bearer: " + e.getMessage());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Points clés**:
|
|
- `@ApplicationScoped` - Bean CDI singleton
|
|
- `@Inject JsonWebToken jwt` - Injecte le token OIDC actuel
|
|
- `jwt.getRawToken()` - Récupère le token brut (chaîne Base64)
|
|
- Ajoute `Authorization: Bearer {token}` à chaque requête
|
|
|
|
### 2. Enregistrement sur tous les REST Clients
|
|
Ajout de l'annotation `@RegisterClientHeaders(AuthHeaderFactory.class)` sur chaque interface REST Client.
|
|
|
|
#### UserServiceClient
|
|
```java
|
|
@Path("/api/users")
|
|
@RegisterRestClient(configKey = "lions-user-manager-api")
|
|
@RegisterClientHeaders(AuthHeaderFactory.class) // ← AJOUTÉ
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
public interface UserServiceClient {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
#### Autres REST Clients modifiés
|
|
- `RoleServiceClient.java:19`
|
|
- `AuditServiceClient.java:20`
|
|
- `SyncServiceClient.java:16`
|
|
|
|
---
|
|
|
|
## 🧪 Test de la Solution
|
|
|
|
### 1. Recompiler le Frontend
|
|
```bash
|
|
cd lions-user-manager-client-quarkus-primefaces-freya
|
|
mvn compile
|
|
```
|
|
|
|
**Résultat attendu**: BUILD SUCCESS ✅
|
|
|
|
### 2. Redémarrer le Frontend (si nécessaire)
|
|
Si le frontend ne recharge pas automatiquement les changements:
|
|
```bash
|
|
# Arrêter le frontend actuel (Ctrl+C)
|
|
mvn quarkus:dev
|
|
```
|
|
|
|
### 3. Test Complet
|
|
1. Accéder à http://localhost:8080
|
|
2. **Se déconnecter** (important pour obtenir un nouveau token)
|
|
3. **Se reconnecter** avec `testuser` / `test123`
|
|
4. Naviguer vers http://localhost:8080/pages/user-manager/users/list.xhtml
|
|
5. **Vérifier**: La liste des utilisateurs se charge sans erreur 401
|
|
|
|
### 4. Vérification des Logs
|
|
|
|
#### Frontend - Token propagé
|
|
```
|
|
FINE Token Bearer ajouté au header Authorization
|
|
```
|
|
|
|
#### Backend - Token reçu et validé
|
|
```
|
|
DEBUG [io.qu.oi.ru.BearerAuthenticationMechanism] Token validation succeeded
|
|
```
|
|
|
|
Si vous voyez encore `Bearer access token is not available` → le token n'est toujours pas propagé (problème de contexte CDI ou hot reload).
|
|
|
|
---
|
|
|
|
## 📊 Comparaison Avant/Après
|
|
|
|
### AVANT (avec bearer-token-propagation uniquement)
|
|
```
|
|
Frontend JSF Bean → REST Client → Backend
|
|
↓
|
|
❌ Pas de token
|
|
↓
|
|
401 Unauthorized
|
|
```
|
|
|
|
### APRÈS (avec AuthHeaderFactory)
|
|
```
|
|
Frontend JSF Bean → REST Client → AuthHeaderFactory
|
|
↓
|
|
@Inject JsonWebToken
|
|
↓
|
|
Authorization: Bearer {token}
|
|
↓
|
|
Backend
|
|
↓
|
|
✅ Token validé
|
|
↓
|
|
200 OK + Données
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 Architecture Technique
|
|
|
|
### Flux d'exécution complet
|
|
|
|
1. **Utilisateur s'authentifie** via OIDC (Keycloak)
|
|
- PKCE flow avec S256
|
|
- Redirection vers Keycloak → Retour avec code → Échange contre tokens
|
|
|
|
2. **Quarkus OIDC reçoit les tokens**
|
|
- `access_token` (contient `realm_access.roles`)
|
|
- `id_token` (identité utilisateur)
|
|
- `refresh_token` (renouvellement)
|
|
|
|
3. **Token injecté dans contexte CDI**
|
|
- `JsonWebToken` bean disponible via `@Inject`
|
|
- Contient toutes les claims du token
|
|
|
|
4. **JSF Bean appelle REST Client**
|
|
- Ex: `userServiceClient.searchUsers(criteria)`
|
|
|
|
5. **AuthHeaderFactory intercepte l'appel**
|
|
- Méthode `update()` appelée avant l'envoi HTTP
|
|
- Injecte `Authorization: Bearer {access_token}`
|
|
|
|
6. **Backend reçoit la requête**
|
|
- `BearerAuthenticationMechanism` extrait le token
|
|
- Valide la signature JWT avec clé publique Keycloak
|
|
- Extrait les rôles depuis `realm_access.roles`
|
|
- Autorise l'accès si rôles suffisants
|
|
|
|
7. **Backend retourne les données**
|
|
- HTTP 200 OK + JSON response
|
|
|
|
---
|
|
|
|
## 🎯 Points Importants
|
|
|
|
### Pourquoi JsonWebToken et pas d'autres solutions ?
|
|
1. ✅ **Native Quarkus** - Fait partie du stack OIDC standard
|
|
2. ✅ **Thread-safe** - Géré par CDI avec contexte de requête
|
|
3. ✅ **Type-safe** - Interface fortement typée
|
|
4. ✅ **Validation automatique** - Token déjà validé par Quarkus OIDC
|
|
|
|
### Alternatives (non retenues)
|
|
- ❌ `SecurityContext` - Ne contient pas le token brut
|
|
- ❌ `OidcSession` - Trop couplé à la session HTTP
|
|
- ❌ Header manuel dans chaque méthode - Code dupliqué et fragile
|
|
- ❌ Filter JAX-RS - Plus complexe, moins naturel avec REST Client
|
|
|
|
### Avantages de cette solution
|
|
1. **Automatique** - Aucun code dans les beans JSF
|
|
2. **Centralisé** - Une seule classe factory
|
|
3. **Réutilisable** - Fonctionne pour tous les REST Clients
|
|
4. **Maintenable** - Facile à déboguer et à tester
|
|
5. **Performant** - Aucune copie du token, juste une référence
|
|
|
|
---
|
|
|
|
## 📝 Checklist de Validation
|
|
|
|
Après implémentation de cette solution:
|
|
|
|
### Frontend
|
|
- [x] `AuthHeaderFactory.java` créé dans `client/filter/`
|
|
- [x] Tous les REST Clients annotés avec `@RegisterClientHeaders`
|
|
- [x] Compilation Maven réussie
|
|
- [x] Aucune erreur de hot reload
|
|
|
|
### Runtime
|
|
- [ ] Se déconnecter puis reconnecter pour obtenir nouveau token
|
|
- [ ] Naviguer vers la liste des utilisateurs
|
|
- [ ] Vérifier logs frontend: "Token Bearer ajouté au header Authorization"
|
|
- [ ] Vérifier logs backend: "Token validation succeeded"
|
|
- [ ] Liste des utilisateurs s'affiche sans erreur 401
|
|
|
|
### Backend
|
|
- [ ] Backend accepte les requêtes avec token
|
|
- [ ] Rôles correctement extraits et appliqués
|
|
- [ ] Pas de logs "Bearer access token is not available"
|
|
|
|
---
|
|
|
|
## 🐛 Troubleshooting
|
|
|
|
### Problème: Token toujours pas propagé après changements
|
|
**Cause**: Hot reload Quarkus n'a pas détecté les changements de factory
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Arrêter le frontend (Ctrl+C)
|
|
cd lions-user-manager-client-quarkus-primefaces-freya
|
|
mvn clean compile quarkus:dev
|
|
```
|
|
|
|
### Problème: "Token JWT non disponible ou vide"
|
|
**Cause**: Contexte CDI ne trouve pas le JsonWebToken
|
|
|
|
**Solution**:
|
|
1. Vérifier que l'utilisateur est authentifié
|
|
2. Se déconnecter et reconnecter
|
|
3. Vérifier logs OIDC: token doit être présent
|
|
|
|
### Problème: 401 sur certaines pages mais pas d'autres
|
|
**Cause**: Chemins publics mal configurés
|
|
|
|
**Solution**: Vérifier `application.properties`:
|
|
```properties
|
|
quarkus.http.auth.permission.public.paths=/,/index.xhtml,...
|
|
quarkus.http.auth.permission.authenticated.paths=/pages/user-manager/*
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Références
|
|
|
|
### Quarkus Documentation
|
|
- [Quarkus OIDC Token Propagation](https://quarkus.io/guides/security-openid-connect-client-reference#token-propagation)
|
|
- [Quarkus REST Client](https://quarkus.io/guides/rest-client)
|
|
- [ClientHeadersFactory](https://download.eclipse.org/microprofile/microprofile-rest-client-2.0/microprofile-rest-client-spec-2.0.html#_clientheadersfactory)
|
|
|
|
### Keycloak
|
|
- [JWT Token Structure](https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange)
|
|
- [Realm Roles vs Client Roles](https://www.keycloak.org/docs/latest/server_admin/#realm-roles)
|
|
|
|
---
|
|
|
|
**Auteur**: Claude Code
|
|
**Date**: 2025-12-05
|
|
**Version**: 1.0.0
|