Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@

import java.lang.System.Logger.Level;
import java.lang.annotation.Annotation;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
Expand Down Expand Up @@ -88,6 +91,8 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur
private final boolean propagate;
private final OidcOutboundConfig outboundConfig;
private final boolean useJwtGroups;
private final ReentrantLock tokenCacheLock = new ReentrantLock();
private CachedToken cachedToken;
private final LruCache<String, TenantAuthenticationHandler> tenantAuthHandlers = LruCache.create();

private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) {
Expand Down Expand Up @@ -246,47 +251,80 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe
return OutboundSecurityResponse.empty();
}

/**
* Retrieves a client-credentials access token and injects it into the outbound
* headers. The first successful call is cached in {@code cachedToken}; while the
* token is still {@linkplain CachedToken#isValid() valid} every subsequent call
* simply reuses it, protected by {@link #tokenCacheLock} to avoid concurrent
* refreshes. Only when the cached entry is close to expiry is the identity
* server contacted again and the cache updated.
*/
private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, SecurityEnvironment outboundEnv) {
OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv);
boolean enabled = target.propagate;
if (enabled) {
ClientCredentialsConfig clientCredentialsConfig = oidcConfig.clientCredentialsConfig();
Parameters.Builder formBuilder = Parameters.builder("oidc-form-params")
.add("grant_type", "client_credentials");

clientCredentialsConfig.scope().ifPresent(scope -> formBuilder.add("scope", scope));

HttpClientRequest postRequest = oidcConfig.appWebClient()
.post()
.uri(oidcConfig.tokenEndpointUri());

OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest);

try (var response = postRequest.submit(formBuilder.build())) {
if (response.status().family() == Status.Family.SUCCESSFUL) {
JsonObject jsonObject = response.as(JsonObject.class);
String accessToken = jsonObject.getString("access_token");

tokenCacheLock.lock();
try {
if (Objects.nonNull(cachedToken) && cachedToken.isValid()) {
Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers());
target.tokenHandler.header(headers, accessToken);
target.tokenHandler.header(headers, cachedToken.token);
return OutboundSecurityResponse.withHeaders(headers);
} else {
return OutboundSecurityResponse.builder()
.status(SecurityResponse.SecurityStatus.FAILURE)
.description("Could not obtain access token from the identity server")
.build();
ClientCredentialsConfig clientCredentialsConfig = oidcConfig.clientCredentialsConfig();
Parameters.Builder formBuilder = Parameters.builder("oidc-form-params")
.add("grant_type", "client_credentials");

clientCredentialsConfig.scope().ifPresent(scope -> formBuilder.add("scope", scope));


HttpClientRequest postRequest = oidcConfig.appWebClient()
.post()
.uri(oidcConfig.tokenEndpointUri());

OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest);

try (var response = postRequest.submit(formBuilder.build())) {
if (response.status().family() == Status.Family.SUCCESSFUL) {
JsonObject jsonObject = response.as(JsonObject.class);
String accessToken = jsonObject.getString("access_token");
cacheTokenWithExpiry(jsonObject, accessToken);
Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers());
target.tokenHandler.header(headers, accessToken);
return OutboundSecurityResponse.withHeaders(headers);
} else {
return OutboundSecurityResponse.builder()
.status(SecurityResponse.SecurityStatus.FAILURE)
.description("Could not obtain access token from the identity server")
.build();
}
} catch (Exception e) {
return OutboundSecurityResponse.builder()
.status(SecurityResponse.SecurityStatus.FAILURE)
.description("An error occurred while obtaining access token from the identity server")
.throwable(e)
.build();
}
}
} catch (Exception e) {
return OutboundSecurityResponse.builder()
.status(SecurityResponse.SecurityStatus.FAILURE)
.description("An error occurred while obtaining access token from the identity server")
.throwable(e)
.build();
} finally {
tokenCacheLock.unlock();
}

}


return OutboundSecurityResponse.empty();
}

private void cacheTokenWithExpiry(JsonObject jsonObject, String accessToken) {
if (jsonObject.containsKey("expires_in")) {
Duration expiresIn = Duration.ofSeconds(jsonObject.getJsonNumber("expires_in").longValueExact());
Instant expiresAt = Instant.now().plus(expiresIn);
cachedToken = new CachedToken(accessToken, expiresAt);
} else {
cachedToken = null;
}
}

/**
* Builder for {@link OidcProvider}.
*/
Expand Down Expand Up @@ -589,5 +627,21 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) {
tokenHandler = handler;
}
}

private static final class CachedToken {

private static final Duration DEFAULT_BUFFER_TIME = Duration.ofSeconds(30);
private final String token;
private final Instant expiresAt;

CachedToken(String token, Instant expiresAt) {
this.token = token;
this.expiresAt = expiresAt;
}

private boolean isValid() {
return Instant.now().isBefore(expiresAt.minus(DEFAULT_BUFFER_TIME));
}
}
}