Skip to content

Commit

Permalink
Small fixes for OIDC client guides
Browse files Browse the repository at this point in the history
  • Loading branch information
jedla97 committed Oct 18, 2024
1 parent acfcab4 commit ddc0163
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ You can inject `Tokens` that use `OidcClient` internally. `Tokens` can be used t

[source,java]
----
import jakarta.inject.PostConstruct;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.client.Tokens;
Expand Down Expand Up @@ -281,6 +281,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
Expand Down Expand Up @@ -404,6 +405,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.runtime.TokensHelper;
Expand Down Expand Up @@ -442,7 +444,10 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
Expand Down Expand Up @@ -473,6 +478,20 @@ The same qualifier can be used to specify the `OidcClient` used for a `Tokens` i

[source,java]
----
import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.Tokens;
@Provider
@Priority(Priorities.AUTHENTICATION)
@RequestScoped
Expand Down Expand Up @@ -502,22 +521,23 @@ Add the following Maven Dependency:
</dependency>
----

Note it will also bring `io.quarkus:quarkus-oidc-client`.
NOTE: It will also bring `io.quarkus:quarkus-oidc-client`.

`quarkus-rest-client-oidc-filter` extension provides `io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter`.

It works similarly to the way `OidcClientRequestFilter` does (see <<resteasy-client-oidc-filter,Use OidcClient in MicroProfile RestClient client filter>>) - it uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization` `Bearer` scheme value. The difference is that it works with xref:rest-client.adoc[Reactive RestClient] and implements a non-blocking client filter that does not block the current IO thread when acquiring or refreshing the tokens.
It works similarly to the way `OidcClientRequestFilter` does (see <<resteasy-client-oidc-filter,Use OidcClient in MicroProfile RestClient client filter>>) - it uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization Bearer` scheme value. The difference is that it works with xref:rest-client.adoc[Reactive RestClient] and implements a non-blocking client filter that does not block the current IO thread when acquiring or refreshing the tokens.

`OidcClientRequestReactiveFilter` delays an initial token acquisition until it is executed to avoid blocking an IO thread.

You can selectively register `OidcClientRequestReactiveFilter` by using either `io.quarkus.oidc.client.reactive.filter.OidcClientFilter` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider` annotations:

[source,java]
----
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter
Expand All @@ -537,6 +557,8 @@ import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
Expand All @@ -554,10 +576,11 @@ For example, given <<use-oidc-clients,this>> `jwt-secret` named OIDC client decl

[source,java]
----
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
Expand All @@ -582,9 +605,9 @@ Add the following Maven Dependency:
</dependency>
----

Note it will also bring `io.quarkus:quarkus-oidc-client`.
NOTE: It will also bring `io.quarkus:quarkus-oidc-client`.

`quarkus-resteasy-client-oidc-filter` extension provides `io.quarkus.oidc.client.filter.OidcClientRequestFilter` Jakarta REST ClientRequestFilter which uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization` `Bearer` scheme value.
`quarkus-resteasy-client-oidc-filter` extension provides `io.quarkus.oidc.client.filter.OidcClientRequestFilter` Jakarta REST ClientRequestFilter which uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization Bearer` scheme value.

By default, this filter will get `OidcClient` to acquire the first pair of access and refresh tokens at its initialization time. If the access tokens are short-lived and refresh tokens are unavailable, then the token acquisition should be delayed with `quarkus.oidc-client.early-tokens-acquisition=false`.

Expand All @@ -594,6 +617,8 @@ You can selectively register `OidcClientRequestFilter` by using either `io.quark
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter
Expand All @@ -612,6 +637,8 @@ or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(OidcClientRequestFilter.class)
Expand All @@ -633,6 +660,8 @@ For example, given <<use-oidc-clients,this>> `jwt-secret` named OIDC client decl
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
Expand All @@ -650,6 +679,14 @@ If you prefer, you can use your own custom filter and inject `Tokens`:

[source,java]
----
import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.Tokens;
@Provider
Expand Down Expand Up @@ -1003,8 +1040,10 @@ Add the following dependencies to your test project:
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> // <1>
</dependency>
----
<1> Use a proper Wiremock version. All available versions can be found link:https://search.maven.org/artifact/org.wiremock/wiremock[here].

Write a Wiremock-based `QuarkusTestResourceLifecycleManager`, for example:
[source, java]
Expand Down Expand Up @@ -1200,6 +1239,8 @@ You can selectively register `AccessTokenRequestReactiveFilter` by using either
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@AccessToken
Expand All @@ -1218,6 +1259,8 @@ or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
Expand Down Expand Up @@ -1246,7 +1289,7 @@ quarkus.resteasy-client-oidc-token-propagation.exchange-token=true <1>
----
<1> Please note that the `exchange-token` configuration property is ignored when the OidcClient name is set with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute.

Note `AccessTokenRequestReactiveFilter` 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.
NOTE: `AccessTokenRequestReactiveFilter` 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.

If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token, then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this:

Expand Down Expand Up @@ -1290,6 +1333,8 @@ You can selectively register `AccessTokenRequestFilter` by using either `io.quar
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@AccessToken
Expand All @@ -1307,6 +1352,8 @@ or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestFilter.class)
Expand Down Expand Up @@ -1350,22 +1397,24 @@ quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
----

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.
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.

`AccessTokenRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.resteasy-client-oidc-token-propagation.client-name` configuration property.

=== RestClient 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` implementations 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 ensuring that `Service A` has a signing key; it should be provisioned from a secure file system or remote secure storage such as Vault.
`JsonWebTokenRequestFilter` makes it easy for `Service A` implementations 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 ensuring that `Service A` has a signing key which should be provisioned from a secure file system or remote secure storage such as Vault.

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

[source,java]
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@JsonWebToken
Expand All @@ -1383,6 +1432,8 @@ or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(JsonWebTokenRequestFilter.class)
Expand Down
20 changes: 9 additions & 11 deletions docs/src/main/asciidoc/security-openid-connect-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
Expand Down Expand Up @@ -237,7 +236,6 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessToken;
Expand Down Expand Up @@ -446,7 +444,7 @@ public class FrontendExceptionMapper implements ExceptionMapper<ClientWebApplica
----

This exception mapper is only added to verify during the tests that `ProtectedResource` returns `403` when the token has no expected role.
Without this mapper, `Quarkus REST (formerly RESTEasy Reactive)` would correctly convert the exceptions that escape from REST client calls to `500` to avoid leaking the information from the downstream resources such as `ProtectedResource`.
Without this mapper, Quarkus REST (formerly RESTEasy Reactive) would correctly convert the exceptions that escape from REST client calls to `500` to avoid leaking the information from the downstream resources such as `ProtectedResource`.
However, in the tests, it would not be possible to assert that `500` is caused by an authorization exception instead of some internal error.

== Configuring the application
Expand Down Expand Up @@ -487,7 +485,7 @@ org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-
----

The preceding configuration references Keycloak, which is used by `ProtectedResource` to verify the incoming access tokens and by `OidcClient` to get the tokens for a user `alice` by using a `password` grant.
Both REST clients point to `ProtectedResource`'s HTTP address.
Both REST clients point to ``ProtectedResource``'s HTTP address.

NOTE: Adding a `%prod.` profile prefix to `quarkus.oidc.auth-server-url` ensures that `Dev Services for Keycloak` launches a container for you when the application is run in dev or test modes.
For more information, see the <<oidc-client-keycloak-dev-mode,Running the application in dev mode>> section.
Expand Down Expand Up @@ -530,18 +528,18 @@ include::{includes}/devtools/dev.adoc[]

xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] launches a Keycloak container and imports `quarkus-realm.json`.

Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-ui[/q/dev-ui] and click a `Provider: Keycloak` link in the *OpenID Connect Dev UI* card.
Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-ui[/q/dev-ui] and click a `Keycloak provider` link in the *OpenID Connect Dev UI* card.

When asked, log in to a `Single Page Application` provided by the OpenID Connect Dev UI:

* Log in as `alice`, with the password, `alice`.
This user has a `user` role.
** Access `/frontend/user-name-with-propagated-token`, which returns `200`.
** Access `/frontend/admin-name-with-propagated-token`, which returns `403`.
* Log out and back in as `admin` with the password, `admin`.
This user has both `admin` and `user` roles.
** Access `/frontend/user-name-with-propagated-token`, which returns `200`.
** Access `/frontend/admin-name-with-propagated-token`, which returns `200`.
* Log out and back in as `bob` with the password, `bob`.
This user has a `user` role.
** Access `/frontend/user-name-with-propagated-token`, which returns `200`.
** Access `/frontend/admin-name-with-propagated-token`, which returns `403`.

You have tested that `FrontendResource` can propagate the access tokens from the OpenID Connect Dev UI.

Expand Down Expand Up @@ -681,7 +679,7 @@ Call the `/admin-name-with-oidc-client-token-header-param`. In contrast with the
[source,bash]
----
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token-param
http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param
----

Next, test the endpoints which use OIDC client in in the blocking mode.
Expand All @@ -699,7 +697,7 @@ Call the `/admin-name-with-oidc-client-token-header-param-blocking`. In contrast
[source,bash]
----
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token-param-blocking
http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param-blocking
----

== References
Expand Down

0 comments on commit ddc0163

Please sign in to comment.