Socle V004 – Client Authentification

Socle V004 - Client Authentification

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_URL est correct
  • Le serveur d’auth est accessible
  • Les ports sont ouverts

Invalid API Key

AuthenticationException: Login failed: 401

Vérifier :

  • API_KEY est correct
  • SOURCE_NAME est enregistré côté serveur

Token expired

Si les tokens expirent trop vite :

  • Vérifier l’horloge système (NTP)
  • Augmenter access-token-buffer-seconds

11. Références

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *