Skip to content

Commit

Permalink
Enhance OIDC Client to support the token revocation
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Jul 22, 2022
1 parent ffc1411 commit de3599a
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,15 @@ If the access token needs to be refreshed but no refresh token is available then

Please note that some OpenID Connect Providers will not return a refresh token in a `client_credentials` grant response. For example, starting from Keycloak 12 a refresh token will not be returned by default for `client_credentials`. The providers may also restrict a number of times a refresh token can be used.

[[revoke-access-tokens]]
=== Revoking Access Tokens

If your OpenId Connect provider such as Keycloak supports a token revocation endpoint then `OidcClient#revokeAccessToken` can be used to revoke the current access token. The revocation endpoint URL will be discovered alongside the token request URI or can be configured with `quarkus.oidc-client.revoke-path`.

You may want to have the access token revoked if using this token with a REST client fails with HTTP `401` or the access token has already been used for a long time and you'd like to refresh it.

This can be achieved by requesting a token refresh using a refresh token. However, if the refresh token is not available then you can refresh it by revoking it first and then request a new access token.

[[oidc-client-authentication]]
=== OidcClient Authentication

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ default Uni<Tokens> getTokens() {
* Refreshes the grant tokens
*/
Uni<Tokens> refreshTokens(String refreshToken);

/**
* Revoke the access token
*/
Uni<Void> revokeAccessToken(String accessToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.groups.UniOnItem;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
Expand All @@ -37,6 +38,7 @@ public class OidcClientImpl implements OidcClient {

private final WebClient client;
private final String tokenRequestUri;
private final String tokenRevokeUri;
private final MultiMap tokenGrantParams;
private final MultiMap commonRefreshGrantParams;
private final String grantType;
Expand All @@ -45,10 +47,11 @@ public class OidcClientImpl implements OidcClient {
private final OidcClientConfig oidcConfig;
private volatile boolean closed;

public OidcClientImpl(WebClient client, String tokenRequestUri, String grantType,
public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType,
MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig) {
this.client = client;
this.tokenRequestUri = tokenRequestUri;
this.tokenRevokeUri = tokenRevokeUri;
this.tokenGrantParams = tokenGrantParams;
this.commonRefreshGrantParams = commonRefreshGrantParams;
this.grantType = grantType;
Expand Down Expand Up @@ -78,61 +81,88 @@ public Uni<Tokens> refreshTokens(String refreshToken) {
return getJsonResponse(refreshGrantParams, Collections.emptyMap(), true);
}

@Override
public Uni<Void> revokeAccessToken(String accessToken) {
checkClosed();
if (accessToken == null) {
throw new OidcClientException("Access token is null");
}
if (tokenRevokeUri != null) {
MultiMap tokenRevokeParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN, accessToken);
return postRequest(tokenRevokeParams, Map.of(), false)
.transformToUni(resp -> Uni.createFrom().voidItem());
} else {
LOG.debugf("%s OidcClient can not revoke the access token because the revocation endpoint URL is not set");
return Uni.createFrom().voidItem();
}

}

private Uni<Tokens> getJsonResponse(MultiMap formBody, Map<String, String> additionalGrantParameters, boolean refresh) {
//Uni needs to be lazy by default, we don't send the request unless
//something has subscribed to it. This is important for the CAS state
//management in TokensHelper
return Uni.createFrom().deferred(new Supplier<Uni<? extends Tokens>>() {
@Override
public Uni<Tokens> get() {
MultiMap body = formBody;
HttpRequest<Buffer> request = client.postAbs(tokenRequestUri);
request.putHeader(HttpHeaders.CONTENT_TYPE.toString(),
HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString());
if (oidcConfig.headers != null) {
for (Map.Entry<String, String> headerEntry : oidcConfig.headers.entrySet()) {
request.putHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
if (clientSecretBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme);
} else if (clientJwtKey != null) {
// if it is a refresh then a map has already been copied
body = !refresh ? copyMultiMap(body) : body;
String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, tokenRequestUri, clientJwtKey);

if (OidcCommonUtils.isClientSecretPostJwtAuthRequired(oidcConfig.credentials)) {
body.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
body.add(OidcConstants.CLIENT_SECRET, jwt);
} else {
body.add(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE);
body.add(OidcConstants.CLIENT_ASSERTION, jwt);
}
} else if (!OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials)) {
body = copyMultiMap(body).set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
if (!additionalGrantParameters.isEmpty()) {
body = copyMultiMap(body);
for (Map.Entry<String, String> entry : additionalGrantParameters.entrySet()) {
body.add(entry.getKey(), entry.getValue());
}
}
// Retry up to three times with a one-second delay between the retries if the connection is closed
Uni<HttpResponse<Buffer>> response = request.sendBuffer(OidcCommonUtils.encodeForm(body))
.onFailure(ConnectException.class)
.retry()
.atMost(oidcConfig.connectionRetryCount)
.onFailure().transform(t -> {
LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t);
// don't wrap t to avoid information leak
return new OidcClientException("OIDC Server is not available");
});
return response.onItem()
return postRequest(formBody, additionalGrantParameters, refresh)
.transform(resp -> emitGrantTokens(resp, refresh));
}
});
}

private UniOnItem<HttpResponse<Buffer>> postRequest(MultiMap formBody, Map<String, String> additionalGrantParameters,
boolean refresh) {
MultiMap body = formBody;
HttpRequest<Buffer> request = client.postAbs(tokenRequestUri);
request.putHeader(HttpHeaders.CONTENT_TYPE.toString(),
HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString());
if (oidcConfig.headers != null) {
for (Map.Entry<String, String> headerEntry : oidcConfig.headers.entrySet()) {
request.putHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
if (clientSecretBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme);
} else if (clientJwtKey != null) {
// if it is a refresh then a map has already been copied
body = !refresh ? copyMultiMap(body) : body;
String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, tokenRequestUri, clientJwtKey);

if (OidcCommonUtils.isClientSecretPostJwtAuthRequired(oidcConfig.credentials)) {
body.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
body.add(OidcConstants.CLIENT_SECRET, jwt);
} else {
body.add(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE);
body.add(OidcConstants.CLIENT_ASSERTION, jwt);
}
} else if (OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials)) {
body = !refresh ? copyMultiMap(body) : body;
body.set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
body.set(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials));
} else {
body = copyMultiMap(body).set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
if (!additionalGrantParameters.isEmpty()) {
body = copyMultiMap(body);
for (Map.Entry<String, String> entry : additionalGrantParameters.entrySet()) {
body.add(entry.getKey(), entry.getValue());
}
}
// Retry up to three times with a one-second delay between the retries if the connection is closed
Uni<HttpResponse<Buffer>> response = request.sendBuffer(OidcCommonUtils.encodeForm(body))
.onFailure(ConnectException.class)
.retry()
.atMost(oidcConfig.connectionRetryCount)
.onFailure().transform(t -> {
LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t);
// don't wrap t to avoid information leak
return new OidcClientException("OIDC Server is not available");
});
return response.onItem();
}

private Tokens emitGrantTokens(HttpResponse<Buffer> resp, boolean refresh) {
if (resp.statusCode() == 200) {
LOG.debugf("%s OidcClient has %s the tokens", oidcConfig.getId().get(), (refresh ? "refreshed" : "acquired"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import io.quarkus.oidc.client.OidcClientException;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.runtime.TlsConfig;
Expand Down Expand Up @@ -121,28 +120,32 @@ protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig

WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx.get()), options);

Uni<String> tokenRequestUriUni = null;
Uni<OidcConfigurationMetadata> tokenUrisUni = null;
if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.tokenPath)) {
tokenRequestUriUni = Uni.createFrom().item(oidcConfig.tokenPath.get());
tokenUrisUni = Uni.createFrom().item(
new OidcConfigurationMetadata(oidcConfig.tokenPath.get(),
OidcCommonUtils.isAbsoluteUrl(oidcConfig.revokePath) ? oidcConfig.revokePath.get() : null));
} else {
String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig);
if (!oidcConfig.discoveryEnabled.orElse(true)) {
tokenRequestUriUni = Uni.createFrom()
.item(OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath));
tokenUrisUni = Uni.createFrom()
.item(new OidcConfigurationMetadata(
OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath),
OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath)));
} else {
tokenRequestUriUni = discoverTokenRequestUri(client, authServerUriString.toString(), oidcConfig);
tokenUrisUni = discoverTokenUris(client, authServerUriString.toString(), oidcConfig);
}
}
return tokenRequestUriUni.onItemOrFailure()
.transform(new BiFunction<String, Throwable, OidcClient>() {
return tokenUrisUni.onItemOrFailure()
.transform(new BiFunction<OidcConfigurationMetadata, Throwable, OidcClient>() {

@Override
public OidcClient apply(String tokenRequestUri, Throwable t) {
public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) {
if (t != null) {
throw toOidcClientException(getEndpointUrl(oidcConfig), t);
}

if (tokenRequestUri == null) {
if (metadata.tokenRequestUri == null) {
throw new ConfigurationException(
"OpenId Connect Provider token endpoint URL is not configured and can not be discovered");
}
Expand Down Expand Up @@ -182,7 +185,8 @@ public OidcClient apply(String tokenRequestUri, Throwable t) {
MultiMap commonRefreshGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
setGrantClientParams(oidcConfig, commonRefreshGrantParams, OidcConstants.REFRESH_TOKEN_GRANT);

return new OidcClientImpl(client, tokenRequestUri, grantType, tokenGrantParams,
return new OidcClientImpl(client, metadata.tokenRequestUri, metadata.tokenRevokeUri, grantType,
tokenGrantParams,
commonRefreshGrantParams,
oidcConfig);
}
Expand All @@ -196,20 +200,17 @@ private static String getEndpointUrl(OidcClientConfig oidcConfig) {

private static void setGrantClientParams(OidcClientConfig oidcConfig, MultiMap grantParams, String grantType) {
grantParams.add(OidcConstants.GRANT_TYPE, grantType);
Credentials creds = oidcConfig.getCredentials();
if (OidcCommonUtils.isClientSecretPostAuthRequired(creds)) {
grantParams.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
grantParams.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(creds));
}
if (oidcConfig.getScopes().isPresent()) {
grantParams.add(OidcConstants.TOKEN_SCOPE, oidcConfig.getScopes().get().stream().collect(Collectors.joining(" ")));
}
}

private static Uni<String> discoverTokenRequestUri(WebClient client, String authServerUrl, OidcClientConfig oidcConfig) {
private static Uni<OidcConfigurationMetadata> discoverTokenUris(WebClient client, String authServerUrl,
OidcClientConfig oidcConfig) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return OidcCommonUtils.discoverMetadata(client, authServerUrl, connectionDelayInMillisecs)
.onItem().transform(json -> json.getString("token_endpoint"));
.onItem().transform(json -> new OidcConfigurationMetadata(json.getString("token_endpoint"),
json.getString("revocation_endpoint")));
}

protected static OidcClientException toOidcClientException(String authServerUrlString, Throwable cause) {
Expand All @@ -233,8 +234,24 @@ public Uni<Tokens> refreshTokens(String refreshToken) {
throw new DisabledOidcClientException(message);
}

@Override
public Uni<Void> revokeAccessToken(String accessToken) {
throw new DisabledOidcClientException(message);
}

@Override
public void close() throws IOException {
}

}

private static class OidcConfigurationMetadata {
private final String tokenRequestUri;
private final String tokenRevokeUri;

OidcConfigurationMetadata(String tokenRequestUri, String tokenRevokeUri) {
this.tokenRequestUri = tokenRequestUri;
this.tokenRevokeUri = tokenRevokeUri;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public class OidcCommonConfig {
@ConfigItem
public Optional<String> tokenPath = Optional.empty();

/**
* Relative path or absolute URL of the OIDC token revocation endpoint.
*/
@ConfigItem
public Optional<String> revokePath = Optional.empty();

/**
* The client-id of the application. Each application has a client-id that is used to identify the application
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public final class OidcConstants {
public static final String INTROSPECTION_TOKEN_USERNAME = "username";
public static final String INTROSPECTION_TOKEN_SUB = "sub";

public static final String REVOCATION_TOKEN = "token";

public static final String PASSWORD_GRANT_USERNAME = "username";
public static final String PASSWORD_GRANT_PASSWORD = "password";

Expand Down

0 comments on commit de3599a

Please sign in to comment.