Skip to content

Commit

Permalink
Support OidcProviderClient injection and token revocation
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Dec 8, 2024
1 parent e7b3e25 commit 5d751e0
Show file tree
Hide file tree
Showing 16 changed files with 219 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public final class OidcConstants {
public static final String INTROSPECTION_TOKEN_ISS = "iss";

public static final String REVOCATION_TOKEN = "token";
public static final String REVOCATION_TOKEN_TYPE_HINT = "token_type_hint";

public static final String PASSWORD_GRANT_USERNAME = "username";
public static final String PASSWORD_GRANT_PASSWORD = "password";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
import io.quarkus.oidc.runtime.Jose4jRecorder;
import io.quarkus.oidc.runtime.OidcAuthenticationMechanism;
import io.quarkus.oidc.runtime.OidcConfig;
import io.quarkus.oidc.runtime.OidcConfigurationMetadataProducer;
import io.quarkus.oidc.runtime.OidcConfigurationAndProviderProducer;
import io.quarkus.oidc.runtime.OidcIdentityProvider;
import io.quarkus.oidc.runtime.OidcJsonWebTokenProducer;
import io.quarkus.oidc.runtime.OidcRecorder;
Expand Down Expand Up @@ -174,7 +174,7 @@ public void additionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBea
builder.addBeanClass(OidcAuthenticationMechanism.class)
.addBeanClass(OidcJsonWebTokenProducer.class)
.addBeanClass(OidcTokenCredentialProducer.class)
.addBeanClass(OidcConfigurationMetadataProducer.class)
.addBeanClass(OidcConfigurationAndProviderProducer.class)
.addBeanClass(OidcIdentityProvider.class)
.addBeanClass(DefaultTenantConfigResolver.class)
.addBeanClass(DefaultTokenStateManager.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class OidcConfigurationMetadata {
public static final String USERINFO_ENDPOINT = "userinfo_endpoint";
public static final String END_SESSION_ENDPOINT = "end_session_endpoint";
private static final String REGISTRATION_ENDPOINT = "registration_endpoint";
private static final String REVOCATION_ENDPOINT = "revocation_endpoint";
public static final String SCOPES_SUPPORTED = "scopes_supported";

private final String discoveryUri;
Expand All @@ -26,6 +27,7 @@ public class OidcConfigurationMetadata {
private final String userInfoUri;
private final String endSessionUri;
private final String registrationUri;
private final String revocationUri;
private final String issuer;
private final JsonObject json;

Expand All @@ -36,6 +38,7 @@ public OidcConfigurationMetadata(String tokenUri,
String userInfoUri,
String endSessionUri,
String registrationUri,
String revocationUri,
String issuer) {
this.discoveryUri = null;
this.tokenUri = tokenUri;
Expand All @@ -45,6 +48,7 @@ public OidcConfigurationMetadata(String tokenUri,
this.userInfoUri = userInfoUri;
this.endSessionUri = endSessionUri;
this.registrationUri = registrationUri;
this.revocationUri = revocationUri;
this.issuer = issuer;
this.json = null;
}
Expand All @@ -70,6 +74,8 @@ public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMe
localMetadataConfig == null ? null : localMetadataConfig.endSessionUri);
this.registrationUri = getMetadataValue(wellKnownConfig, REGISTRATION_ENDPOINT,
localMetadataConfig == null ? null : localMetadataConfig.registrationUri);
this.revocationUri = getMetadataValue(wellKnownConfig, REVOCATION_ENDPOINT,
localMetadataConfig == null ? null : localMetadataConfig.revocationUri);
this.issuer = getMetadataValue(wellKnownConfig, ISSUER,
localMetadataConfig == null ? null : localMetadataConfig.issuer);
this.json = wellKnownConfig;
Expand All @@ -87,6 +93,10 @@ public String getTokenUri() {
return tokenUri;
}

public String getRevocationUri() {
return revocationUri;
}

public String getIntrospectionUri() {
return introspectionUri;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkus.oidc.runtime;

import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;

import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.security.identity.SecurityIdentity;

@RequestScoped
public class OidcConfigurationAndProviderProducer {
@Inject
TenantConfigBean tenantConfig;
@Inject
SecurityIdentity identity;

@Produces
@RequestScoped
OidcConfigurationMetadata produceMetadata() {
OidcConfigurationMetadata configMetadata = OidcUtils.getAttribute(identity, OidcUtils.CONFIG_METADATA_ATTRIBUTE);

if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig().tenantEnabled()) {
configMetadata = tenantConfig.getDefaultTenant().provider().getMetadata();
}
if (configMetadata == null) {
throw new OIDCException("OidcConfigurationMetadata can not be injected");
}
return configMetadata;
}

@Produces
@RequestScoped
OidcProviderClient produceProviderClient() {
OidcProviderClient client = null;
String tenantId = OidcUtils.getAttribute(identity, OidcUtils.TENANT_ID_ATTRIBUTE);
if (tenantId != null) {
if (OidcUtils.DEFAULT_TENANT_ID.equals(tenantId)) {
return tenantConfig.getDefaultTenant().getOidcProviderClient();
}
TenantConfigContext context = tenantConfig.getStaticTenant(tenantId);
if (context == null) {
context = tenantConfig.getDynamicTenant(tenantId);
}
if (context != null) {
client = context.getOidcProviderClient();
}
}
if (client == null) {
throw new OIDCException("OidcProviderClient can not be injected");
}
return client;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ public String getName() {
var vertxContext = getRoutingContextAttribute(request);
OidcUtils.setBlockingApiAttribute(builder, vertxContext);
OidcUtils.setRoutingContextAttribute(builder, vertxContext);
OidcUtils.setOidcProviderClientAttribute(builder, resolvedContext.getOidcProviderClient());
SecurityIdentity identity = builder.build();
// If the primary token is a bearer access token then there's no point of checking if
// it should be refreshed as RT is only available for the code flow tokens
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,8 @@ private boolean isTokenExpired(Long exp) {
}

private int getLifespanGrace() {
return client.getOidcConfig().token().lifespanGrace().isPresent()
? client.getOidcConfig().token().lifespanGrace().getAsInt()
return oidcConfig.token().lifespanGrace().isPresent()
? oidcConfig.token().lifespanGrace().getAsInt()
: 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private static String initIntrospectionBasicAuthScheme(OidcTenantConfig oidcConf
}
}

public OidcConfigurationMetadata getMetadata() {
OidcConfigurationMetadata getMetadata() {
return metadata;
}

Expand Down Expand Up @@ -116,18 +116,18 @@ private Uni<JsonWebKeySet> doGetJsonWebKeySet(OidcRequestContextProperties reque
.transform(resp -> getJsonWebKeySet(requestProps, resp));
}

public Uni<UserInfoResponse> getUserInfo(final String token) {
public Uni<UserInfoResponse> getUserInfo(final String accessToken) {

final OidcRequestContextProperties requestProps = getRequestProps(null, null);

return doGetUserInfo(requestProps, token, List.of())
return doGetUserInfo(requestProps, accessToken, List.of())
.onFailure(OidcCommonUtils.validOidcClientRedirect(metadata.getUserInfoUri()))
.recoverWithUni(
new Function<Throwable, Uni<? extends UserInfoResponse>>() {
@Override
public Uni<UserInfoResponse> apply(Throwable t) {
OidcClientRedirectException ex = (OidcClientRedirectException) t;
return doGetUserInfo(requestProps, token, ex.getCookies());
return doGetUserInfo(requestProps, accessToken, ex.getCookies());
}
});
}
Expand Down Expand Up @@ -160,10 +160,6 @@ private JsonWebKeySet getJsonWebKeySet(OidcRequestContextProperties requestProps
return new JsonWebKeySet(getString(requestProps, metadata.getJsonWebKeySetUri(), resp, OidcEndpoint.Type.JWKS));
}

public OidcTenantConfig getOidcConfig() {
return oidcConfig;
}

public Uni<AuthorizationCodeTokens> getAuthorizationCodeTokens(String code, String redirectUri, String codeVerifier) {
final MultiMap codeGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
codeGrantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.AUTHORIZATION_CODE);
Expand All @@ -189,6 +185,41 @@ public Uni<AuthorizationCodeTokens> refreshAuthorizationCodeTokens(String refres
.transform(resp -> getAuthorizationCodeTokens(requestProps, resp));
}

public Uni<Boolean> revokeAccessToken(String accessToken) {
return revokeToken(accessToken, OidcConstants.ACCESS_TOKEN_VALUE);
}

public Uni<Boolean> revokeRefreshToken(String refreshToken) {
return revokeToken(refreshToken, OidcConstants.REFRESH_TOKEN_VALUE);
}

private Uni<Boolean> revokeToken(String token, String tokenTypeHint) {

if (metadata.getRevocationUri() != null) {
OidcRequestContextProperties requestProps = getRequestProps(null, null);
MultiMap tokenRevokeParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN, token);
tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN_TYPE_HINT, tokenTypeHint);

return getHttpResponse(requestProps, metadata.getRevocationUri(), tokenRevokeParams, false)
.transform(resp -> toRevokeResponse(requestProps, resp));
} else {
LOG.debugf("The %s token can not be revoked because the revocation endpoint URL is not set", tokenTypeHint);
return Uni.createFrom().item(false);
}

}

private Boolean toRevokeResponse(OidcRequestContextProperties requestProps, HttpResponse<Buffer> resp) {
// Per RFC7009, 200 is returned if a token has been revoked successfully or if the client submitted an
// invalid token, https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.
// 503 is at least theoretically possible if the OIDC server declines and suggests to Retry-After some period of time.
// However this period of time can be set to unpredictable value.
Buffer buffer = resp.body();
OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.TOKEN_REVOCATION);
return resp.statusCode() == 503 ? false : true;
}

private UniOnItem<HttpResponse<Buffer>> getHttpResponse(OidcRequestContextProperties requestProps, String uri,
MultiMap formBody, boolean introspect) {
HttpRequest<Buffer> request = client.postAbs(uri);
Expand Down Expand Up @@ -320,7 +351,7 @@ public void close() {
client.close();
}

public Key getClientJwtKey() {
Key getClientJwtKey() {
return clientJwtKey;
}

Expand Down Expand Up @@ -357,15 +388,15 @@ private OidcRequestContextProperties getRequestProps(OidcRequestContextPropertie
return new OidcRequestContextProperties(newProperties);
}

public Vertx getVertx() {
Vertx getVertx() {
return vertx;
}

public WebClient getWebClient() {
WebClient getWebClient() {
return client;
}

static record UserInfoResponse(String contentType, String data) {
public static record UserInfoResponse(String contentType, String data) {
};

}
Original file line number Diff line number Diff line change
Expand Up @@ -615,9 +615,10 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi
String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath());
String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath());
String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath());
String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath());
String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath());
String revocationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath());
return new OidcConfigurationMetadata(tokenUri,
introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri,
introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri, revocationUri,
oidcConfig.token().issuer().orElse(null));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ static QuarkusSecurityIdentity validateAndCreateIdentity(Map<String, Object> req
builder.setPrincipal(jwtPrincipal);
var vertxContext = getRoutingContextAttribute(request);
setRoutingContextAttribute(builder, vertxContext);
OidcUtils.setOidcProviderClientAttribute(builder, resolvedContext.getOidcProviderClient());
setSecurityIdentityRoles(builder, config, rolesJson);
setSecurityIdentityPermissions(builder, config, rolesJson);
setSecurityIdentityUserInfo(builder, userInfo);
Expand Down Expand Up @@ -419,6 +420,11 @@ public static void setRoutingContextAttribute(QuarkusSecurityIdentity.Builder bu
builder.addAttribute(RoutingContext.class.getName(), routingContext);
}

public static void setOidcProviderClientAttribute(QuarkusSecurityIdentity.Builder builder,
OidcProviderClient oidcProviderClient) {
builder.addAttribute(OidcProviderClient.class.getName(), oidcProviderClient);
}

public static void setSecurityIdentityUserInfo(QuarkusSecurityIdentity.Builder builder, UserInfo userInfo) {
if (userInfo != null) {
builder.addAttribute(USER_INFO_ATTRIBUTE, userInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.quarkus.oidc.RefreshToken;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcProviderClient;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.SecurityIdentityAssociation;
Expand Down Expand Up @@ -56,6 +57,9 @@ public class ProtectedResource {
@Inject
AccessTokenCredential accessTokenCredential;

@Inject
OidcProviderClient oidcProviderClient;

@Inject
RefreshToken refreshToken;

Expand Down Expand Up @@ -189,7 +193,13 @@ public String getAccessToken() {
throw new OIDCException("Access token values are not equal");
}

return accessToken.getRawToken() != null && !accessToken.getRawToken().isEmpty() ? "AT injected" : "no access";
return accessToken.getRawToken() != null && !accessToken.getRawToken().isEmpty()
? "AT injected, active: " + isTokenActive()
: "no access";
}

private boolean isTokenActive() {
return oidcProviderClient.introspectToken(accessTokenCredential.getToken()).await().indefinitely().isActive();
}

@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,7 @@ public void testAccessTokenInjection() throws IOException {

page = webClient.getPage("http://localhost:8081/web-app/access");

assertEquals("AT injected", page.getBody().asNormalizedText());
assertEquals("AT injected, active: true", page.getBody().asNormalizedText());
webClient.getCookieManager().clearCookies();
}
}
Expand Down Expand Up @@ -1240,7 +1240,7 @@ public void testDefaultSessionManagerSplitTokens() throws IOException, Interrupt
page.getBody().asNormalizedText());

page = webClient.getPage("http://localhost:8081/web-app/access/tenant-split-tokens");
assertEquals("tenant-split-tokens:AT injected", page.getBody().asNormalizedText());
assertEquals("tenant-split-tokens:AT injected, active: true", page.getBody().asNormalizedText());
page = webClient.getPage("http://localhost:8081/web-app/refresh/tenant-split-tokens");
assertEquals("tenant-split-tokens:RT injected", page.getBody().asNormalizedText());

Expand Down
5 changes: 5 additions & 0 deletions integration-tests/oidc-wiremock-logout/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
Expand Down
Loading

0 comments on commit 5d751e0

Please sign in to comment.