JwtAuthenticationHeaderProvider.java

package io.github.evisentin.wordpress.rest.client.domain.auth.header;

import io.github.evisentin.wordpress.rest.client.domain.auth.WpJwtAuthenticationStrategy;
import io.github.evisentin.wordpress.rest.client.domain.auth.jwt.JwtResponse;
import io.github.evisentin.wordpress.rest.client.domain.auth.jwt.JwtTokenClient;
import io.github.evisentin.wordpress.rest.client.domain.auth.jwt.TokenUtils;
import lombok.NonNull;

import java.time.Instant;

/**
 * Creates JWT bearer {@code Authorization} headers.
 *
 * <p>This provider fetches a JWT token using a {@link JwtTokenClient}, caches it, and reuses it until it expires.
 * Token refresh is synchronized to avoid duplicate token requests when accessed concurrently.</p>
 */
public final class JwtAuthenticationHeaderProvider
        implements AuthenticationHeaderProvider<WpJwtAuthenticationStrategy> {

    private final JwtTokenClient tokenClient;
    private final Object lock = new Object();

    private volatile String token;
    private volatile Instant expiresAt;

    public JwtAuthenticationHeaderProvider(final @NonNull JwtTokenClient tokenClient) {
        this.tokenClient = tokenClient;
    }

    @Override
    public String createAuthorizationHeader(final @NonNull WpJwtAuthenticationStrategy strategy) {

        // Fast path: if we already have a valid (non-expired) token,
        // return it immediately without acquiring a lock.
        if (hasValidToken()) {
            return bearerToken();
        }

        synchronized (lock) {
            // Double-check after acquiring the lock to avoid duplicate token fetches.
            // Another thread may have refreshed the token while we were waiting.
            if (hasValidToken()) {
                return bearerToken();
            }

            JwtResponse response = tokenClient.fetchToken(strategy);

            token = response.getToken();
            expiresAt = TokenUtils.resolveExpiration(token);

            return bearerToken();
        }
    }

    private String bearerToken() {
        return String.format("Bearer %s", token);
    }

    private boolean hasValidToken() {
        return (token != null) && !TokenUtils.isExpired(expiresAt);
    }
}