From 39b509a97ac1218375653f003c5a4772fff21559 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 12 Dec 2023 15:27:37 +0000 Subject: [PATCH] Improve OIDC bearer token concept doc --- ...rity-oidc-bearer-token-authentication.adoc | 73 +++++++++++++++++-- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index 16261ebf9deb5..e56a94ac75e7c 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -108,6 +108,27 @@ SecurityIdentity roles can be mapped from the verified JWT access tokens as foll * If `groups` claim is available then its value is used * If `realm_access/roles` or `resource_access/client_id/roles` (where `client_id` is the value of the `quarkus.oidc.client-id` property) claim is available then its value is used. This check supports the tokens issued by Keycloak + +For example, the following JWT token has a complex `groups` claim which contains an array `roles` containing roles: + +[source,json] +---- +{ + "iss": "https://server.example.com", + "sub": "24400320", + "upn": "jdoe@example.com", + "preferred_username": "jdoe", + "exp": 1311281970, + "iat": 1311280970, + "groups": { + "roles": [ + "microprofile_jwt_user" + ], + } +} +---- + +`microprofile_jwt_user` role has to be mapped to SecurityIdentity roles and you can do it with this configuration: `quarkus.oidc.roles.role-claim-path=groups/roles`. If the token is opaque (binary) then a `scope` property from the remote token introspection response will be used. @@ -123,6 +144,7 @@ SecurityIdentity permissions are mapped in the form of the `io.quarkus.security. [source, java] ---- +import java.util.List; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -152,6 +174,20 @@ public class ProtectedResource { return List.of(new Order(1)); } + public static class Order { + String id; + public Order() { + } + public Order(String id) { + this.id = id; + } + public String getId() { + return id; + } + public void setId() { + this.id = id; + } + } } ---- <1> Only requests with OpenID Connect scope `email` are going to be granted access. @@ -262,6 +298,7 @@ In such cases you may want to consider skipping the issuer verification by setti import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; import org.eclipse.microprofile.jwt.JsonWebToken; @@ -739,7 +776,6 @@ and finally write the test code, for example: import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken; import static org.hamcrest.Matchers.equalTo; -import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import io.quarkus.test.common.QuarkusTestResource; @@ -753,7 +789,7 @@ public class BearerTokenAuthorizationTest { @Test public void testBearerToken() { - RestAssured.given().auth().oauth2(getAccessToken("alice")))) + RestAssured.given().auth().oauth2(getAccessToken("alice")) .when().get("/api/users/preferredUserName") .then() .statusCode(200) @@ -782,7 +818,7 @@ quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y smallrye.jwt.sign.key.location=/privateKey.pem ---- -copy `privateKey.pem` from the `integration-tests/oidc-tenancy` in the `main` Quarkus repository and use a test code similar to the one in the `Wiremock` section above to generate JWT tokens. You can use your own test keys if preferred. +copy link:https://github.com/quarkusio/quarkus/tree/main/integration-tests/oidc-tenancy/src/main/resources/privateKey.pem[privateKey.pem] from the `integration-tests/oidc-tenancy` in the `main` Quarkus repository and use a test code similar to the one in the `Wiremock` section above to generate JWT tokens. You can use your own test keys if preferred. This approach provides a more limited coverage compared to the Wiremock approach - for example, the remote communication code is not covered. @@ -857,8 +893,14 @@ where `ProtectedResource` class may look like this: [source, java] ---- +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.UserInfo; +import io.quarkus.security.Authenticated; + import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/service") @@ -929,7 +971,12 @@ where `ProtectedResource` class may look like this: [source, java] ---- +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + import io.quarkus.oidc.TokenIntrospection; +import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; @Path("/service") @@ -1040,6 +1087,7 @@ package org.acme.quickstart.oidc; import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import jakarta.inject.Inject; +import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import io.vertx.core.eventbus.EventBus; @@ -1053,7 +1101,18 @@ public class OrderResource { @POST public void order(String product, @HeaderParam(AUTHORIZATION) String bearer) { String rawToken = bearer.substring("Bearer ".length()); <1> - eventBus.publish("product-order", new Product(product, 1, rawToken)); + eventBus.publish("product-order", new Product(product, rawToken)); + } + + public static class Product { + public String product; + public String customerAccessToken; + public Product() { + } + public Product(String product, String customerAccessToken) { + this.product = product; + this.customerAccessToken = customerAccessToken; + } } } ---- @@ -1086,9 +1145,8 @@ public class OrderService { @Blocking @ConsumeEvent("product-order") void processOrder(Product product) { - String rawToken = product.customerAccessToken; - AccessTokenCredential token = new AccessTokenCredential(rawToken); - SecurityIdentity = identityProvider.authenticate(token).await().indefinitely(); <2> + AccessTokenCredential tokenCredential = new AccessTokenCredential(product.customerAccessToken); + SecurityIdentity securityIdentity = identityProvider.authenticate(tokenCredential).await().indefinitely(); <2> ... } @@ -1120,7 +1178,6 @@ For more information, see xref:security-code-flow-authentication#oidc-request-fi * xref:security-oidc-configuration-properties-reference.adoc[OIDC configuration properties] * xref:security-oidc-bearer-token-authentication-tutorial.adoc[Protect a service application by using OIDC Bearer token authentication] -* xref:security-protect-service-applications-by-using-oidc-bearer-authentication-how-to.adoc[Protect service applications by using OIDC Bearer token authentication] * https://www.keycloak.org/documentation.html[Keycloak Documentation] * https://openid.net/connect/[OpenID Connect] * https://tools.ietf.org/html/rfc7519[JSON Web Token]