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 28, 2023
1 parent 7d4f0c6 commit 3896883
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 37 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,7 +17,9 @@
import io.quarkus.oidc.client.OidcClientConfig;
import io.quarkus.oidc.client.OidcClientException;
import io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.OidcRequestFilter.Endpoint;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.smallrye.mutiny.Uni;
Expand Down Expand Up @@ -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(Endpoint.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(Endpoint.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(Endpoint.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,21 @@ 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(Endpoint endpoint, 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(endpoint, 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(Endpoint endpoint, HttpRequest<Buffer> request, MultiMap formBody,
Map<String, String> additionalGrantParameters,
boolean refresh) {
MultiMap body = formBody;
Expand Down Expand Up @@ -165,7 +169,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(endpoint, request, buffer).sendBuffer(buffer)
.onFailure(ConnectException.class)
.retry()
.atMost(oidcConfig.connectionRetryCount)
Expand Down Expand Up @@ -259,9 +263,15 @@ 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(Endpoint endpoint, HttpRequest<Buffer> request, Buffer body) {
if (!filters.isEmpty()) {
OidcRequestContextProperties props = new OidcRequestContextProperties();
for (OidcRequestFilter filter : filters) {
Endpoint filterEndpoint = filter.endpoint();
if (Endpoint.ALL == filterEndpoint || endpoint == filterEndpoint) {
filter.filter(request, body, props);
}
}
}
return request;
}
Expand Down
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 @@ -8,12 +8,49 @@
* which are made from the OIDC adapter to the OIDC provider
*/
public interface OidcRequestFilter {

enum Endpoint {
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
}

/**
* 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);

default Endpoint endpoint() {
return Endpoint.ALL;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import io.quarkus.arc.ArcContainer;
import io.quarkus.credentials.CredentialsProvider;
import io.quarkus.credentials.runtime.CredentialsProviderFinder;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.OidcRequestFilter.Endpoint;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Provider;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret;
Expand Down Expand Up @@ -429,10 +431,17 @@ public static Predicate<? super Throwable> oidcEndpointNotAvailable() {

public static Uni<JsonObject> discoverMetadata(WebClient client, List<OidcRequestFilter> filters,
String authServerUrl, long connectionDelayInMillisecs) {
final String discoveryUrl = authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION;
final String discoveryUrl = getDiscoveryUri(authServerUrl);
HttpRequest<Buffer> request = client.getAbs(discoveryUrl);
for (OidcRequestFilter filter : filters) {
filter.filter(request, null, null);
if (!filters.isEmpty()) {
OidcRequestContextProperties requestProps = new OidcRequestContextProperties(
Map.of(OidcRequestContextProperties.DISCOVERY_ENDPOINT, discoveryUrl));
for (OidcRequestFilter filter : filters) {
Endpoint filterEndpoint = filter.endpoint();
if (filterEndpoint == Endpoint.ALL || filterEndpoint == Endpoint.DISCOVERY) {
filter.filter(request, null, requestProps);
}
}
}
return request.send().onItem().transform(resp -> {
if (resp.statusCode() == 200) {
Expand All @@ -452,6 +461,10 @@ public static Uni<JsonObject> discoverMetadata(WebClient client, List<OidcReques
});
}

public static String getDiscoveryUri(String authServerUrl) {
return authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION;
}

private static byte[] getFileContent(Path path) throws IOException {
byte[] data;
final InputStream resource = Thread.currentThread().getContextClassLoader()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class OidcConfigurationMetadata {
private static final String END_SESSION_ENDPOINT = "end_session_endpoint";
private static final String SCOPES_SUPPORTED = "scopes_supported";

private final String discoveryUri;
private final String tokenUri;
private final String introspectionUri;
private final String authorizationUri;
Expand All @@ -33,6 +34,7 @@ public OidcConfigurationMetadata(String tokenUri,
String userInfoUri,
String endSessionUri,
String issuer) {
this.discoveryUri = null;
this.tokenUri = tokenUri;
this.introspectionUri = introspectionUri;
this.authorizationUri = authorizationUri;
Expand All @@ -44,10 +46,12 @@ public OidcConfigurationMetadata(String tokenUri,
}

public OidcConfigurationMetadata(JsonObject wellKnownConfig) {
this(wellKnownConfig, null);
this(wellKnownConfig, null, null);
}

public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMetadata localMetadataConfig) {
public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMetadata localMetadataConfig,
String discoveryUri) {
this.discoveryUri = discoveryUri;
this.tokenUri = getMetadataValue(wellKnownConfig, TOKEN_ENDPOINT,
localMetadataConfig == null ? null : localMetadataConfig.tokenUri);
this.introspectionUri = getMetadataValue(wellKnownConfig, INTROSPECTION_ENDPOINT,
Expand All @@ -69,6 +73,10 @@ private static String getMetadataValue(JsonObject wellKnownConfig, String proper
return localValue != null ? localValue : wellKnownConfig.getString(propertyName);
}

public String getDiscoveryUri() {
return discoveryUri;
}

public String getTokenUri() {
return tokenUri;
}
Expand Down

0 comments on commit 3896883

Please sign in to comment.