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 Jan 2, 2025
1 parent e4cab19 commit 8874a06
Show file tree
Hide file tree
Showing 25 changed files with 455 additions and 141 deletions.
88 changes: 88 additions & 0 deletions docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,94 @@ public class SecurityEventListener {

TIP: You can listen to other security events as described in the xref:security-customization.adoc#observe-security-events[Observe security events] section of the Security Tips and Tricks guide.


[[oidc-token-revocation]]
=== Token revocation

Sometimes, you may want to revoke the current authorization code flow access and/or refresh tokens.
For example, if the user has logged out, your logout event listener can revoke access refresh tokens associated with the current session.

To support such cases, you can use `quarkus.oidc.OidcProviderClient` which provides access to the OIDC provider's UserInfo, token introspection and revocation endpoints.

For example:

[source, java]
----
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.ObservesAsync;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OidcProviderClient;
import io.quarkus.oidc.RefreshToken;
import io.quarkus.oidc.SecurityEvent;
@ApplicationScoped
public class SecurityEventListener {
public void event(@ObservesAsync SecurityEvent event) {
if (SecurityEvent.Type.OIDC_BACKCHANNEL_LOGOUT_COMPLETED == event.getEventType()) { <1>
OidcProviderClient oidcProvider = event.getSecurityIdentity().getAttribute(OidcProviderClient.class.getName()); <2>
String accessToken = event.getSecurityIdentity().getCredential(AccessTokenCredential.class).getToken(); <3>
oidcProvider.revokeAccessToken(accessToken).await().indefinitely(); <3>
String refreshToken = event.getSecurityIdentity().getCredential(RefreshToken.class).getToken(); <4>
oidcProvider.revokeRefreshToken(refreshToken).await().indefinitely(); <4>
}
}
}
----
<1> Observe OIDC back channel logout completion events
<2> Get `OidcProviderClient` as a security identity attribute.
<3> Get the current authorization code flow access token and revoke it.
<4> Get the current authorization code flow refresh token and revoke it.

Alternatively, when you do a local logout with <<oidc-session,OidcSession>>, you can inject `OidcProviderClient`, `AccessTokenCredential` and `RefreshToken`.
For example:

[source,java]
----
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OidcProviderClient;
import io.quarkus.oidc.OidcSession;
import io.quarkus.oidc.RefreshToken;
@Path("/service")
public class ServiceResource {
@Inject
OidcSession oidcSession;
@Inject
OidcProviderClient oidcProviderClient;
@Inject
AccessTokenCredential accessToken;
@Inject
RefreshToken refreshToken;
@GET
@Path("logout")
public String logout() {
oidcSession.logout().await().indefinitely(); <1>
oidcProvider.revokeAccessToken(accessToken.getToken()).await().indefinitely(); <2>
oidcProvider.revokeRefreshToken(refreshToken.getToken()).await().indefinitely(); <3>
return "You are logged out";
}
}
----
<1> Do the local logout by clearing the session cookie.
<2> Revoke the authorization code flow access token.
<3> Revoke the authorization code flow refresh token.

=== Propagating tokens to downstream services

For information about Authorization Code Flow access token propagation to downstream services, see the xref:security-openid-connect-client-reference.adoc#token-propagation-rest[Token Propagation] section.
Expand Down
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 @@ -80,7 +80,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 @@ -181,7 +181,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,44 @@
package io.quarkus.oidc;

import io.smallrye.mutiny.Uni;

/**
* Provides access to OIDC UserInfo, token introspection and revocation endpoints.
*/
public interface OidcProviderClient {

/**
* Get UserInfo.
*
* @param accessToken access token which is required to access a UserInfo endpoint.
* @return Uni<UserInfo> {@link UserInfo}
*/
Uni<UserInfo> getUserInfo(String accessToken);

/**
* Introspect the access token.
*
* @param accessToken access oken which must be introspected.
* @return Uni<TokenIntrospection> {@link TokenIntrospection}
*/
Uni<TokenIntrospection> introspectAccessToken(String accessToken);

/**
* Revoke the access token.
*
* @param accessToken access token which needs to be revoked.
* @return Uni<Boolean> true if the access token has been revoked or found already being invalidated,
* false if the access token can not be currently revoked in which case a revocation request might be retried.
*/
Uni<Boolean> revokeAccessToken(String accessToken);

/**
* Revoke the refresh token.
*
* @param refreshToken refresh token which needs to be revoked.
* @return Uni<Boolean> true if the refresh token has been revoked or found already being invalidated,
* false if the refresh token can not be currently revoked in which case a revocation request might be retried.
*/
Uni<Boolean> revokeRefreshToken(String refreshToken);

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ public class DynamicVerificationKeyResolver {
HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT,
HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT);

private final OidcProviderClient client;
private final OidcProviderClientImpl client;
private final MemoryCache<Key> cache;
final CertChainPublicKeyResolver chainResolverFallback;

public DynamicVerificationKeyResolver(OidcProviderClient client, OidcTenantConfig config) {
public DynamicVerificationKeyResolver(OidcProviderClientImpl client, OidcTenantConfig config) {
this.client = client;
this.cache = new MemoryCache<Key>(client.getVertx(), config.jwks().cleanUpTimerInterval(),
config.jwks().cacheTimeToLive(), config.jwks().cacheSize());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public OidcConfigurationMetadata getOidcMetadata() {
}

@Override
public OidcProviderClient getOidcProviderClient() {
public OidcProviderClientImpl getOidcProviderClient() {
return delegate.getOidcProviderClient();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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.oidc.OidcProviderClient;
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 @@ -408,6 +408,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
Loading

0 comments on commit 8874a06

Please sign in to comment.