Skip to content

Commit

Permalink
Merge pull request #16586 from sberyozkin/oidc_client_dynamic_props
Browse files Browse the repository at this point in the history
Update OidcClient to accept dynamic parameters and AccessTokenPropagationFilter to exchange the tokens
  • Loading branch information
sberyozkin authored May 5, 2021
2 parents f5cfe9b + 0bae08c commit 6d3967d
Show file tree
Hide file tree
Showing 29 changed files with 386 additions and 68 deletions.
49 changes: 37 additions & 12 deletions docs/src/main/asciidoc/security-openid-connect-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ public interface ProtectedResourceService {

Alternatively, `OidcClientRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-client-filter.register-filter=true` property is set.

`OidcClientRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-filter.client-name` configuration property.

=== Use OidcClient in MicroProfile RestClient Reactive client filter

`quarkus-oidc-client-reactive-filter` extension provides `io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter`.
Expand All @@ -158,6 +160,8 @@ public interface ProtectedResourceService {
}
----

`OidcClientRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-reactive-filter.client-name` configuration property.

=== Use injected Tokens

If you prefer you can use your own custom filter and inject `Tokens`:
Expand Down Expand Up @@ -396,7 +400,7 @@ quarkus.oidc-client.credentials.jwt.key-password=mykeypassword
quarkus.oidc-client.credentials.jwt.key-id=mykey
----

Using `private_key_jwt` or `private_key_jwt` authentication methods ensures that no client secret goes over the wire.
Using `client_secret_jwt` or `private_key_jwt` authentication methods ensures that no client secret goes over the wire.

[[integration-testing-oidc-client]]
=== Testing
Expand Down Expand Up @@ -449,7 +453,6 @@ import com.github.tomakehurst.wiremock.core.Options.ChunkedEncodingPolicy;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager {
private WireMockServer server;
@Override
Expand Down Expand Up @@ -531,12 +534,18 @@ quarkus.oidc-client.token-path=/protocol/openid-connect/tokens

`quarkus-oidc-token-propagation` extension provide `io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` and `io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter` JAX-RS ClientRequestFilters which propagates the current link:security-openid-connect[Bearer] or link:security-openid-connect-web-authentication[Authorization Code Flow] access token as an HTTP `Authorization` `Bearer` scheme value.

When you need to propagate the current Authorization Code Flow access token then the immediate token propagation will work well - as the code flow access tokens (as opposed to ID tokens) are meant to be propagated for the current Quarkus endpoint to access the remote services on behalf of the currently authenticated user.

However, the direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims.

Additionally, a complex application may need to exchange or update the tokens before propagating them. For example, the access context might be different when Service A is accessing Service B. In this case, Service A might be granted a narrow or a completely different set of scopes to access Service B.

Please see below how both `AccessTokenRequestFilter` and `JsonWebTokenRequestFilter` can help.

=== AccessTokenRequestFilter

`AccessTokenRequestFilter` treats all tokens as Strings and as such it can work with both JWT and opaque tokens.

When you need to propagate the current Authorization Code Flow access token then `AccessTokenRequestFilter` will be the best option as such tokens do not need to be exchanged or otherwise re-enhanced. Authorization Code Flow access tokens may be also be opaque/binary tokens.

You can selectively register `AccessTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.AccessToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example:

[source,java]
Expand Down Expand Up @@ -573,13 +582,28 @@ public interface ProtectedResourceService {

Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-token-propagation.register-filter` property is set to `true` and `quarkus.oidc-token-propagation.json-web-token` property is set to `false` (which is a default value).

This filter will be additionally enhanced in the future to support exchanging the access tokens before propagating them.
==== Exchange Token Before Propagation

=== JsonWebTokenRequestFilter
If the current access token needs to be exchanged before propagation and you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or other OpenId Connect Provider which supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant then you can configure `AccessTokenRequestFilter` like this:

[source,properties]
----
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.oidc-token-propagation.exchange-token=true
----

Using `JsonWebTokenRequestFilter` is recommended if you work with Bearer JWT tokens where these tokens can have their claims such as `issuer` and `audience` modified and the updated tokens secured (for example, re-signed) again. It expects an injected `org.eclipse.microprofile.jwt.JsonWebToken` and therefore will not work with the opaque tokens.
Note `AccessTokenRequestFilter` will use `OidcClient` to exchange the current token and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenId Connect Provider.

Direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims.
`AccessTokenRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation.client-name` configuration property.

=== JsonWebTokenRequestFilter

Using `JsonWebTokenRequestFilter` is recommended if you work with Bearer JWT tokens where these tokens can have their claims such as `issuer` and `audience` modified and the updated tokens secured (for example, re-signed) again. It expects an injected `org.eclipse.microprofile.jwt.JsonWebToken` and therefore will not work with the opaque tokens. Also, if your OpenId Connect Provider supports a Token Exchange protocol then it is recommended to use `AccessTokenRequestFilter` instead - as both JWT and opaque bearer tokens can be securely exchanged with `AccessTokenRequestFilter`.

`JsonWebTokenRequestFilter` makes it easy for `Service A` implemementations to update the injected `org.eclipse.microprofile.jwt.JsonWebToken` with the new `issuer` and `audience` claim values and secure the updated token again with a new signature. The only difficult step is to ensure `Service A` has a signing key - it should be provisioned from a secure file system or from the remote secure storage such as Vault.

Expand Down Expand Up @@ -617,9 +641,11 @@ public interface ProtectedResourceService {
}
----

Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if both `quarkus.oidc-token-propagation.register-filter` and ``quarkus.oidc-token-propagation.json-web-token` properties are set to `true`.
Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if both `quarkus.oidc-token-propagation.register-filter` and `quarkus.oidc-token-propagation.json-web-token` properties are set to `true`.

If this filter has to update the inject token and secure it with a new signature again then you can configure it like this:
==== Update Token Before Propagation

If the injected token needs to have its `iss` (issuer) and/or `aud` (audience) claims updated and secured again with a new signature then you can configure `JsonWebTokenRequestFilter` like this:

[source,properties]
----
Expand All @@ -633,8 +659,7 @@ smallrye.jwt.new-token.audience=http://downstream-resource
smallrye.jwt.new-token.override-matching-claims=true
----


This filter will be additionally enhanced in the future to support exchanging the access tokens before propagating them.
As already noted above, please use `AccessTokenRequestFilter` if you work with Keycloak or OpenId Connect Provider which supports a Token Exchange protocol.

[[integration-testing-token-propagation]]
=== Testing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,9 @@ A request will be sent to the OpenId Provider UserInfo endpoint using the access
[[config-metadata]]
== Configuration Metadata

The discovered link:https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata[OpenId Connect Configuration Metadata] is represented by `io.quarkus.oidc.OidcConfigurationMetadata` and can be either injected or accessed as a `SecurityIdentity` `configuration-metadata` attribute.
The current tenant's discovered link:https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata[OpenId Connect Configuration Metadata] is represented by `io.quarkus.oidc.OidcConfigurationMetadata` and can be either injected or accessed as a `SecurityIdentity` `configuration-metadata` attribute.

The default tenant's `OidcConfigurationMetadata` is injected if the endpoint is public.

== Token Claims And SecurityIdentity Roles

Expand Down
4 changes: 3 additions & 1 deletion docs/src/main/asciidoc/security-openid-connect.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,9 @@ A request will be sent to the OpenId Provider UserInfo endpoint and an `io.quar
[[config-metadata]]
== Configuration Metadata

The discovered link:https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata[OpenId Connect Configuration Metadata] is represented by `io.quarkus.oidc.OidcConfigurationMetadata` and can be either injected or accessed as a `SecurityIdentity` `configuration-metadata` attribute.
The current tenant's discovered link:https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata[OpenId Connect Configuration Metadata] is represented by `io.quarkus.oidc.OidcConfigurationMetadata` and can be either injected or accessed as a `SecurityIdentity` `configuration-metadata` attribute.

The default tenant's `OidcConfigurationMetadata` is injected if the endpoint is public.

== Token Claims And SecurityIdentity Roles

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.quarkus.oidc.client.filter;

import java.io.IOException;
import java.util.Optional;

import javax.annotation.Priority;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Priorities;
import javax.ws.rs.client.ClientRequestContext;
Expand All @@ -11,6 +13,7 @@
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import io.quarkus.oidc.client.runtime.AbstractTokensProducer;
Expand All @@ -24,6 +27,10 @@ public class OidcClientRequestFilter extends AbstractTokensProducer implements C
private static final Logger LOG = Logger.getLogger(OidcClientRequestFilter.class);
private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " ";

@Inject
@ConfigProperty(name = "quarkus.oidc-client-filter.client-name")
Optional<String> clientName;

@Override
public void filter(ClientRequestContext requestContext) throws IOException {
try {
Expand All @@ -41,4 +48,8 @@ private String getAccessToken() {
// It should be reactive when run with Resteasy Reactive
return awaitTokens().getAccessToken();
}

protected Optional<String> clientId() {
return clientName;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.oidc.client.filter.runtime;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
Expand All @@ -12,4 +14,10 @@ public class OidcClientFilterConfig {
*/
@ConfigItem(defaultValue = "false")
public boolean registerFilter;

/**
* Name of the configured OidcClient.
*/
@ConfigItem
public Optional<String> clientName;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.quarkus.oidc.client.reactive.filter;

import java.util.Optional;
import java.util.function.Consumer;

import javax.annotation.Priority;
import javax.inject.Inject;
import javax.ws.rs.Priorities;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter;
Expand All @@ -23,6 +26,10 @@ public class OidcClientRequestReactiveFilter extends AbstractTokensProducer impl
private static final Logger LOG = Logger.getLogger(OidcClientRequestReactiveFilter.class);
private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " ";

@Inject
@ConfigProperty(name = "quarkus.oidc-client-reactive-filter.client-name")
Optional<String> clientName;

protected void initTokens() {
if (earlyTokenAcquisition) {
LOG.debug("Token acquisition will be delayed until this filter is executed to avoid blocking an IO thread");
Expand Down Expand Up @@ -53,4 +60,8 @@ public void accept(Throwable t) {
}
});
}

protected Optional<String> clientId() {
return clientName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.oidc.client.reactive.filter.runtime;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(name = "oidc-client-reactive-filter", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public class OidcClientReactiveFilterConfig {

/**
* Name of the configured OidcClient.
*/
@ConfigItem
public Optional<String> clientName;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.oidc.client;

import java.io.Closeable;
import java.util.Collections;
import java.util.Map;

import io.smallrye.mutiny.Uni;

Expand All @@ -12,7 +14,16 @@ public interface OidcClient extends Closeable {
/**
* Returns the grant tokens
*/
Uni<Tokens> getTokens();
default Uni<Tokens> getTokens() {
return getTokens(Collections.emptyMap());
}

/**
* Returns the grant tokens
*
* @param additionalGrantParameters additional grant parameters
*/
Uni<Tokens> getTokens(Map<String, String> additionalGrantParameters);

/**
* Refreshes the grant tokens
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,31 @@ public static enum Type {
/**
* 'client_credentials' grant requiring an OIDC client authentication only
*/
CLIENT,
CLIENT("client_credentials"),
/**
* 'password' grant requiring both OIDC client and user ('username' and 'password') authentications
*/
PASSWORD
PASSWORD("password"),
/**
* 'authorization_code' grant requiring an OIDC client authentication as well as
* at least 'code' and 'redirect_uri' parameters which must be passed to OidcClient at the token request time.
*/
CODE("authorization_code"),
/**
* 'urn:ietf:params:oauth:grant-type:token-exchange' grant requiring an OIDC client authentication as well as
* at least 'subject_token' parameter which must be passed to OidcClient at the token request time.
*/
EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange");

private String grantType;

private Type(String grantType) {
this.grantType = grantType;
}

public String getGrantType() {
return grantType;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.time.Duration;

import io.vertx.core.json.JsonObject;

/**
* Access and Refresh tokens returned from a token grant request
*/
Expand All @@ -10,18 +12,25 @@ public class Tokens {
final private Long accessTokenExpiresAt;
final private Long refreshTokenTimeSkew;
final private String refreshToken;
final private JsonObject grantResponse;

public Tokens(String accessToken, Long accessTokenExpiresAt, Duration refreshTokenTimeSkewDuration, String refreshToken) {
public Tokens(String accessToken, Long accessTokenExpiresAt, Duration refreshTokenTimeSkewDuration, String refreshToken,
JsonObject grantResponse) {
this.accessToken = accessToken;
this.accessTokenExpiresAt = accessTokenExpiresAt;
this.refreshTokenTimeSkew = refreshTokenTimeSkewDuration == null ? null : refreshTokenTimeSkewDuration.getSeconds();
this.refreshToken = refreshToken;
this.grantResponse = grantResponse;
}

public String getAccessToken() {
return accessToken;
}

public String get(String propertyName) {
return grantResponse.getString(propertyName);
}

public String getRefreshToken() {
return refreshToken;
}
Expand Down
Loading

0 comments on commit 6d3967d

Please sign in to comment.