Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update OIDC client and token propagation to support a jwt-bearer token grant #29130

Merged
merged 1 commit into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ include::_attributes.adoc[]
This reference guide explains how to use:

- `quarkus-oidc-client`, `quarkus-oidc-client-reactive-filter` and `quarkus-oidc-client-filter` extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as https://www.keycloak.org[Keycloak]
- `quarkus-oidc-token-propagation` and `quarkus-oidc-token-propagation-reactive` extensions to propagate the current `Bearer` or `Authorization Code Flow` access tokens
- `quarkus-oidc-token-propagation-reactive` and `quarkus-oidc-token-propagation` extensions to propagate the current `Bearer` or `Authorization Code Flow` access tokens

The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services.

Expand Down Expand Up @@ -110,7 +110,7 @@ It can be further customized using a `quarkus.oidc-client.grant-options.password

==== Other Grants

`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which can not be captured in the configuration. These grants are `refresh token` (with the external refresh token), `token exchange` and `authorization code`.
`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which can not be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`.

Using the `refresh_token` grant which uses an out-of-band refresh token to acquire a new set of tokens will be required if the existing refresh token has been posted to the current Quarkus endpoint for it to acquire the access token. In this case `OidcClient` needs to be configured as follows:

Expand All @@ -124,7 +124,7 @@ quarkus.oidc-client.grant.type=refresh

and then you can use `OidcClient.refreshTokens` method with a provided refresh token to get the access token.

Using the `token exchange` grant may be required if you are building a complex microservices application and would like to avoid the same `Bearer` token be propagated to and used by more than one service. Please see <<token-propagation,Token Propagation in MicroProfile RestClient client filter>> for more details.
Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants may be required if you are building a complex microservices application and would like to avoid the same `Bearer` token be propagated to and used by more than one service. Please see <<token-propagation-reactive,Token Propagation in MicroProfile RestClient Reactive filter>> and <<token-propagation,Token Propagation in MicroProfile RestClient filter>> for more details.

Using `OidcClient` to support the `authorization code` grant might be required if for some reasons you can not use the xref:security-openid-connect-web-authentication.adoc[Quarkus OpenID Connect extension] to support Authorization Code Flow. If there is a very good reason for you to implement Authorization Code Flow then you can configure `OidcClient` as follows:

Expand Down Expand Up @@ -823,6 +823,64 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".level=T
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-level=TRACE
----

[[token-propagation-reactive]]
== Token Propagation Reactive

The `quarkus-oidc-token-propagation-reactive` extension provides RestEasy Reactive Client `io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter` that simplifies the propagation of authentication information by propagating the xref:security-openid-connect.adoc[Bearer] token present in the current active request or the token acquired from the xref:security-openid-connect-web-authentication.adoc[Authorization Code Flow], as the HTTP `Authorization` header's `Bearer` scheme value.

You can selectively register `AccessTokenRequestReactiveFilter` using `org.eclipse.microprofile.rest.client.annotation.RegisterProvider` annotation, for example:

[source,java]
----
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {

@GET
String getUserName();
}
----


Additionally, `AccessTokenRequestReactiveFilter` can support a complex application that needs to exchange the tokens before propagating them.

If you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or other OpenID Connect Providers which support a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant then you can configure `AccessTokenRequestReactiveFilter` to exchange the token 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
----

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 exhange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this:

[source,properties]
----
quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access

quarkus.oidc-token-propagation-reactive.exchange-token=true
----

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

[[token-propagation]]
== Token Propagation

Expand Down Expand Up @@ -893,6 +951,21 @@ quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.oidc-token-propagation.exchange-token=true
----

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 exhange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this:

[source,properties]
----
quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access

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

`AccessTokenRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation.client-name` configuration property.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ public static enum Type {
* 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"),

/**
* 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant requiring an OIDC client authentication as well as
* at least an 'assertion' parameter which must be passed to OidcClient at the token request time.
*/
JWT("urn:ietf:params:oauth:grant-type:jwt-bearer"),
/**
* 'refresh_token' grant requiring an OIDC client authentication and a refresh token.
* Note, OidcClient supports this grant by default if an access token acquisition response contained a refresh
Expand Down
50 changes: 50 additions & 0 deletions extensions/oidc-token-propagation-reactive/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,36 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-deployment</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -56,4 +86,24 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>test-keycloak</id>
<activation>
<property>
<name>test-containers</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.oidc.token.propagation.reactive;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface AccessTokenPropagationService {

@GET
String getUserName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.oidc.token.propagation.reactive;

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.rest.client.inject.RestClient;

@Path("/frontend")
public class FrontendResource {
@Inject
@RestClient
AccessTokenPropagationService accessTokenPropagationService;

@Inject
JsonWebToken jwt;

@GET
@Path("token-propagation")
@RolesAllowed("admin")
public String userNameTokenPropagation() {
if ("alice".equals(jwt.getName())) {
return "Token issued to " + jwt.getName() + " has been exchanged, new user name: "
+ accessTokenPropagationService.getUserName();
} else {
throw new RuntimeException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.oidc.token.propagation.reactive;

import static org.hamcrest.Matchers.equalTo;

import java.util.Set;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;

@QuarkusTestResource(OidcWiremockTestResource.class)
public class OidcTokenPropagationTest {

private static Class<?>[] testClasses = {
FrontendResource.class,
ProtectedResource.class,
AccessTokenPropagationService.class
};

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(testClasses)
.addAsResource("application.properties"));

@Test
public void testGetUserNameWithTokenPropagation() {
RestAssured.given().auth().oauth2(getBearerAccessToken())
.when().get("/frontend/token-propagation")
.then()
.statusCode(200)
.body(equalTo("Token issued to alice has been exchanged, new user name: bob"));
}

public String getBearerAccessToken() {
return OidcWiremockTestResource.getAccessToken("alice", Set.of("admin"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.oidc.token.propagation.reactive;

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.Authenticated;

@Path("/protected")
@Authenticated
public class ProtectedResource {

@Inject
JsonWebToken jwt;

@GET
@RolesAllowed("user")
public String principalName() {
return jwt.getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret

quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.credentials.client-secret.value=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.credentials.client-secret.method=post
quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.token-path=${keycloak.url}/realms/quarkus/jwt-bearer-token

quarkus.oidc-token-propagation-reactive.exchange-token=true

io.quarkus.oidc.token.propagation.reactive.AccessTokenPropagationService/mp-rest/uri=http://localhost:8081/protected
4 changes: 4 additions & 0 deletions extensions/oidc-token-propagation-reactive/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Loading