Skip to content

Commit

Permalink
Merge pull request #37257 from sberyozkin/oidc_request_filter_type
Browse files Browse the repository at this point in the history
Allow restricting OidcRequestFilters to specific OIDC endpoints
  • Loading branch information
sberyozkin committed Dec 1, 2023
2 parents e00ac7c + efdc5c9 commit b153b63
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,12 @@ xref:security-openid-connect-multitenancy.adoc#tenant-config-resolver[Dynamic te
Authentication that requires dynamic tenant will fail.
====

[[oidc-request-filters]]
== OIDC request filters

You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers, as well as log requests.
For more information, see xref:security-code-flow-authentication#oidc-request-filters[OIDC request filters].

== References

* xref:security-oidc-configuration-properties-reference.adoc[OIDC configuration properties]
Expand Down
67 changes: 63 additions & 4 deletions docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,70 @@ quarkus.oidc.introspection-credentials.name=introspection-user-name
quarkus.oidc.introspection-credentials.secret=introspection-user-secret
----

[[oidc-client-filters]]
==== OIDC request customization
[[oidc-request-filters]]
==== OIDC request filters

You can customize OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers.
For more information, see xref:security-openid-connect-client-reference#oidc-client-filters[Client request customization].
You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers, as well as log requests.

For example:

[source,java]
----
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
public class OidcTokenRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProps) {
OidcConfigurationMetadata metadata = contextProps.get(OidcConfigurationMetadata.class.getName()); <1>
// Metadata URI is absolute, request URI value is relative
if (metadata.getTokenUri().endsWith(request.uri())) { <2>
request.putHeader("TokenGrantDigest", calculateDigest(buffer.toString()));
}
}
private String calculateDigest(String bodyString) {
// Apply the required digest algorithm to the body string
}
}
----
<1> Get `OidcConfigurationMetadata` which contains all supported OIDC endpoint addresses.
<2> Use `OidcConfigurationMetadata` to filter requests to the OIDC token endpoint only.

Alternatively, you can use `OidcRequestFilter.Endpoint` enum to make sure this filter is applied to the token endpoint requests only:

[source,java]
----
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.DISCOVERY) <1>
public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProps) {
request.putHeader("Discovery", "OK");
}
}
----
<1> Restrict this filter to requests targeting the OIDC discovery endpoint only.

==== Redirecting to and from the OIDC provider

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,10 +872,10 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".level=T
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-level=TRACE
----

[[oidc-client-filters]]
== OIDC request customization
[[oidc-request-filters]]
== OIDC request filters

You can customize OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations which can update or add new request headers, for example, a filter can analyze the request body and add its digest as a new header value:
You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations which can update or add new request headers, for example, a filter can analyze the request body and add its digest as a new header value:

[source,java]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import io.quarkus.oidc.client.OidcClientConfig;
import io.quarkus.oidc.client.OidcClientException;
import io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
Expand Down Expand Up @@ -46,12 +48,12 @@ public class OidcClientImpl implements OidcClient {
private final String clientSecretBasicAuthScheme;
private final Key clientJwtKey;
private final OidcClientConfig oidcConfig;
private final List<OidcRequestFilter> filters;
private final Map<OidcEndpoint.Type, List<OidcRequestFilter>> filters;
private volatile boolean closed;

public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType,
MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig,
List<OidcRequestFilter> filters) {
Map<OidcEndpoint.Type, List<OidcRequestFilter>> filters) {
this.client = client;
this.tokenRequestUri = tokenRequestUri;
this.tokenRevokeUri = tokenRevokeUri;
Expand All @@ -71,7 +73,7 @@ public Uni<Tokens> getTokens(Map<String, String> additionalGrantParameters) {
throw new OidcClientException(
"Only 'refresh_token' grant is supported, please call OidcClient#refreshTokens method instead");
}
return getJsonResponse(tokenGrantParams, additionalGrantParameters, false);
return getJsonResponse(OidcEndpoint.Type.TOKEN, tokenGrantParams, additionalGrantParameters, false);
}

@Override
Expand All @@ -82,7 +84,7 @@ public Uni<Tokens> refreshTokens(String refreshToken, Map<String, String> additi
}
MultiMap refreshGrantParams = copyMultiMap(commonRefreshGrantParams);
refreshGrantParams.add(OidcConstants.REFRESH_TOKEN_VALUE, refreshToken);
return getJsonResponse(refreshGrantParams, additionalGrantParameters, true);
return getJsonResponse(OidcEndpoint.Type.TOKEN, refreshGrantParams, additionalGrantParameters, true);
}

@Override
Expand All @@ -94,7 +96,8 @@ public Uni<Boolean> revokeAccessToken(String accessToken, Map<String, String> ad
if (tokenRevokeUri != null) {
MultiMap tokenRevokeParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN, accessToken);
return postRequest(client.postAbs(tokenRevokeUri), tokenRevokeParams, additionalParameters, false)
return postRequest(OidcEndpoint.Type.TOKEN_REVOCATION, client.postAbs(tokenRevokeUri), tokenRevokeParams,
additionalParameters, false)
.transform(resp -> toRevokeResponse(resp));
} else {
LOG.debugf("%s OidcClient can not revoke the access token because the revocation endpoint URL is not set");
Expand All @@ -111,20 +114,23 @@ private Boolean toRevokeResponse(HttpResponse<Buffer> resp) {
return resp.statusCode() == 503 ? false : true;
}

private Uni<Tokens> getJsonResponse(MultiMap formBody, Map<String, String> additionalGrantParameters, boolean refresh) {
private Uni<Tokens> getJsonResponse(OidcEndpoint.Type endpointType, 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() {
return postRequest(client.postAbs(tokenRequestUri), formBody, additionalGrantParameters, refresh)
return postRequest(endpointType, client.postAbs(tokenRequestUri), formBody, additionalGrantParameters, refresh)
.transform(resp -> emitGrantTokens(resp, refresh));
}
});
}

private UniOnItem<HttpResponse<Buffer>> postRequest(HttpRequest<Buffer> request, MultiMap formBody,
private UniOnItem<HttpResponse<Buffer>> postRequest(OidcEndpoint.Type endpointType, HttpRequest<Buffer> request,
MultiMap formBody,
Map<String, String> additionalGrantParameters,
boolean refresh) {
MultiMap body = formBody;
Expand Down Expand Up @@ -165,7 +171,7 @@ private UniOnItem<HttpResponse<Buffer>> postRequest(HttpRequest<Buffer> request,
}
// Retry up to three times with a one-second delay between the retries if the connection is closed
Buffer buffer = OidcCommonUtils.encodeForm(body);
Uni<HttpResponse<Buffer>> response = filter(request, buffer).sendBuffer(buffer)
Uni<HttpResponse<Buffer>> response = filter(endpointType, request, buffer).sendBuffer(buffer)
.onFailure(ConnectException.class)
.retry()
.atMost(oidcConfig.connectionRetryCount)
Expand Down Expand Up @@ -259,9 +265,12 @@ private void checkClosed() {
}
}

private HttpRequest<Buffer> filter(HttpRequest<Buffer> request, Buffer body) {
for (OidcRequestFilter filter : filters) {
filter.filter(request, body, null);
private HttpRequest<Buffer> filter(OidcEndpoint.Type endpointType, HttpRequest<Buffer> request, Buffer body) {
if (!filters.isEmpty()) {
OidcRequestContextProperties props = new OidcRequestContextProperties();
for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, endpointType)) {
filter.filter(request, body, props);
}
}
return request;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.quarkus.oidc.client.OidcClientException;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
Expand Down Expand Up @@ -122,7 +123,7 @@ protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig

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

List<OidcRequestFilter> clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer();
Map<OidcEndpoint.Type, List<OidcRequestFilter>> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters();

Uni<OidcConfigurationMetadata> tokenUrisUni = null;
if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.tokenPath)) {
Expand All @@ -137,7 +138,7 @@ protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig
OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath),
OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath)));
} else {
tokenUrisUni = discoverTokenUris(client, clientRequestFilters, authServerUriString.toString(), oidcConfig);
tokenUrisUni = discoverTokenUris(client, oidcRequestFilters, authServerUriString.toString(), oidcConfig);
}
}
return tokenUrisUni.onItemOrFailure()
Expand Down Expand Up @@ -193,7 +194,7 @@ public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) {
tokenGrantParams,
commonRefreshGrantParams,
oidcConfig,
clientRequestFilters);
oidcRequestFilters);
}

});
Expand All @@ -211,10 +212,10 @@ private static void setGrantClientParams(OidcClientConfig oidcConfig, MultiMap g
}

private static Uni<OidcConfigurationMetadata> discoverTokenUris(WebClient client,
List<OidcRequestFilter> clientRequestFilters,
Map<OidcEndpoint.Type, List<OidcRequestFilter>> oidcRequestFilters,
String authServerUrl, OidcClientConfig oidcConfig) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return OidcCommonUtils.discoverMetadata(client, clientRequestFilters, authServerUrl, connectionDelayInMillisecs)
return OidcCommonUtils.discoverMetadata(client, oidcRequestFilters, authServerUrl, connectionDelayInMillisecs)
.onItem().transform(json -> new OidcConfigurationMetadata(json.getString("token_endpoint"),
json.getString("revocation_endpoint")));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.oidc.common;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Annotation that can be used to restrict {@link OidcRequestFilter} to specific OIDC endpoints
*/
@Target({ TYPE })
@Retention(RUNTIME)
public @interface OidcEndpoint {

enum Type {
ALL,

/**
* Applies to OIDC discovery requests
*/
DISCOVERY,

/**
* Applies to OIDC token endpoint requests
*/
TOKEN,

/**
* Applies to OIDC token revocation endpoint requests
*/
TOKEN_REVOCATION,

/**
* Applies to OIDC token introspection requests
*/
INTROSPECTION,
/**
* Applies to OIDC JSON Web Key Set endpoint requests
*/
JWKS,
/**
* Applies to OIDC UserInfo endpoint requests
*/
USERINFO
}

/**
* Identifies an OIDC tenant to which a given feature applies.
*/
Type value() default Type.ALL;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package io.quarkus.oidc.common;

import java.util.Collections;
import java.util.Map;

public class OidcRequestContextProperties {

public static String TOKEN = "token";
public static String TOKEN_CREDENTIAL = "token_credential";
public static String DISCOVERY_ENDPOINT = "discovery_endpoint";

private final Map<String, Object> properties;

public OidcRequestContextProperties() {
this(Map.of());
}

public OidcRequestContextProperties(Map<String, Object> properties) {
this.properties = properties;
}

public Object get(String name) {
return properties.get(name);
public <T> T get(String name) {
@SuppressWarnings("unchecked")
T value = (T) properties.get(name);
return value;
}

public String getString(String name) {
Expand All @@ -25,4 +33,8 @@ public <T> T get(String name, Class<T> type) {
return type.cast(get(name));
}

public Map<String, Object> getAll() {
return Collections.unmodifiableMap(properties);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@

/**
* Request filter which can be used to customize requests such as the verification JsonWebKey set and token grant requests
* which are made from the OIDC adapter to the OIDC provider
* which are made from the OIDC adapter to the OIDC provider.
* <p/>
* Filter can be restricted to a specific OIDC endpoint with a {@link OidcEndpoint} annotation.
*/
public interface OidcRequestFilter {

/**
* Filter OIDC requests
*
* @param request HTTP request that can have its headers customized
* @param body request body, will be null for HTTP GET methods, may be null for other HTTP methods
* @param contextProperties context properties that can be available in context of some requests, can be null
* @param contextProperties context properties that can be available in context of some requests
*/
void filter(HttpRequest<Buffer> request, Buffer requestBody, OidcRequestContextProperties contextProperties);
}

0 comments on commit b153b63

Please sign in to comment.