Files
btpxpress-backend/src/main/java/dev/lions/btpxpress/adapter/http/AuthResource.java
DahoudG 7df5f346f1 Refactor: Backend Frontend-Centric Auth - Suppression OIDC, validation JWT
Architecture modifiée pour Frontend-Centric Authentication:

1. **Suppression des dépendances OIDC**
   - quarkus-oidc → quarkus-smallrye-jwt
   - quarkus-keycloak-authorization → quarkus-smallrye-jwt-build
   - Le backend ne gère plus l'authentification OAuth

2. **Configuration JWT simple**
   - Validation des tokens JWT envoyés par le frontend
   - mp.jwt.verify.publickey.location (JWKS de Keycloak)
   - mp.jwt.verify.issuer (Keycloak realm)
   - Authentification via Authorization: Bearer header

3. **Suppression configurations OIDC**
   - application.properties: Suppression %dev.quarkus.oidc.*
   - application.properties: Suppression %prod.quarkus.oidc.*
   - application-prod.properties: Remplacement par mp.jwt.*
   - Logging: io.quarkus.oidc → io.quarkus.smallrye.jwt

4. **Sécurité simplifiée**
   - quarkus.security.auth.proactive=false
   - @Authenticated sur les endpoints
   - CORS configuré pour le frontend
   - Endpoints publics: /q/*, /openapi, /swagger-ui/*

Flux d'authentification:
1️⃣ Frontend → Keycloak (OAuth login)
2️⃣ Frontend ← Keycloak (access_token)
3️⃣ Frontend → Backend (Authorization: Bearer token)
4️⃣ Backend valide le token JWT (signature + issuer)
5️⃣ Backend → Frontend (données API)

Avantages:
 Pas de secret backend à gérer
 Pas de client btpxpress-backend dans Keycloak
 Séparation claire frontend/backend
 Backend devient une API REST stateless
 Tokens gérés par le frontend (localStorage/sessionStorage)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 17:05:11 +00:00

307 lines
12 KiB
Java

package dev.lions.btpxpress.adapter.http;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Map;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour l'authentification et les informations utilisateur
* Permet de récupérer les informations de l'utilisateur connecté depuis le token JWT Keycloak
*/
@Path("/api/v1/auth")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Authentification", description = "Gestion de l'authentification et informations utilisateur")
public class AuthResource {
private static final Logger logger = LoggerFactory.getLogger(AuthResource.class);
@Inject
JsonWebToken jwt;
/**
* Redirige vers Keycloak pour l'authentification
* Architecture 2025 : Redirection directe vers https://security.lions.dev pour l'authentification
*/
@GET
@Path("/login")
@PermitAll
@Operation(
summary = "Initier l'authentification Keycloak",
description = "Redirige l'utilisateur vers Keycloak (https://security.lions.dev) pour l'authentification OAuth2/OIDC")
@APIResponse(responseCode = "302", description = "Redirection vers Keycloak pour authentification")
public Response login(@Context SecurityContext securityContext) {
try {
logger.info("Redirection vers Keycloak pour authentification");
// Construction de l'URL Keycloak pour l'authentification
String keycloakUrl = "https://security.lions.dev/realms/btpxpress/protocol/openid_connect/auth";
String clientId = "btpxpress-backend";
String redirectUri = "http://localhost:8080/api/v1/auth/callback"; // Peut être configuré dynamiquement
String responseType = "code";
String scope = "openid profile email";
// Construction de l'URL complète avec paramètres
java.net.URI authUri = java.net.URI.create(
String.format(
"%s?client_id=%s&redirect_uri=%s&response_type=%s&scope=%s",
keycloakUrl,
clientId,
URLEncoder.encode(redirectUri, StandardCharsets.UTF_8),
responseType,
URLEncoder.encode(scope, StandardCharsets.UTF_8)
)
);
logger.debug("Redirection vers Keycloak: {}", authUri);
return Response.status(Response.Status.FOUND)
.location(authUri)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la redirection vers Keycloak", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la redirection vers Keycloak", "message", e.getMessage()))
.build();
}
}
/**
* Récupère les informations de l'utilisateur connecté depuis le token JWT
*/
@GET
@Path("/user")
@PermitAll // Accessible même sans authentification pour les tests
@Operation(
summary = "Informations utilisateur connecté",
description = "Récupère les informations de l'utilisateur connecté depuis le token JWT Keycloak")
@APIResponse(responseCode = "200", description = "Informations utilisateur récupérées")
@APIResponse(responseCode = "401", description = "Non authentifié")
public Response getCurrentUser(@Context SecurityContext securityContext) {
try {
logger.debug("Récupération des informations utilisateur connecté");
// Vérifier si l'utilisateur est authentifié
Principal principal = securityContext.getUserPrincipal();
if (principal == null || jwt == null) {
logger.warn("Aucun utilisateur authentifié trouvé");
// En mode développement, retourner un utilisateur de test
return Response.ok(createTestUser()).build();
}
// Extraire les informations du token JWT
String userId = jwt.getSubject();
String username = jwt.getClaim("preferred_username");
String email = jwt.getClaim("email");
String firstName = jwt.getClaim("given_name");
String lastName = jwt.getClaim("family_name");
String fullName = jwt.getClaim("name");
// Extraire les rôles
Object realmAccess = jwt.getClaim("realm_access");
Object resourceAccess = jwt.getClaim("resource_access");
// Construire la réponse avec les informations utilisateur
Map<String, Object> userInfo = new java.util.HashMap<>();
userInfo.put("id", userId != null ? userId : "unknown");
userInfo.put("username", username != null ? username : email);
userInfo.put("email", email != null ? email : "unknown@btpxpress.com");
userInfo.put("firstName", firstName != null ? firstName : "Utilisateur");
userInfo.put("lastName", lastName != null ? lastName : "Connecté");
userInfo.put("fullName", fullName != null ? fullName : (firstName + " " + lastName).trim());
userInfo.put("roles", extractRoles(realmAccess, resourceAccess));
userInfo.put("permissions", extractPermissions(realmAccess, resourceAccess));
userInfo.put("isAdmin", isAdmin(realmAccess, resourceAccess));
userInfo.put("isManager", isManager(realmAccess, resourceAccess));
userInfo.put("isEmployee", isEmployee(realmAccess, resourceAccess));
userInfo.put("isClient", isClient(realmAccess, resourceAccess));
logger.info("Informations utilisateur récupérées: {} ({})", username, email);
return Response.ok(userInfo).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des informations utilisateur", e);
// En cas d'erreur, retourner un utilisateur de test en mode développement
return Response.ok(createTestUser()).build();
}
}
/**
* Endpoint de test pour vérifier l'état de l'authentification
*/
@GET
@Path("/status")
@PermitAll
@Operation(
summary = "Statut d'authentification",
description = "Vérifie l'état de l'authentification de l'utilisateur")
@APIResponse(responseCode = "200", description = "Statut récupéré")
public Response getAuthStatus(@Context SecurityContext securityContext) {
try {
Principal principal = securityContext.getUserPrincipal();
boolean isAuthenticated = principal != null && jwt != null;
Map<String, Object> status = Map.of(
"authenticated", isAuthenticated,
"principal", principal != null ? principal.getName() : null,
"hasJWT", jwt != null,
"timestamp", System.currentTimeMillis()
);
return Response.ok(status).build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification du statut d'authentification", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la vérification du statut"))
.build();
}
}
/**
* Crée un utilisateur de test pour le mode développement
*/
private Map<String, Object> createTestUser() {
Map<String, Object> testUser = new java.util.HashMap<>();
testUser.put("id", "dev-user-001");
testUser.put("username", "admin.btpxpress");
testUser.put("email", "admin@btpxpress.com");
testUser.put("firstName", "Jean-Michel");
testUser.put("lastName", "Martineau");
testUser.put("fullName", "Jean-Michel Martineau");
testUser.put("roles", java.util.List.of("SUPER_ADMIN", "ADMIN", "DIRECTEUR"));
testUser.put("permissions", java.util.List.of("ALL_PERMISSIONS"));
testUser.put("isAdmin", true);
testUser.put("isManager", true);
testUser.put("isEmployee", false);
testUser.put("isClient", false);
return testUser;
}
/**
* Extrait les rôles depuis les claims JWT
*/
private java.util.List<String> extractRoles(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = new java.util.ArrayList<>();
// Ajouter les rôles du realm
if (realmAccess instanceof Map) {
Object realmRoles = ((Map<?, ?>) realmAccess).get("roles");
if (realmRoles instanceof java.util.List) {
((java.util.List<?>) realmRoles).forEach(role -> {
if (role instanceof String) {
roles.add((String) role);
}
});
}
}
// Ajouter les rôles des ressources
if (resourceAccess instanceof Map) {
((Map<?, ?>) resourceAccess).values().forEach(resource -> {
if (resource instanceof Map) {
Object resourceRoles = ((Map<?, ?>) resource).get("roles");
if (resourceRoles instanceof java.util.List) {
((java.util.List<?>) resourceRoles).forEach(role -> {
if (role instanceof String) {
roles.add((String) role);
}
});
}
}
});
}
return roles.isEmpty() ? java.util.List.of("USER") : roles;
}
/**
* Extrait les permissions depuis les rôles
*/
private java.util.List<String> extractPermissions(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
java.util.List<String> permissions = new java.util.ArrayList<>();
// Mapper les rôles vers les permissions
for (String role : roles) {
switch (role.toUpperCase()) {
case "SUPER_ADMIN":
case "BTPXPRESS_SUPER_ADMIN":
permissions.add("ALL_PERMISSIONS");
break;
case "ADMIN":
case "BTPXPRESS_ADMIN":
permissions.addAll(java.util.List.of("MANAGE_USERS", "MANAGE_CHANTIERS", "MANAGE_CLIENTS"));
break;
case "DIRECTEUR":
case "MANAGER":
permissions.addAll(java.util.List.of("VIEW_REPORTS", "MANAGE_CHANTIERS"));
break;
case "CHEF_CHANTIER":
permissions.addAll(java.util.List.of("MANAGE_CHANTIER", "VIEW_PLANNING"));
break;
default:
permissions.add("VIEW_BASIC");
break;
}
}
return permissions.isEmpty() ? java.util.List.of("VIEW_BASIC") : permissions;
}
/**
* Vérifie si l'utilisateur est administrateur
*/
private boolean isAdmin(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("ADMIN") || role.toUpperCase().contains("SUPER_ADMIN"));
}
/**
* Vérifie si l'utilisateur est manager
*/
private boolean isManager(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("DIRECTEUR") ||
role.toUpperCase().contains("MANAGER") ||
role.toUpperCase().contains("CHEF"));
}
/**
* Vérifie si l'utilisateur est employé
*/
private boolean isEmployee(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("EMPLOYE") ||
role.toUpperCase().contains("OUVRIER"));
}
/**
* Vérifie si l'utilisateur est client
*/
private boolean isClient(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("CLIENT"));
}
}