Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OidcProviderClient injection and token revocation #44993

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
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 @@ -1586,6 +1586,56 @@ 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.
You can revoke tokens with `quarkus.oidc.OidcProviderClient` which provides access to the OIDC provider's UserInfo, token introspection and revocation endpoints.

For example, when a local logout with <<oidc-session,OidcSession>> is performed, you can use an injected `OidcProviderClient` to revoke access and refresh tokens associated with the current session:

[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;

import io.smallrye.mutiny.Uni;

@Path("/service")
public class ServiceResource {

@Inject
OidcSession oidcSession;

@Inject
OidcProviderClient oidcProviderClient;

@Inject
AccessTokenCredential accessToken;

@Inject
RefreshToken refreshToken;

@GET
public Uni<String> logout() {
return oidcSession.logout() <1>
.chain(() -> oidcClient.revokeAccessToken(accessToken.getToken())) <2>
.chain(() -> oidcClient.revokeRefreshToken(refreshToken.getToken())) <3>
.map((result) -> "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 Expand Up @@ -1826,7 +1876,6 @@ testImplementation("io.quarkus:quarkus-junit5")
For integration testing against Keycloak, use xref:security-openid-connect-dev-services.adoc[Dev services for Keycloak].
This service initializes a test container, creates a `quarkus` realm, and configures a `quarkus-app` client with the secret `secret`.
It also sets up two users: `alice` with `admin` and `user` roles, and `bob` with the `user` role.
All these properties are customizable. For details, see xref:security-openid-connect-dev-services.adoc#keycloak-initialization[Keycloak Initialization].

First, prepare the `application.properties` file.

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 @@ -409,6 +409,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
Loading