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>
307 lines
12 KiB
Java
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"));
|
|
}
|
|
}
|