23 – Client Authentification JWT (Nouveauté V4)
Version : 4.0.0 Date : 2025-12-09
1. Introduction
Le SocleAuthClient est un client d’authentification JWT intégré au Socle V4 pour communiquer avec les services centraux (LogHub, Registry, etc.).
Pattern d’authentification
┌─────────────────┐ ┌─────────────────┐
│ Application │ │ Auth Server │
│ Socle V4 │ │ (central) │
└────────┬────────┘ └────────┬────────┘
│ │
│ 1. POST /auth/login │
│ {sourceName, apiKey} │
│─────────────────────────────────────►│
│ │
│ 2. {accessToken, refreshToken} │
│◄─────────────────────────────────────│
│ │
│ 3. Requêtes avec Bearer token │
│ Authorization: Bearer <accessToken> │
│─────────────────────────────────────►│ Services
│ │
│ 4. POST /auth/refresh (auto) │
│ {refreshToken} │
│─────────────────────────────────────►│
│ │
│ 5. {accessToken (new)} │
│◄─────────────────────────────────────│
2. Configuration
2.1 application.yml
socle:
auth:
enabled: ${AUTH_ENABLED:false}
server-url: ${AUTH_SERVER_URL:https://auth.lmvi.org}
source-name: ${SOURCE_NAME:${socle.app_name}}
api-key: ${API_KEY:}
access-token-buffer-seconds: 60
connect-timeout-ms: 10000
read-timeout-ms: 30000
2.2 Variables d’environnement
| Variable | Description | Défaut |
|---|---|---|
AUTH_ENABLED |
Activer l’authentification | false |
AUTH_SERVER_URL |
URL du serveur d’auth | – |
SOURCE_NAME |
Identifiant du client | ${APP_NAME} |
API_KEY |
Clé API (secret) | – |
3. Interface SocleAuthClient
package eu.lmvi.socle.client.auth;
/**
* Client d'authentification Socle V4
*/
public interface SocleAuthClient {
/**
* Login initial avec API Key
* @return Tokens d'accès et de refresh
* @throws AuthenticationException si échec
*/
AuthTokens login() throws AuthenticationException;
/**
* Refresh du token d'accès
* @param refreshToken Token de refresh
* @return Nouveaux tokens
* @throws AuthenticationException si échec
*/
AuthTokens refresh(String refreshToken) throws AuthenticationException;
/**
* Obtenir un token d'accès valide (avec refresh auto si nécessaire)
* @return Token d'accès valide
* @throws AuthenticationException si échec
*/
String getValidAccessToken() throws AuthenticationException;
/**
* Vérifie si le client est authentifié
* @return true si un token valide existe
*/
boolean isAuthenticated();
/**
* Invalide les tokens courants
*/
void logout();
}
4. DTOs
4.1 AuthTokens
package eu.lmvi.socle.client.auth;
public record AuthTokens(
String accessToken,
String refreshToken,
Instant accessTokenExpiry,
Instant refreshTokenExpiry
) {
public boolean isAccessTokenExpired() {
return Instant.now().isAfter(accessTokenExpiry);
}
public boolean isAccessTokenExpiringSoon(int bufferSeconds) {
return Instant.now().plusSeconds(bufferSeconds).isAfter(accessTokenExpiry);
}
public boolean isRefreshTokenExpired() {
return Instant.now().isAfter(refreshTokenExpiry);
}
}
4.2 LoginRequest / LoginResponse
// Request
public record LoginRequest(
String sourceName,
String apiKey
) {}
// Response
public record LoginResponse(
String accessToken,
String refreshToken,
int expiresIn, // secondes
int refreshExpiresIn // secondes
) {}
5. Implémentation AuthTokenManager
package eu.lmvi.socle.client.auth;
@Component
@ConditionalOnProperty(name = "socle.auth.enabled", havingValue = "true")
public class AuthTokenManager implements SocleAuthClient {
private static final Logger log = LoggerFactory.getLogger(AuthTokenManager.class);
private final SocleConfiguration config;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private volatile AuthTokens currentTokens;
private final ReentrantLock refreshLock = new ReentrantLock();
public AuthTokenManager(SocleConfiguration config) {
this.config = config;
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(config.getAuthConnectTimeoutMs(), TimeUnit.MILLISECONDS)
.readTimeout(config.getAuthReadTimeoutMs(), TimeUnit.MILLISECONDS)
.build();
}
@Override
public AuthTokens login() throws AuthenticationException {
log.info("Login to auth server: {}", config.getAuthServerUrl());
LoginRequest request = new LoginRequest(
config.getSourceName(),
config.getApiKey()
);
try {
String json = objectMapper.writeValueAsString(request);
Request httpRequest = new Request.Builder()
.url(config.getAuthServerUrl() + "/api/v1/auth/login")
.post(RequestBody.create(json, MediaType.parse("application/json")))
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
throw new AuthenticationException("Login failed: " + response.code());
}
LoginResponse loginResponse = objectMapper.readValue(
response.body().string(),
LoginResponse.class
);
currentTokens = new AuthTokens(
loginResponse.accessToken(),
loginResponse.refreshToken(),
Instant.now().plusSeconds(loginResponse.expiresIn()),
Instant.now().plusSeconds(loginResponse.refreshExpiresIn())
);
log.info("Login successful, token expires in {} seconds", loginResponse.expiresIn());
return currentTokens;
}
} catch (IOException e) {
throw new AuthenticationException("Login failed", e);
}
}
@Override
public AuthTokens refresh(String refreshToken) throws AuthenticationException {
log.debug("Refreshing access token");
RefreshRequest request = new RefreshRequest(refreshToken);
try {
String json = objectMapper.writeValueAsString(request);
Request httpRequest = new Request.Builder()
.url(config.getAuthServerUrl() + "/api/v1/auth/refresh")
.post(RequestBody.create(json, MediaType.parse("application/json")))
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
// Refresh failed, need to re-login
log.warn("Refresh failed, attempting re-login");
return login();
}
RefreshResponse refreshResponse = objectMapper.readValue(
response.body().string(),
RefreshResponse.class
);
currentTokens = new AuthTokens(
refreshResponse.accessToken(),
currentTokens.refreshToken(), // Keep same refresh token
Instant.now().plusSeconds(refreshResponse.expiresIn()),
currentTokens.refreshTokenExpiry()
);
log.debug("Token refreshed, new expiry in {} seconds", refreshResponse.expiresIn());
return currentTokens;
}
} catch (IOException e) {
throw new AuthenticationException("Refresh failed", e);
}
}
@Override
public String getValidAccessToken() throws AuthenticationException {
// First time - login
if (currentTokens == null) {
login();
return currentTokens.accessToken();
}
// Refresh token expired - need full re-login
if (currentTokens.isRefreshTokenExpired()) {
log.info("Refresh token expired, re-login required");
login();
return currentTokens.accessToken();
}
// Access token expiring soon - refresh
int bufferSeconds = config.getAccessTokenBufferSeconds();
if (currentTokens.isAccessTokenExpiringSoon(bufferSeconds)) {
refreshLock.lock();
try {
// Double-check after acquiring lock
if (currentTokens.isAccessTokenExpiringSoon(bufferSeconds)) {
refresh(currentTokens.refreshToken());
}
} finally {
refreshLock.unlock();
}
}
return currentTokens.accessToken();
}
@Override
public boolean isAuthenticated() {
return currentTokens != null && !currentTokens.isAccessTokenExpired();
}
@Override
public void logout() {
currentTokens = null;
log.info("Logged out");
}
}
6. Utilisation
6.1 Injection
@Service
public class MonService {
@Autowired(required = false)
private SocleAuthClient authClient;
public void callSecuredApi() throws Exception {
if (authClient == null || !authClient.isAuthenticated()) {
throw new IllegalStateException("Auth not configured");
}
String token = authClient.getValidAccessToken();
// Utiliser le token
Request request = new Request.Builder()
.url("https://api.mycompany.com/secured")
.header("Authorization", "Bearer " + token)
.build();
// ...
}
}
6.2 Intégration avec LogForwarder
// Dans HttpLogTransport
public class HttpLogTransport implements LogTransport {
private final SocleAuthClient authClient;
@Override
public void send(List<LogEntry> entries) throws Exception {
String token = authClient.getValidAccessToken();
Request request = new Request.Builder()
.url(logHubUrl)
.header("Authorization", "Bearer " + token)
.post(RequestBody.create(toJson(entries), JSON))
.build();
// ...
}
}
6.3 Intégration avec MOP
// Dans MainOrchestratorProcess.start()
if (authClient != null && config.isAuthEnabled()) {
log.info("[step:auth] Login auprès du serveur d'auth");
try {
authClient.login();
} catch (AuthenticationException e) {
log.error("Auth failed, continuing without auth", e);
}
}
7. Gestion des erreurs
7.1 AuthenticationException
public class AuthenticationException extends Exception {
public AuthenticationException(String message) {
super(message);
}
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
7.2 Retry automatique
Le AuthTokenManager gère automatiquement :
- Le refresh avant expiration
- Le re-login si le refresh échoue
- Le re-login si le refresh token expire
7.3 Fallback sans auth
if (authClient == null) {
log.warn("Auth not configured, proceeding without authentication");
// Continuer sans auth (pour dev local)
}
8. Sécurité
8.1 Stockage de l’API Key
# NE PAS mettre dans le code
# Utiliser des variables d'environnement
export API_KEY="xxx-secret-key"
# Ou un gestionnaire de secrets
# Kubernetes Secret, AWS Secrets Manager, etc.
8.2 Tokens en mémoire
Les tokens sont stockés en mémoire uniquement :
- Jamais persistés sur disque
- Invalidés au restart
- Thread-safe
8.3 HTTPS obligatoire
socle:
auth:
server-url: https://auth.mycompany.com # HTTPS obligatoire
9. Monitoring
9.1 Métriques
// Exposées via /metrics
socle_auth_login_total # Nombre de logins
socle_auth_login_errors_total # Erreurs de login
socle_auth_refresh_total # Nombre de refresh
socle_auth_token_expiry_seconds # Temps avant expiration
9.2 Logs
INFO - Login to auth server: https://auth.lmvi.org
INFO - Login successful, token expires in 900 seconds
DEBUG - Refreshing access token
DEBUG - Token refreshed, new expiry in 900 seconds
WARN - Refresh failed, attempting re-login
10. Troubleshooting
Connection refused
AuthenticationException: Login failed: Connection refused
Vérifier :
AUTH_SERVER_URLest correct- Le serveur d’auth est accessible
- Les ports sont ouverts
Invalid API Key
AuthenticationException: Login failed: 401
Vérifier :
API_KEYest correctSOURCE_NAMEest enregistré côté serveur
Token expired
Si les tokens expirent trop vite :
- Vérifier l’horloge système (NTP)
- Augmenter
access-token-buffer-seconds

Laisser un commentaire