Skip to content

Commit

Permalink
Allow restricting OidcRequestFilters to specific OIDC endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Nov 30, 2023
1 parent 07ca9f7 commit 7d16feb
Show file tree
Hide file tree
Showing 16 changed files with 293 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

Check warning on line 1114 in docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'OIDC request filters'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'OIDC request filters'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc", "range": {"start": {"line": 1114, "column": 4}}}, "severity": "INFO"}

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.

Check warning on line 1116 in docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc", "range": {"start": {"line": 1116, "column": 120}}}, "severity": "INFO"}
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
74 changes: 70 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,77 @@ 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

Check warning on line 283 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'OIDC request filters'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'OIDC request filters'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 283, "column": 6}}}, "severity": "INFO"}

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.

Check warning on line 285 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 285, "column": 175}}}, "severity": "INFO"}

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:

Check warning on line 320 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 320, "column": 65}}}, "severity": "WARNING"}

[source,java]
----
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
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) {
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
}
@Override
public Endpoint endpoint() {
return Endpoint.TOKEN; <1>
}
}
----
<1> Restrict this filter to requests targeting the OIDC token 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:

Check warning on line 878 in docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-client-reference.adoc", "range": {"start": {"line": 878, "column": 1}}}, "severity": "INFO"}

Check warning on line 878 in docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-client-reference.adoc", "range": {"start": {"line": 878, "column": 128}}}, "severity": "INFO"}

Check warning on line 878 in docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-client-reference.adoc", "range": {"start": {"line": 878, "column": 244}}}, "severity": "INFO"}

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

Please sign in to comment.