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 23, 2024
1 parent 2ac009a commit ad479c3
Show file tree
Hide file tree
Showing 24 changed files with 452 additions and 139 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 currenty session.

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

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 @@ -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,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 ad479c3

Please sign in to comment.