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 30b4f0d207f84..c3ad0d8514da7 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -15,43 +15,45 @@ Secure HTTP access to Jakarta REST (formerly known as JAX-RS) endpoints in your Quarkus supports the Bearer token authentication mechanism through the Quarkus OpenID Connect (OIDC) extension. -The bearer tokens are issued by OIDC and OAuth 2.0 compliant authorization servers, such as link:https://www.keycloak.org[Keycloak]. +The bearer tokens are issued by OIDC and OAuth 2.0-compliant authorization servers, such as link:https://www.keycloak.org[Keycloak]. Bearer token authentication is the process of authorizing HTTP requests based on the existence and validity of a bearer token. The bearer token provides information about the subject of the call, which is used to determine whether or not an HTTP resource can be accessed. The following diagrams outline the Bearer token authentication mechanism in Quarkus: -.Bearer token authentication mechanism in Quarkus with Single-page application +.Bearer token authentication mechanism in Quarkus with single-page application image::security-bearer-token-authorization-mechanism-1.png[alt=Bearer token authentication, width="60%", align=center] -1. The Quarkus service retrieves verification keys from the OpenID Connect provider. The verification keys are used to verify the bearer access token signatures. -2. The Quarkus user accesses the Single-page application. -3. The Single-page application uses Authorization Code Flow to authenticate the user and retrieve tokens from the OpenID Connect provider. -4. The Single-page application uses the access token to retrieve the service data from the Quarkus service. -5. The Quarkus service verifies the bearer access token signature using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the Single-page application. -6. The Single-page application returns the same data to the Quarkus user. +1. The Quarkus service retrieves verification keys from the OIDC provider. +The verification keys are used to verify the bearer access token signatures. +2. The Quarkus user accesses the single-page application (SPA). +3. The single-page application uses Authorization Code Flow to authenticate the user and retrieve tokens from the OIDC provider. +4. The single-page application uses the access token to retrieve the service data from the Quarkus service. +5. The Quarkus service verifies the bearer access token signature by using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the single-page application. +6. The single-page application returns the same data to the Quarkus user. .Bearer token authentication mechanism in Quarkus with Java or command line client image::security-bearer-token-authorization-mechanism-2.png[alt=Bearer token authentication, width="60%", align=center] -1. The Quarkus service retrieves verification keys from the OpenID Connect provider. The verification keys are used to verify the bearer access token signatures. -2. The Client uses `client_credentials` that requires client ID and secret or password grant, which also requires client ID, secret, user name, and password to retrieve the access token from the OpenID Connect provider. -3. The Client uses the access token to retrieve the service data from the Quarkus service. -4. The Quarkus service verifies the bearer access token signature using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the Client. +1. The Quarkus service retrieves verification keys from the OIDC provider. +The verification keys are used to verify the bearer access token signatures. +2. The client uses `client_credentials` that requires client ID and secret or password grant, which requires client ID, secret, username, and password to retrieve the access token from the OIDC provider. +3. The client uses the access token to retrieve the service data from the Quarkus service. +4. The Quarkus service verifies the bearer access token signature by using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the client. -If you need to authenticate and authorize the users using OpenID Connect Authorization Code Flow, see xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications]. -Also, if you use Keycloak and bearer tokens, see xref:security-keycloak-authorization.adoc[Using Keycloak to Centralize Authorization]. +If you need to authenticate and authorize users by using OIDC authorization code flow, see the Quarkus xref:security-oidc-code-flow-authentication.adoc[OpenID Connect authorization code flow mechanism for protecting web applications] guide. +Also, if you use Keycloak and bearer tokens, see the Quarkus xref:security-keycloak-authorization.adoc[Using Keycloak to Centralize Authorization] guide. -To learn about how you can protect service applications by using OIDC Bearer token authentication, see xref:security-oidc-bearer-token-authentication-tutorial.adoc[OIDC Bearer token authentication tutorial]. +To learn about how you can protect service applications by using OIDC Bearer token authentication, see the following tutorial: -If you want to protect web applications by using OIDC authorization code flow authentication, see xref:security-oidc-code-flow-authentication-concept.adoc[OIDC authorization code flow authentication]. +* xref:security-oidc-bearer-token-authentication-tutorial.adoc[Protect a web application by using OpenID Connect (OIDC) authorization code flow]. -For information about how to support multiple tenants, see xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy]. +For information about how to support multiple tenants, see the Quarkus xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] guide. === Accessing JWT claims -If you need to access JWT token claims then you can inject `JsonWebToken`: +If you need to access JWT token claims, you can inject `JsonWebToken`: [source,java] ---- @@ -80,17 +82,19 @@ public class AdminResource { } ---- -Injection of `JsonWebToken` is supported in `@ApplicationScoped`, `@Singleton` and `@RequestScoped` scopes however the use of `@RequestScoped` is required if the individual claims are injected as simple types, please see xref:security-jwt.adoc#supported-injection-scopes[Support Injection Scopes for JsonWebToken and Claims] for more details. +Injection of `JsonWebToken` is supported in `@ApplicationScoped`, `@Singleton`, and `@RequestScoped` scopes. +However, the use of `@RequestScoped` is required if the individual claims are injected as simple types. +For more information, see the xref:security-jwt.adoc#supported-injection-scopes[Support Injection Scopes for JsonWebToken and Claims] section of the Quarkus "Using JWT RBAC" guide. [[user-info]] === User Info -Set `quarkus.oidc.authentication.user-info-required=true` if a UserInfo JSON object from the OIDC userinfo endpoint has to be requested. -A request will be sent to the OpenID Provider UserInfo endpoint and an `io.quarkus.oidc.UserInfo` (a simple `jakarta.json.JsonObject` wrapper) object will be created. -`io.quarkus.oidc.UserInfo` can be either injected or accessed as a SecurityIdentity `userinfo` attribute. +If you must request a UserInfo JSON object from the OIDC `UserInfo` endpoint, set `quarkus.oidc.authentication.user-info-required=true`. +A request is sent to the OIDC provider `UserInfo` endpoint, and an `io.quarkus.oidc.UserInfo` (a simple `javax.json.JsonObject` wrapper) object is created. +`io.quarkus.oidc.UserInfo` can be injected or accessed as a `SecurityIdentity` `userinfo` attribute. [[config-metadata]] -=== Configuration Metadata +=== Configuration metadata 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. @@ -99,19 +103,20 @@ The default tenant's `OidcConfigurationMetadata` is injected if the endpoint is [[token-claims-and-security-identity-roles]] === Token Claims And SecurityIdentity Roles -SecurityIdentity roles can be mapped from the verified JWT access tokens as follows: +You can map `SecurityIdentity` roles from the verified JWT access tokens as follows: -* If `quarkus.oidc.roles.role-claim-path` property is set and matching array or string claims are found then the roles are extracted from these claims. - For example, `customroles`, `customroles/array`, `scope`, `"http://namespace-qualified-custom-claim"/roles`, `"http://namespace-qualified-roles"`, etc. -* 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 +* If the `quarkus.oidc.roles.role-claim-path` property is set, and matching array or string claims are found, then the roles are extracted from these claims. +For example, `customroles`, `customroles/array`, `scope`, `"http://namespace-qualified-custom-claim"/roles`, `"http://namespace-qualified-roles"`. +* If a group's claim is available, then its value is used. +* If a `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. -If the token is opaque (binary) then a `scope` property from the remote token introspection response will be used. +If the token is opaque (binary), then a `scope` property from the remote token introspection response is used. -If UserInfo is the source of the roles then set `quarkus.oidc.authentication.user-info-required=true` and `quarkus.oidc.roles.source=userinfo`, and if needed, `quarkus.oidc.roles.role-claim-path`. +If `UserInfo` is the source of the roles, then set `quarkus.oidc.authentication.user-info-required=true` and `quarkus.oidc.roles.source=userinfo`, and if needed, set `quarkus.oidc.roles.role-claim-path`. -Additionally, a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented in xref:security-customization.adoc#security-identity-customization[Security Identity Customization]. +A custom `SecurityIdentityAugmentor` can also be used to add the roles. +For more information, see the xref:security-customization.adoc#security-identity-customization[Security Identity Customization] section of the Quarkus "Security tips and tricks" guide. [[token-scopes-and-security-identity-permissions]] === Token scopes And SecurityIdentity permissions @@ -154,19 +159,21 @@ public class ProtectedResource { <1> Only requests with OpenID Connect scope `email` are going to be granted access. <2> The read access is limited to the client requests with scope `orders_read`. -Please refer to the Permission annotation section of the xref:security-authorize-web-endpoints-reference.adoc#permission-annotation[Authorization of web endpoints] -guide for more information about the `io.quarkus.security.PermissionsAllowed` annotation. +For more information about the `io.quarkus.security.PermissionsAllowed` annotation, see the xref:security-authorize-web-endpoints-reference.adoc#permission-annotation[Permission annotation] section of the "Authorization of web endpoints" guide. [[token-verification-introspection]] -=== Token Verification And Introspection +=== Token verification and introspection -If the token is a JWT token then, by default, it will be verified with a `JsonWebKey` (JWK) key from a local `JsonWebKeySet` retrieved from the OpenID Connect Provider's JWK endpoint. The token's key identifier `kid` header value will be used to find the matching JWK key. -If no matching `JWK` is available locally then `JsonWebKeySet` will be refreshed by fetching the current key set from the JWK endpoint. The `JsonWebKeySet` refresh can be repeated only after the `quarkus.oidc.token.forced-jwk-refresh-interval` (default is 10 minutes) expires. -If no matching `JWK` is available after the refresh then the JWT token will be sent to the OpenID Connect Provider's token introspection endpoint. +If the token is a JWT token, then, by default, it is verified with a `JsonWebKey` (JWK) key from a local `JsonWebKeySet`, retrieved from the OIDC provider's JWK endpoint. +The key identifier (`kid`) header value is used to find the matching JWK key. +If no matching `JWK` is available locally, then `JsonWebKeySet` is refreshed by fetching the current key set from the JWK endpoint. +The `JsonWebKeySet` refresh can be repeated only after the `quarkus.oidc.token.forced-jwk-refresh-interval` expires. +The default expiry time is 10 minutes. +If no matching `JWK` is available after the refresh, the JWT token is sent to the OIDC provider's token introspection endpoint. -If the token is opaque (it can be a binary token or an encrypted JWT token) then it will always be sent to the OpenID Connect Provider's token introspection endpoint. +If the token is opaque, it can be a binary token or an encrypted JWT token, and it is sent to the OIDC provider's token introspection endpoint. -If you work with JWT tokens only and expect that a matching `JsonWebKey` will always be available (possibly after a key set refresh) then you should disable the token introspection: +If you work only with JWT tokens and expect a matching `JsonWebKey` to always be available, for example, after refreshing a key set, you must disable token introspection, as follows: [source, properties] ---- @@ -174,7 +181,8 @@ quarkus.oidc.token.allow-jwt-introspection=false quarkus.oidc.token.allow-opaque-token-introspection=false ---- -However, there could be cases where JWT tokens must be verified via the introspection only. It can be forced by configuring an introspection endpoint address only, for example, in case of Keycloak you can do it like this: +There might be cases where JWT tokens must be verified through introspection only, which can be forced by configuring an introspection endpoint address only. +The following properties configuration shows you an example of how you can achieve this with Keycloak: [source, properties] ---- @@ -184,9 +192,11 @@ quarkus.oidc.discovery-enabled=false quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect ---- -An advantage of this indirect enforcement of JWT tokens being only introspected remotely is that two remote call are avoided: a remote OIDC metadata discovery call followed by another remote call fetching the verification keys which will not be used, while its disavantage is that the users need to know the introspection endpoint address and configure it manually. +There are advantages and disadvantages to indirectly enforcing the introspection of JWT tokens remotely. +An advantage is that you eliminate the need for two remote calls: a remote OIDC metadata discovery call followed by another remote call to fetch the verification keys that will not be used. +A disadvantage is that you need to know the introspection endpoint address and configure it manually. -The alternative approach is to allow discovering the OIDC metadata (which is a default option) but require that only the remote JWT introspection is performed: +The alternative approach is to allow the default option of OIDC metadata discovery but also require that only the remote JWT introspection is performed, as shown in the following example: [source, properties] ---- @@ -194,20 +204,25 @@ quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.token.require-jwt-introspection-only=true ---- -An advantage of this approach is that the configuration is simple and easy to understand, while its disavantage is that a remote OIDC metadata discovery call is required to discover an introspection endpoint address (though the verification keys will also not be fetched). +An advantage of this approach is that the configuration is simpler and easier to understand. +A disadvantage is that a remote OIDC metadata discovery call is required to discover an introspection endpoint address even though the verification keys will not be fetched. -Note that `io.quarkus.oidc.TokenIntrospection` (a simple `jakarta.json.JsonObject` wrapper) object will be created and can be either injected or accessed as a SecurityIdentity `introspection` attribute if either JWT or opaque token has been successfully introspected. +The `io.quarkus.oidc.TokenIntrospection` object, which is a simple `jakarta.json.JsonObject`, will be created. +It can be injected or accessed as a `SecurityIdentity` `introspection` attribute providing either the JWT or opaque token has been successfully introspected. [[token-introspection-userinfo-cache]] -=== Token Introspection and UserInfo Cache +=== Token introspection and UserInfo cache -All opaque and sometimes JWT Bearer access tokens have to be remotely introspected. If `UserInfo` is also required then the same access token will be used to do a remote call to OpenID Connect Provider again. So, if `UserInfo` is required and the current access token is opaque then for every such token there will be 2 remote calls done - one to introspect it and one to get UserInfo with it, and if the token is JWT then usually only a single remote call will be needed - to get UserInfo with it. +All opaque and sometimes JWT bearer access tokens must be remotely introspected. +If `UserInfo` is also required, the same access token is used in a subsequent remote call to the OIDC provider. +So, if `UserInfo` is required, and the current access token is opaque, two remote calls are made for every such token; one remote call to introspect the token and another to get `UserInfo`. +If the token is JWT, only a single remote call is needed to introspect the token and to also get `UserInfo`. -The cost of making up to 2 remote calls per every incoming bearer or code flow access token can sometimes be problematic. +The cost of making up to two remote calls for every incoming bearer or code flow access token can sometimes be problematic. +If this is the case in production, consider caching the token introspection and `UserInfo` data for a short period, for example, 3 or 5 minutes. -If it is the case in your production then it can be recommended that the token introspection and `UserInfo` data are cached for a short period of time, for example, for 3 or 5 minutes. - -`quarkus-oidc` provides `quarkus.oidc.TokenIntrospectionCache` and `quarkus.oidc.UserInfoCache` interfaces which can be used to implement `@ApplicationScoped` cache implementation which can be used to store and retrieve `quarkus.oidc.TokenIntrospection` and/or `quarkus.oidc.UserInfo` objects, for example: +`quarkus-oidc` provides `quarkus.oidc.TokenIntrospectionCache` and `quarkus.oidc.UserInfoCache` interfaces, usable for `@ApplicationScoped` cache implementation. +Use `@ApplicationScoped` cache implementation to store and retrieve `quarkus.oidc.TokenIntrospection` and/or `quarkus.oidc.UserInfo` objects, as outlined in the following example: [source, java] ---- @@ -219,40 +234,45 @@ public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache } ---- -Each OIDC tenant can either permit or deny storing its `quarkus.oidc.TokenIntrospection` and/or `quarkus.oidc.UserInfo` data with boolean `quarkus.oidc."tenant".allow-token-introspection-cache` and `quarkus.oidc."tenant".allow-user-info-cache` properties. +Each OIDC tenant can either permit or deny the storing of its `quarkus.oidc.TokenIntrospection` data, `quarkus.oidc.UserInfo` data, or both with boolean `quarkus.oidc."tenant".allow-token-introspection-cache` and `quarkus.oidc."tenant".allow-user-info-cache` properties. -Additionally, `quarkus-oidc` provides a simple default memory based token cache which implements both `quarkus.oidc.TokenIntrospectionCache` and `quarkus.oidc.UserInfoCache` interfaces. +Additionally, `quarkus-oidc` provides a simple default memory-based token cache, which implements both `quarkus.oidc.TokenIntrospectionCache` and `quarkus.oidc.UserInfoCache` interfaces. -It can be activated and configured as follows: +You can configure and activate the OIDC token cache as follows: [source, properties] ---- -# 'max-size' is 0 by default so the cache can be activated by setting 'max-size' to a positive value. +# 'max-size' is 0 by default, so the cache can be activated by setting 'max-size' to a positive value: quarkus.oidc.token-cache.max-size=1000 -# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer. +# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer: quarkus.oidc.token-cache.time-to-live=3M -# 'clean-up-timer-interval' is not set by default so the cleanup timer can be activated by setting 'clean-up-timer-interval'. +# 'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval'. quarkus.oidc.token-cache.clean-up-timer-interval=1M ---- -The default cache uses a token as a key and each entry can have `TokenIntrospection` and/or `UserInfo`. It will only keep up to a `max-size` number of entries. If the cache is full when a new entry is to be added then an attempt will be made to find a space for it by removing a single expired entry. Additionally, the cleanup timer, if activated, will periodically check for the expired entries and remove them. +The default cache uses a token as a key, and each entry can have `TokenIntrospection`, `UserInfo`, or both. +It will only keep up to a `max-size` number of entries. +If the cache is already full when a new entry is to be added, an attempt is made to find a space by removing a single expired entry. +Additionally, the cleanup timer, if activated, periodically checks for expired entries and removes them. -Please experiment with the default cache implementation or register a custom one. +You can experiment with the default cache implementation or register a custom one. [[jwt-claim-verification]] -=== JSON Web Token Claim Verification +=== JSON web token claim verification -Once the bearer JWT token's signature has been verified and its `expires at` (`exp`) claim has been checked, the `iss` (`issuer`) claim value is verified next. +When the bearer JWT token's signature has been verified and its `expires at` (`exp`) claim has been checked, the `iss` (`issuer`) claim value is verified next. -By default, the `iss` claim value is compared to the `issuer` property which may have been discovered in the well-known provider configuration. -But if `quarkus.oidc.token.issuer` property is set then the `iss` claim value is compared to it instead. +By default, the `iss` claim value is compared to the `issuer` property, which might have been discovered in the well-known provider configuration. +However, if the `quarkus.oidc.token.issuer` property is set, then the `iss` claim value is compared to it instead. -In some cases, this `iss` claim verification may not work. For example, if the discovered `issuer` property contains an internal HTTP/IP address while the token `iss` claim value contains an external HTTP/IP address. Or when a discovered `issuer` property contains the template tenant variable but the token `iss` claim value has the complete tenant-specific issuer value. +In some cases, this `iss` claim verification might not work. +For example, if the discovered `issuer` property has an internal HTTP/IP address while the token `iss` claim value has an external HTTP/IP address or when a discovered `issuer` property has the template tenant variable, but the token `iss` claim value has the complete tenant-specific issuer value. -In such cases you may want to consider skipping the issuer verification by setting `quarkus.oidc.token.issuer=any`. Please note that it is not recommended and should be avoided unless no other options are available: +Consider skipping the issuer verification by setting `quarkus.oidc.token.issuer=any` in such cases. +Only skip the issuer verification if no other options are available: -- If you work with Keycloak and observe the issuer verification errors due to the different host addresses then configure Keycloak with a `KEYCLOAK_FRONTEND_URL` property to ensure the same host address is used. -- If the `iss` property is tenant specific in a multi-tenant deployment then you can use the `SecurityIdentity` `tenant-id` attribute to check the issuer is correct in the endpoint itself or the custom Jakarta REST filter, for example: +- If you are using Keycloak and observe issuer verification errors caused by different host addresses, configure Keycloak with a `KEYCLOAK_FRONTEND_URL` property to ensure the same host address is used. +- If the `iss` property is tenant-specific in a multitenant deployment, use the `SecurityIdentity` `tenant-id` attribute to check that the issuer is correct in the endpoint itself or the custom JAX-RS filter. [source, java] ---- @@ -282,16 +302,18 @@ public class IssuerValidator implements ContainerRequestFilter { } ---- -Note it is also recommended to use `quarkus.oidc.token.audience` property to verify the token `aud` (`audience`) claim value. +[NOTE] +==== +Consider using the `quarkus.oidc.token.audience` property to verify the token `aud` (audience) claim value. +==== [[single-page-applications]] -=== Single Page Applications - -Single Page Application (SPA) typically uses `XMLHttpRequest`(XHR) and the JavaScript utility code provided by the OpenID Connect provider to acquire a bearer token and use it -to access Quarkus `service` applications. +=== Single-page applications -For example, here is how you can use `keycloak.js` to authenticate the users and refresh the expired tokens from the SPA: +A single-page application (SPA) typically uses `XMLHttpRequest`(XHR) and the JavaScript utility code provided by the OIDC provider to acquire a bearer token to access Quarkus `service` applications. +.Example +You can use `keycloak.js` to authenticate users and refresh the expired tokens from the SPA: [source,html] ---- @@ -332,18 +354,20 @@ For example, here is how you can use `keycloak.js` to authenticate the users and ---- -=== Cross Origin Resource Sharing +=== Cross-origin resource sharing -If you plan to consume your OpenID Connect `service` application from a Single Page Application running on a different domain, you will need to configure CORS (Cross-Origin Resource Sharing). Please read the xref:http-reference.adoc#cors-filter[HTTP CORS documentation] for more details. +If you plan to use your OpenID Connect `service` application from a single-page application running on a different domain, configure cross-origin resource sharing (CORS). +For more information, see the xref:http-reference.adoc#cors-filter[HTTP CORS documentation] documentation. -=== Provider Endpoint configuration +=== Provider endpoint configuration -OIDC `service` application needs to know OpenID Connect provider's token, `JsonWebKey` (JWK) set and possibly `UserInfo` and introspection endpoint addresses. +An OIDC `service` application needs to know the OIDC provider's token, `JsonWebKey` (JWK) set, and possibly `UserInfo` and introspection endpoint addresses. By default, they are discovered by adding a `/.well-known/openid-configuration` path to the configured `quarkus.oidc.auth-server-url`. -Alternatively, if the discovery endpoint is not available, or if you would like to save on the discovery endpoint round-trip, you can disable the discovery and configure them with relative path values, for example: +Alternatively, if the discovery endpoint is unavailable, or if you want to save on the discovery endpoint round-trip, you can disable the discovery and configure them with relative path values. +.Example [source, properties] ---- quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus @@ -358,20 +382,23 @@ quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect ---- -=== Token Propagation +[[oidc-token-propagation]] +=== Token propagation -Please see xref:security-openid-connect-client-reference.adoc#token-propagation[Token Propagation] section about the Bearer access token propagation to the downstream services. +For information about bearer access token propagation to the downstream services, see the xref:security-openid-connect-client-reference.adoc#token-propagation[Token Propagation] section of the Quarkus "OpenID Connect (OIDC) and OAuth2 client and filters reference" guide. [[oidc-provider-authentication]] -=== Oidc Provider Client Authentication +=== OIDC provider client authentication -`quarkus.oidc.runtime.OidcProviderClient` is used when a remote request to an OpenID Connect Provider has to be done. If the bearer token has to be introspected then `OidcProviderClient` has to authenticate to the OpenID Connect Provider. Please see xref:security-oidc-code-flow-authentication.adoc#oidc-provider-client-authentication[OidcProviderClient Authentication] for more information about all the supported authentication options. +`quarkus.oidc.runtime.OidcProviderClient` is used when a remote request to an OIDC provider is required. +If introspection of the bearer token is necessary, then `OidcProviderClient` must authenticate to the OIDC provider. +For information about supported authentication options, see the xref:security-oidc-code-flow-authentication.adoc#oidc-provider-client-authentication[OidcProviderClient Authentication] section in the Quarkus "OpenID Connect authorization code flow mechanism for protecting web applications" guide. [[integration-testing]] === Testing -Start by adding the following dependencies to your test project: - +You can begin testing by adding the following dependencies to your test project: +==== [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -393,12 +420,19 @@ Start by adding the following dependencies to your test project: testImplementation("io.rest-assured:rest-assured") testImplementation("io.quarkus:quarkus-junit5") ---- +==== + +//@sberyozkin - It might be good to add a stament here about the different mechanisms or methods to integration testing. For example, WireMock... etc -[[integration-testing-wiremock]] -==== Wiremock -Add the following dependencies to your test project: +[[wiremock-integration-testing]] +==== WireMock +You can also use link:https://wiremock.org/[WireMock] for integration testing. + +. Add the following dependencies to your test project: ++ +==== [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -414,9 +448,11 @@ Add the following dependencies to your test project: ---- testImplementation("io.quarkus:quarkus-test-oidc-server") ---- - -Prepare the REST test endpoint, set `application.properties`, for example: - +==== ++ +. Prepare the REST test endpoint and set `application.properties`, as shown in the following example: ++ +==== [source, properties] ---- # keycloak.url is set by OidcWiremockTestResource @@ -424,9 +460,10 @@ quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.client-id=quarkus-service-app quarkus.oidc.application-type=service ---- - -and finally write the test code, for example: - +==== +. Finally, write the test code, as shown in the following example: ++ +==== [source, java] ---- import static org.hamcrest.Matchers.equalTo; @@ -451,7 +488,7 @@ public class BearerTokenAuthorizationTest { .when().get("/api/users/me") .then() .statusCode(200) - // the test endpoint returns the name extracted from the injected SecurityIdentity Principal + // The test endpoint returns the name extracted from the injected `SecurityIdentity` principal. .body("userName", equalTo("alice")); } @@ -464,18 +501,17 @@ public class BearerTokenAuthorizationTest { } } ---- +==== +. The `quarkus-test-oidc-server` extension includes a signing RSA private key file in a `JSON Web Key` (`JWK`) format and points to it with a `smallrye.jwt.sign.key.location` configuration property. +You can sign the token by using a no-argument `sign()` operation. -Note that the `quarkus-test-oidc-server` extension includes a signing RSA private key file in a `JSON Web Key` (`JWK`) format and points to it with a `smallrye.jwt.sign.key.location` configuration property. It allows to use a no argument `sign()` operation to sign the token. - -Testing your `quarkus-oidc` `service` application with `OidcWiremockTestResource` provides the best coverage as even the communication channel is tested against the Wiremock HTTP stubs. -`OidcWiremockTestResource` will be enhanced going forward to support more complex bearer token test scenarios. - -If there is an immediate need for a test to define Wiremock stubs not currently supported by `OidcWiremockTestResource` -one can do so via a `WireMockServer` instance injected into the test class, for example: +Testing your `quarkus-oidc` `service` application with `OidcWiremockTestResource` provides the best coverage because even the communication channel is tested against the WireMock HTTP stubs. +It is anticipated that `OidcWiremockTestResource` will be enhanced in an upcoming release to support more complex bearer token test scenarios. +In the meantime, if you need to run a test with WireMock stubs that are not yet supported by `OidcWiremockTestResource`, you can inject a `WireMockServer` instance into the test class, as shown in the following example: [NOTE] ==== -`OidcWiremockTestResource` does not work with `@QuarkusIntegrationTest` against Docker containers, because the Wiremock server is running in the JVM running the test, which cannot be accessed from the Docker container running the Quarkus application. +`OidcWiremockTestResource` does not work with `@QuarkusIntegrationTest` against Docker containers because the WireMock server runs in the JVM that runs the test, which is inaccessible from the Quarkus application Docker container. ==== [source, java] @@ -518,11 +554,13 @@ public class CustomOidcWireMockStubTest { [[integration-testing-keycloak-devservices]] ==== Dev Services for Keycloak -Using xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] is recommended for the integration testing against Keycloak. -`Dev Services for Keycloak` will launch and initialize a test container: it will create a `quarkus` realm, a `quarkus-app` client (`secret` secret) and add `alice` (`admin` and `user` roles) and `bob` (`user` role) users, where all of these properties can be customized. - -First you need to add the following dependency: +Consider using xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] for integration testing against Keycloak. +`Dev Services for Keycloak` will start and initialize a test container. +Then, it will create a `quarkus` realm and a `quarkus-app` client (`secret` secret) and add `alice` (`admin` and `user` roles) and `bob` (`user` role) users, where all of these properties can be customized. +. First, add the following dependency, which provides a utility class `io.quarkus.test.keycloak.client.KeycloakTestClient` that you can use in tests for acquiring the access tokens: ++ +==== [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -532,33 +570,40 @@ First you need to add the following dependency: test ---- - +==== ++ +==== [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- testImplementation("io.quarkus:quarkus-test-keycloak-server") ---- - -which provides a utility class `io.quarkus.test.keycloak.client.KeycloakTestClient` you can use in tests for acquiring the access tokens. - -Next prepare your `application.properties`. You can start with a completely empty `application.properties` as `Dev Services for Keycloak` will register `quarkus.oidc.auth-server-url` pointing to the running test container as well as `quarkus.oidc.client-id=quarkus-app` and `quarkus.oidc.credentials.secret=secret`. - -But if you already have all the required `quarkus-oidc` properties configured then you only need to associate `quarkus.oidc.auth-server-url` with the `prod` profile for `Dev Services for Keycloak`to start a container, for example: - +==== ++ +. Next, prepare your `application.properties` configuration file. +You can start with an empty `application.properties` file because `Dev Services for Keycloak` registers `quarkus.oidc.auth-server-url` and points it to the running test container, `quarkus.oidc.client-id=quarkus-app`, and `quarkus.oidc.credentials.secret=secret`. +. *Optional*: If you have already configured the required `quarkus-oidc` properties, you will need only to associate `quarkus.oidc.auth-server-url` with the `prod` profile for `Dev Services for Keycloak` to start a container, as shown in the following example: ++ +==== [source,properties] ---- %prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus ---- - -If a custom realm file has to be imported into Keycloak before running the tests then you can configure `Dev Services for Keycloak` as follows: - +==== ++ +. If a custom realm file must be imported into Keycloak before running the tests, configure `Dev Services for Keycloak` as follows: ++ +==== [source,properties] ---- %prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.keycloak.devservices.realm-path=quarkus-realm.json ---- +==== ++ +. Finally, write your test, as outlined in the following examples: -Finally, write your test which will be executed in JVM mode: +.Example of a test executed in JVM mode: [source,java] ---- @@ -592,7 +637,7 @@ public class BearerTokenAuthenticationTest { } ---- -and in native mode: +.Example of a test executed in native mode: [source,java] ---- @@ -605,16 +650,26 @@ public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthentication } ---- -Please see xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] for more information about the way it is initialized and configured. +For more information about initializing and configuring Dev Services for Keycloak, see the xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] guide. [[integration-testing-keycloak]] + ==== KeycloakTestResourceLifecycleManager -If you need to do some integration testing against Keycloak then you are encouraged to do it with <>. -Use `KeycloakTestResourceLifecycleManager` for your tests only if there is a good reason not to use `Dev Services for Keycloak`. -Start with adding the following dependency: +You can also use `KeycloakTestResourceLifecycleManager` for integration testing with Keycloak. + +[IMPORTANT] +==== +Where possible, use the method described in <> instead of using `KeycloakTestResourceLifecycleManager`, unless you have specific requirements for using this method. +==== + +The following example provides `io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager`, which is an implementaton of `io.quarkus.test.common.QuarkusTestResourceLifecycleManager` that starts a Keycloak container. + +. To start integration testing, add the following dependency: ++ +==== [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -630,18 +685,18 @@ Start with adding the following dependency: ---- testImplementation("io.quarkus:quarkus-test-keycloak-server") ---- - -which provides `io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager` - an implementation of `io.quarkus.test.common.QuarkusTestResourceLifecycleManager` which starts a Keycloak container. - -And configure the Maven Surefire plugin as follows: - +==== ++ +. Configure the Maven Surefire plugin as follows, or similarly with `maven.failsafe.plugin` for native image testing: ++ +==== [source,xml] ---- maven-surefire-plugin - + ${keycloak.docker.image} +
package org.acme.security.openid.connect;
+
+import org.eclipse.microprofile.jwt.JsonWebToken;
+import jakarta.inject.Inject;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/api/admin")
+public class AdminResource {
+
+    @Inject
+    JsonWebToken jwt;
+
+    @GET
+    @RolesAllowed("admin")
+    @Produces(MediaType.TEXT_PLAIN)
+    public String admin() {
+        return "Access for subject " + jwt.getSubject() + " is granted";
+    }
+}
+

Injection of JsonWebToken is supported in @ApplicationScoped, @Singleton, and @RequestScoped scopes. +However, the use of @RequestScoped is required if the individual claims are injected as simple types. +For more information, see the Support Injection Scopes for JsonWebToken and Claims section of the Quarkus "Using JWT RBAC" guide.

+ +
+

User Info

+

If you must request a UserInfo JSON object from the OIDC UserInfo endpoint, set quarkus.oidc.authentication.user-info-required=true. +A request is sent to the OIDC provider UserInfo endpoint, and an io.quarkus.oidc.UserInfo (a simple javax.json.JsonObject wrapper) object is created. +io.quarkus.oidc.UserInfo can be injected or accessed as a SecurityIdentity userinfo attribute.

+
+
+

Configuration metadata

+

The current tenant’s discovered 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

+

You can map SecurityIdentity roles from the verified JWT access tokens as follows:

+
    +
  • +

    +If the quarkus.oidc.roles.role-claim-path property is set, and matching array or string claims are found, then the roles are extracted from these claims. +For example, customroles, customroles/array, scope, "http://namespace-qualified-custom-claim"/roles, "http://namespace-qualified-roles". +

    +
  • +
  • +

    +If a groups claim is available, then its value is used. +

    +
  • +
  • +

    +If a 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. +

    +
  • +
+

If the token is opaque (binary), then a scope property from the remote token introspection response is used.

+

If UserInfo is the source of the roles, then set quarkus.oidc.authentication.user-info-required=true and quarkus.oidc.roles.source=userinfo, and if needed, set quarkus.oidc.roles.role-claim-path.

+

A custom SecurityIdentityAugmentor can also be used to add the roles. +For more information, see the Security Identity Customization section of the Quarkus "Security tips and tricks" guide.

+
+
+

Token scopes And SecurityIdentity permissions

+

SecurityIdentity permissions are mapped in the form of the io.quarkus.security.StringPermission from the scope parameter of the source of the roles, using the same claim separator.

+
+
+
import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.jwt.Claims;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import io.quarkus.security.PermissionsAllowed;
+
+@Path("/service")
+public class ProtectedResource {
+
+    @Inject
+    JsonWebToken accessToken;
+
+    @PermissionsAllowed("email") <1>
+    @GET
+    @Path("/email")
+    public Boolean isUserEmailAddressVerifiedByUser() {
+        return accessToken.getClaim(Claims.email_verified.name());
+    }
+
+    @PermissionsAllowed("orders_read") <2>
+    @GET
+    @Path("/order")
+    public List<Order> listOrders() {
+        return List.of(new Order(1));
+    }
+
+}
+
+ + +
1 +Only requests with OpenID Connect scope email are going to be granted access. +
2 +The read access is limited to the client requests with scope orders_read. +
+

For more information about the io.quarkus.security.PermissionsAllowed annotation, see the Permission annotation section of the "Authorization of web endpoints" guide.

+
+
+

Token verification and introspection

+

If the token is a JWT token, then, by default, it is verified with a JsonWebKey (JWK) key from a local JsonWebKeySet,retrieved from the OIDC provider’s JWK endpoint. +The key identifier (kid) header value is used to find the matching JWK key. +If no matching JWK is available locally, then JsonWebKeySet is refreshed by fetching the current key set from the JWK endpoint. +The JsonWebKeySet refresh can be repeated only after the quarkus.oidc.token.forced-jwk-refresh-interval expires. +The default expiry time is 10 minutes. +If no matching JWK is available after the refresh, the JWT token is sent to the OIDC provider’s token introspection endpoint.

+

If the token is opaque, it can be a binary token or an encrypted JWT token, and it is sent to the OIDC provider’s token introspection endpoint.

+

If you work only with JWT tokens and expect a matching JsonWebKey to always be available, for example, after refreshing a key set, you must disable token introspection, as follows:

+
+
+
quarkus.oidc.token.allow-jwt-introspection=false
+quarkus.oidc.token.allow-opaque-token-introspection=false
+

There might be cases where JWT tokens must be verified through introspection only, which can be forced by configuring an introspection endpoint address only. +The following properties configuration shows you an example pf how to achieve this with Keycloak:

+
+
+
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
+quarkus.oidc.discovery-enabled=false
+# Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
+quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
+

There are advantages and disadvantages to indirectly enforcing the introspection of JWT tokens remotely. +An advantage is that you eliminate the need for two remote calls: a remote OIDC metadata discovery call followed by another remote call to fetch the verification keys that will not be used. +A disadvantage is that you need to know the introspection endpoint address and configure it manually.

+

The alternative approach is to allow the default option of OIDC metadata discovery but also require that only the remote JWT introspection is performed, as shown in the following example:

+
+
+
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
+quarkus.oidc.token.require-jwt-introspection-only=true
+

An advantage of this approach is that the configuration is simpler and easier to understand. +A disadvantage is that a remote OIDC metadata discovery call is required to discover an introspection endpoint address even though the verification keys will not be fetched.

+

The io.quarkus.oidc.TokenIntrospection object, which is a simple jakarta.json.JsonObject, will be created. +It can be injected or accessed as a SecurityIdentity introspection attribute providing either the JWT or opaque token has been successfully introspected.

+
+
+

Token introspection and UserInfo cache

+

All opaque and sometimes JWT bearer access tokens must be remotely introspected. +If UserInfo is also required, the same access token are used in a subsequent remote call to the OIDC provider. +So, if UserInfo is required, and the current access token is opaque, two remote calls are made for every such token; one remote call to introspect the token and another to get UserInfo. +If the token is JWT, only a single remote call is needed to introspect the token and to also get UserInfo.

+

The cost of making up to two remote calls for every incoming bearer or code flow access token can sometimes be problematic. +If this is the case in production, consider caching the token introspection and UserInfo data for a short period, for example, 3 or 5 minutes.

+

quarkus-oidc provides quarkus.oidc.TokenIntrospectionCache and quarkus.oidc.UserInfoCache interfaces, usable for @ApplicationScoped cache implementation. +Use @ApplicationScoped cache implementation to store and retrieve quarkus.oidc.TokenIntrospection and/or quarkus.oidc.UserInfo objects, as outlined in the following example:

+
+
+
@ApplicationScoped
+@Alternative
+@Priority(1)
+public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
+...
+}
+

Each OIDC tenant can either permit or deny the storing of its quarkus.oidc.TokenIntrospection data, quarkus.oidc.UserInfo data, or both with boolean quarkus.oidc."tenant".allow-token-introspection-cache and quarkus.oidc."tenant".allow-user-info-cache properties.

+

Additionally, quarkus-oidc provides a simple default memory-based token cache, which implements both quarkus.oidc.TokenIntrospectionCache and quarkus.oidc.UserInfoCache interfaces.

+

You can configure and activate the OIDC token cache as follows:

+
+
+
# 'max-size' is 0 by default, so the cache can be activated by setting 'max-size' to a positive value:
+quarkus.oidc.token-cache.max-size=1000
+# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer:
+quarkus.oidc.token-cache.time-to-live=3M
+# 'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval'.
+quarkus.oidc.token-cache.clean-up-timer-interval=1M
+

The default cache uses a token as a key, and each entry can have TokenIntrospection, UserInfo, or both. +It will only keep up to a max-size number of entries. +If the cache is already full when a new entry is to be added, an attempt is made to find a space by removing a single expired entry. +Additionally, the cleanup timer, if activated, periodically checks for expired entries and removes them.

+

You can experiment with the default cache implementation or register a custom one.

+
+
+

JSON web token claim verification

+

When the bearer JWT token’s signature has been verified and its expires at (exp) claim has been checked, the iss (issuer) claim value is verified next.

+

By default, the iss claim value is compared to the issuer property, which might have been discovered in the well-known provider configuration. +However, if the quarkus.oidc.token.issuer property is set, then the iss claim value is compared to it instead.

+

In some cases, this iss claim verification might not work. +For example, if the discovered issuer property has an internal HTTP/IP address while the token iss claim value has an external HTTP/IP address or when a discovered issuer property has the template tenant variable, but the token iss claim value has the complete tenant-specific issuer value.

+

Consider skipping the issuer verification by setting quarkus.oidc.token.issuer=any in such cases. +Only skip the issuer verification if no other options are available:

+
    +
  • +

    +If you are using Keycloak and observe issuer verification errors caused by different host addresses, configure Keycloak with a KEYCLOAK_FRONTEND_URL property to ensure the same host address is used. +

    +
  • +
  • +

    +If the iss property is tenant-specific in a multitenant deployment, use the SecurityIdentity tenant-id attribute to check that the issuer is correct in the endpoint itself or the custom JAX-RS filter. +

    +
  • +
+
+
+
import jakarta.inject.Inject;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.ext.Provider;
+
+import org.eclipse.microprofile.jwt.JsonWebToken;
+import io.quarkus.oidc.OidcConfigurationMetadata;
+import io.quarkus.security.identity.SecurityIdentity;
+
+@Provider
+public class IssuerValidator implements ContainerRequestFilter {
+    @Inject
+    OidcConfigurationMetadata configMetadata;
+
+    @Inject JsonWebToken jwt;
+    @Inject SecurityIdentity identity;
+
+    public void filter(ContainerRequestContext requestContext) {
+        String issuer = configMetadata.getIssuer().replace("{tenant-id}", identity.getAttribute("tenant-id"));
+        if (!issuer.equals(jwt.getIssuer())) {
+            requestContext.abortWith(Response.status(401).build());
+        }
+    }
+}
+
+ + + +
+Note + +

Consider using the quarkus.oidc.token.audience property to verify the token aud (audience) claim value.

+
+
+
+
+

Single-page applications

+

A single-page application (SPA) typically uses XMLHttpRequest(XHR) and the JavaScript utility code provided by the OIDC provider to acquire a bearer token and uses it to access Quarkus service applications.

+
Example

You can use keycloak.js to authenticate users and refresh the expired tokens from the SPA:

+
+
+
<html>
+<head>
+    <title>keycloak-spa</title>
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src="http://localhost:8180/js/keycloak.js"></script>
+    <script>
+        var keycloak = new Keycloak();
+        keycloak.init({onLoad: 'login-required'}).success(function () {
+            console.log('User is now authenticated.');
+        }).error(function () {
+            window.location.reload();
+        });
+        function makeAjaxRequest() {
+            axios.get("/api/hello", {
+                headers: {
+                    'Authorization': 'Bearer ' + keycloak.token
+                }
+            })
+            .then( function (response) {
+                console.log("Response: ", response.status);
+            }).catch(function (error) {
+                console.log('refreshing');
+                keycloak.updateToken(5).then(function () {
+                    console.log('Token refreshed');
+                }).catch(function () {
+                    console.log('Failed to refresh token');
+                    window.location.reload();
+                });
+            });
+    }
+    </script>
+</head>
+<body>
+    <button onclick="makeAjaxRequest()">Request</button>
+</body>
+</html>
+
+
+

Cross-origin resource sharing

+

If you plan to use your OpenID Connect service application from a single-page application running on a different domain, configure cross-origin resource sharing (CORS). +For more information, see the HTTP CORS documentation documentation.

+
+
+

Provider endpoint configuration

+

An OIDC service application needs to know the OIDC provider’s token, JsonWebKey (JWK) set, and possibly UserInfo and introspection endpoint addresses.

+

By default, they are discovered by adding a /.well-known/openid-configuration path to the configured quarkus.oidc.auth-server-url.

+

Alternatively, if the discovery endpoint is unavailable, or if you want to save on the discovery endpoint round-trip, you can disable the discovery and configure them with relative path values.

+
+
Example
+
+
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
+quarkus.oidc.discovery-enabled=false
+# Token endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/token
+quarkus.oidc.token-path=/protocol/openid-connect/token
+# JWK set endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/certs
+quarkus.oidc.jwks-path=/protocol/openid-connect/certs
+# UserInfo endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/userinfo
+quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo
+# Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
+quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
+
+
+

Token propagation

+

For information about bearer access token propagation to the downstream services, see the Token Propagation section of the Quarkus "OpenID Connect (OIDC) and OAuth2 client and filters reference" guide.

+
+
+

OIDC provider client authentication

+

quarkus.oidc.runtime.OidcProviderClient is used when a remote request to an OIDC provider is required. +If introspection of the bearer token is necessary, then OidcProviderClient must authenticate to the OIDC provider. +For information about supported authentication options, see the OidcProviderClient Authentication section in the Quarkus "OpenID Connect authorization code flow mechanism for protecting web applications" guide.

+
+
+

Testing

+

You can begin testing by adding the following dependencies to your test project:

+
+
pom.xml
+
+
    1: <dependency>
+    2:     <groupId>io.rest-assured</groupId>
+    3:     <artifactId>rest-assured</artifactId>
+    4:     <scope>test</scope>
+    5: </dependency>
+    6: <dependency>
+    7:     <groupId>io.quarkus</groupId>
+    8:     <artifactId>quarkus-junit5</artifactId>
+    9:     <scope>test</scope>
+   10: </dependency>
+
+
build.gradle
+
+
+

Wiremock

+

Add the following dependencies to your test project:

+
+
pom.xml
+
+
    1: <dependency>
+    2:     <groupId>io.quarkus</groupId>
+    3:     <artifactId>quarkus-test-oidc-server</artifactId>
+    4:     <scope>test</scope>
+    5: </dependency>
+
+
build.gradle
+
+

Prepare the REST test endpoint and set application.properties, as shown in the following example:

+
+
+
# keycloak.url is set by OidcWiremockTestResource
+quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
+quarkus.oidc.client-id=quarkus-service-app
+quarkus.oidc.application-type=service
+

Finally, write the test code, as shown in the following example:

+
+
+
import static org.hamcrest.Matchers.equalTo;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.oidc.server.OidcWiremockTestResource;
+import io.restassured.RestAssured;
+import io.smallrye.jwt.build.Jwt;
+
+@QuarkusTest
+@QuarkusTestResource(OidcWiremockTestResource.class)
+public class BearerTokenAuthorizationTest {
+
+    @Test
+    public void testBearerToken() {
+        RestAssured.given().auth().oauth2(getAccessToken("alice", Set.of("user")))
+            .when().get("/api/users/me")
+            .then()
+            .statusCode(200)
+            // the test endpoint returns the name extracted from the injected SecurityIdentity Principal
+            .body("userName", equalTo("alice"));
+    }
+
+    private String getAccessToken(String userName, Set<String> groups) {
+        return Jwt.preferredUserName(userName)
+                .groups(groups)
+                .issuer("https://server.example.com")
+                .audience("https://service.example.com")
+                .sign();
+    }
+}
+

The quarkus-test-oidc-server extension includes a signing RSA private key file in a JSON Web Key (JWK) format and points to it with a smallrye.jwt.sign.key.location configuration property. +You can sign the token by using a no-argument sign() operation.

+

Testing your quarkus-oidc service application with OidcWiremockTestResource provides the best coverage because even the communication channel is tested against the Wiremock HTTP stubs. +It is anticipated that OidcWiremockTestResource will be enhanced in an upcoming release to support more complex bearer token test scenarios.

+

In the meantime, if you need to run a test with Wiremock stubs that are not yet supported by OidcWiremockTestResource, inject a WireMockServer instance into the test class, as shown in the following example:

+
+ + + +
+Note + +

OidcWiremockTestResource does not work with @QuarkusIntegrationTest against Docker containers because the Wiremock server runs in the JVM that runs the test, which is inaccessible from the Quarkus application Docker container.

+
+
+
+
+
package io.quarkus.it.keycloak;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static org.hamcrest.Matchers.equalTo;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.oidc.server.OidcWireMock;
+import io.restassured.RestAssured;
+
+@QuarkusTest
+public class CustomOidcWireMockStubTest {
+
+    @OidcWireMock
+    WireMockServer wireMockServer;
+
+    @Test
+    public void testInvalidBearerToken() {
+        wireMockServer.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect")
+                .withRequestBody(matching(".*token=invalid_token.*"))
+                .willReturn(WireMock.aResponse().withStatus(400)));
+
+        RestAssured.given().auth().oauth2("invalid_token").when()
+                .get("/api/users/me/bearer")
+                .then()
+                .statusCode(401)
+                .header("WWW-Authenticate", equalTo("Bearer"));
+    }
+}
+
+
+

Dev Services for Keycloak

+

Consider using Dev Services for Keycloak for integration testing against Keycloak. +Dev Services for Keycloak will start and initialize a test container. +Then, it will create a quarkus realm and a quarkus-app client (secret secret) and add alice (admin and user roles) and bob (user role) users, where all of these properties can be customized.

+

First, add the following dependency, which provides a utility class io.quarkus.test.keycloak.client.KeycloakTestClient that you can use in tests for acquiring the access tokens:

+
+
pom.xml
+
+
    1: <dependency>
+    2:     <groupId>io.quarkus</groupId>
+    3:     <artifactId>quarkus-test-keycloak-server</artifactId>
+    4:     <scope>test</scope>
+    5: </dependency>
+
+
build.gradle
+
+

Next, prepare your application.properties configuration file. +You can start with an empty application.properties file because Dev Services for Keycloak registers quarkus.oidc.auth-server-url and points it to the running test container, quarkus.oidc.client-id=quarkus-app, and quarkus.oidc.credentials.secret=secret.

+

If you have already configured the required quarkus-oidc properties, you will need only to associate quarkus.oidc.auth-server-url with the prod profile for Dev Services for Keycloak to start a container. +See the following example:

+
+
+
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
+

If a custom realm file must be imported into Keycloak before running the tests, configure Dev Services for Keycloak as follows:

+
+
+
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
+quarkus.keycloak.devservices.realm-path=quarkus-realm.json
+

Finally, write your test, as outlined in the following examples:

+
+
Example of a test executed in JVM mode:
+
+
package org.acme.security.openid.connect;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.keycloak.client.KeycloakTestClient;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+public class BearerTokenAuthenticationTest {
+
+    KeycloakTestClient keycloakClient = new KeycloakTestClient();
+
+    @Test
+    public void testAdminAccess() {
+        RestAssured.given().auth().oauth2(getAccessToken("alice"))
+                .when().get("/api/admin")
+                .then()
+                .statusCode(200);
+        RestAssured.given().auth().oauth2(getAccessToken("bob"))
+                .when().get("/api/admin")
+                .then()
+                .statusCode(403);
+    }
+
+    protected String getAccessToken(String userName) {
+        return keycloakClient.getAccessToken(userName);
+    }
+}
+
+
Example of a test executed in native mode:
+
+
package org.acme.security.openid.connect;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthenticationTest {
+}
+

For more information about initializing and configuring Dev Services for Keycloak, see the Dev Services for Keycloak guide.

+
+
+

KeycloakTestResourceLifecycleManager

+

If you need to do some integration testing against Keycloak, consider using [integration-testing-keycloak-devservicesk]].

+

Avoid using KeycloakTestResourceLifecycleManager for your tests unless you have a good reason for not using Dev Services for Keycloak.

+

To start, add the following dependency:

+
+
pom.xml
+
+
    1: <dependency>
+    2:     <groupId>io.quarkus</groupId>
+    3:     <artifactId>quarkus-test-keycloak-server</artifactId>
+    4:     <scope>test</scope>
+    5: </dependency>
+
+
build.gradle
+
+

The previous example provides io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager, which is an implementaton of io.quarkus.test.common.QuarkusTestResourceLifecycleManager that starts a Keycloak container.

+

Configure the Maven Surefire plugin as follows, or similarly with maven.failsafe.plugin for native image testing:

+
+
+
<plugin>
+    <artifactId>maven-surefire-plugin</artifactId>
+    <configuration>
+        <systemPropertyVariables>
+            <!-- or, alternatively, configure 'keycloak.version' -->
+            <keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
+            <!--
+              Disable HTTPS if required:
+              <keycloak.use.https>false</keycloak.use.https>
+            -->
+        </systemPropertyVariables>
+    </configuration>
+</plugin>
+

Prepare the REST test endpoint and set application.properties. +For example:

+
+
+
# keycloak.url is set by KeycloakTestResourceLifecycleManager
+quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
+quarkus.oidc.client-id=quarkus-service-app
+quarkus.oidc.credentials=secret
+quarkus.oidc.application-type=service
+

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;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
+import io.restassured.RestAssured;
+
+@QuarkusTest
+@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
+public class BearerTokenAuthorizationTest {
+
+    @Test
+    public void testBearerToken() {
+        RestAssured.given().auth().oauth2(getAccessToken("alice"))))
+            .when().get("/api/users/preferredUserName")
+            .then()
+            .statusCode(200)
+            // the test endpoint returns the name extracted from the injected SecurityIdentity Principal
+            .body("userName", equalTo("alice"));
+    }
+
+}
+

In the provided example, KeycloakTestResourceLifecycleManager registers two users: alice and admin. +By default: +* The user alice has the user role, which you can customize by using a keycloak.token.user-roles system property. +* The user admin has both the user and admin roles, which you can customize by using the keycloak.token.admin-roles system property.

+

By default, KeycloakTestResourceLifecycleManager uses HTTPS to initialize a Keycloak instance, and this can be disabled by using keycloak.use.https=false. +The default realm name is quarkus, and the client ID is quarkus-service-app. +If you want to customize the values of these, set the keycloak.realm and keycloak.service.client system properties.

+
+
+

Local public key

+

You can use a local inline public key for testing your quarkus-oidc service applications, as shown in the following example:

+
+
+
quarkus.oidc.client-id=test
+quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
+
+smallrye.jwt.sign.key.location=/privateKey.pem
+

To generate JWT tokens, 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 preceding Wiremock section. +You can use your own test keys, if preferred.

+

This approach provides limited coverage compared to the Wiremock approach. +For example, the remote communication code is not covered.

+
+
+

TestSecurity annotation

+

Use @TestSecurity and @OidcSecurity annotations to test the service application endpoint code that depends on the injected JsonWebToken and UserInfo and OidcConfigurationMetadata. +To do this, add the following dependency:

+
+
pom.xml
+
+
    1: <dependency>
+    2:     <groupId>io.quarkus</groupId>
+    3:     <artifactId>quarkus-test-security-oidc</artifactId>
+    4:     <scope>test</scope>
+    5: </dependency>
+
+
build.gradle
+
+

Write a test code as outlined in the following example:

+
+
+
import static org.hamcrest.Matchers.is;
+import org.junit.jupiter.api.Test;
+import io.quarkus.test.common.http.TestHTTPEndpoint;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import io.quarkus.test.security.oidc.Claim;
+import io.quarkus.test.security.oidc.ConfigMetadata;
+import io.quarkus.test.security.oidc.OidcSecurity;
+import io.quarkus.test.security.oidc.OidcConfigurationMetadata;
+import io.quarkus.test.security.oidc.UserInfo;
+import io.restassured.RestAssured;
+
+@QuarkusTest
+@TestHTTPEndpoint(ProtectedResource.class)
+public class TestSecurityAuthTest {
+
+    @Test
+    @TestSecurity(user = "userOidc", roles = "viewer")
+    public void testOidc() {
+        RestAssured.when().get("test-security-oidc").then()
+                .body(is("userOidc:viewer"));
+    }
+
+    @Test
+    @TestSecurity(user = "userOidc", roles = "viewer")
+    @OidcSecurity(claims = {
+            @Claim(key = "email", value = "user@gmail.com")
+    }, userinfo = {
+            @UserInfo(key = "sub", value = "subject")
+    }, config = {
+            @ConfigMetadata(key = "issuer", value = "issuer")
+    })
+    public void testOidcWithClaimsUserInfoAndMetadata() {
+        RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
+                .body(is("userOidc:viewer:user@gmail.com:subject:issuer"));
+    }
+
+}
+

In the previous test code example, the ProtectedResource class might look like the following code snippet:

+
+
+
import io.quarkus.oidc.OidcConfigurationMetadata;
+import io.quarkus.oidc.UserInfo;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+@Path("/service")
+@Authenticated
+public class ProtectedResource {
+
+    @Inject
+    JsonWebToken accessToken;
+    @Inject
+    UserInfo userInfo;
+    @Inject
+    OidcConfigurationMetadata configMetadata;
+
+    @GET
+    @Path("test-security-oidc")
+    public String testSecurityOidc() {
+        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
+    }
+
+    @GET
+    @Path("test-security-oidc-claims-userinfo-metadata")
+    public String testSecurityOidcWithClaimsUserInfoMetadata() {
+        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
+                + ":" + accessToken.getClaim("email")
+                + ":" + userInfo.getString("sub")
+                + ":" + configMetadata.get("issuer");
+    }
+}
+
+ + + +
+Note + +

The @TestSecurity annotation must always be used, and its user property must be returned as JsonWebToken.getName() and roles property. +The JsonWebToken.getGroups(). +@OidcSecurity annotation is optional and can be used to set the additional token claims and the UserInfo and OidcConfigurationMetadata properties. +Additionally, if the quarkus.oidc.token.issuer property is configured, it will be used as an OidcConfigurationMetadata issuer property value.

+
+
+

If you work with opaque tokens, you can test them by using the following code example:

+
+
+
import static org.hamcrest.Matchers.is;
+import org.junit.jupiter.api.Test;
+import io.quarkus.test.common.http.TestHTTPEndpoint;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import io.quarkus.test.security.oidc.OidcSecurity;
+import io.quarkus.test.security.oidc.TokenIntrospection;
+import io.restassured.RestAssured;
+
+@QuarkusTest
+@TestHTTPEndpoint(ProtectedResource.class)
+public class TestSecurityAuthTest {
+
+    @Test
+    @TestSecurity(user = "userOidc", roles = "viewer")
+    @OidcSecurity(introspectionRequired = true,
+        introspection = {
+            @TokenIntrospection(key = "email", value = "user@gmail.com")
+        }
+    )
+    public void testOidcWithClaimsUserInfoAndMetadata() {
+        RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
+                .body(is("userOidc:viewer:userOidc:viewer"));
+    }
+
+}
+

In the previous test code example, the ProtectedResource class might look similar to the following code example:

+
+
+
import io.quarkus.oidc.TokenIntrospection;
+import io.quarkus.security.identity.SecurityIdentity;
+
+@Path("/service")
+@Authenticated
+public class ProtectedResource {
+
+    @Inject
+    SecurityIdentity securityIdentity;
+    @Inject
+    TokenIntrospection introspection;
+
+    @GET
+    @Path("test-security-oidc-opaque-token")
+    public String testSecurityOidcOpaqueToken() {
+        return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next()
+            + ":" + introspection.getString("username")
+            + ":" + introspection.getString("scope")
+            + ":" + introspection.getString("email");
+    }
+}
+

The @TestSecurity, user, and roles attributes are available as TokenIntrospection, username, and scope properties. +Use io.quarkus.test.security.oidc.TokenIntrospection to add the additional introspection response properties, such as an email, and so on.

+
+ + + +
+Tip + +

@TestSecurity and @OidcSecurity can be combined in a meta-annotation, as outlined in the following example:

+
+
+
    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ ElementType.METHOD })
+    @TestSecurity(user = "userOidc", roles = "viewer")
+    @OidcSecurity(introspectionRequired = true,
+        introspection = {
+            @TokenIntrospection(key = "email", value = "user@gmail.com")
+        }
+    )
+    public @interface TestSecurityMetaAnnotation {
+
+    }
+

This is particularly useful if multiple test methods must use the same set of security settings.

+
+
+
+
+
+

Check log errors

+

To see more details about token verification errors, enable io.quarkus.oidc.runtime.OidcProvider and TRACE level logging:

+
+
+
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
+quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
+

To retrieve more details to help troubleshoot an OidcProvider client initialization error, enable io.quarkus.oidc.runtime.OidcRecorder and TRACE level logging as follows:

+
+
+
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
+quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
+
+
+

External and internal access to OIDC providers

+

The externally-accessible token of the OIDC provider and other endpoints might have different HTTP(S) URLs compared to the URLs that are auto-discovered or configured relative to the quarkus.oidc.auth-server-url internal URL. +For example, suppose your SPA acquires a token from an external token endpoint address and sends it to Quarkus as a bearer token. +In that case, an issuer verification failure might be reported by the endpoint.

+

In such cases, if you work with Keycloak, start it with the KEYCLOAK_FRONTEND_URL system property set to the externally-accessible base URL. +If you work with other OIDC providers, refer to your provider’s documentation.

+
+
+

How to use the client-id property

+

The quarkus.oidc.client-id property identifies the OIDC client that requested the current bearer token. +The client requestor can be an SPA application running in a browser or a Quarkus web-app confidential client application propagating the access token to the Quarkus service application.

+

This property is required if the service application is expected to introspect the tokens remotely, which is always the case for opaque tokens. +This property is optional if the local JSON Web Token (JWT) verification only is used.

+

Setting the quarkus.oidc.client-id property is encouraged even if the endpoint does not require access to the remote introspection endpoint. +This is because when client-id is set, it can be used to verify the token audience. +It will also be included in logs when the token verification fails, enabling better traceability of tokens issued to specific clients and analysis over a longer period.

+

For example, if your OIDC provider sets a token audience, consider the following configuration pattern:

+
+
+
# Set client-id
+quarkus.oidc.client-id=quarkus-app
+# Token audience claim must contain 'quarkus-app'
+quarkus.oidc.token.audience=${quarkus.oidc.client-id}
+

If you set quarkus.oidc.client-id, but your endpoint does not require remote access to one of the OIDC provider endpoints, do not set a client secret with quarkus.oidc.credentials or similar because they will not be used.

+
+ + + +
+Note + +

Quarkus web-app applications always require the quarkus.oidc.client-id property.

+
+
+
+ + + + +

+ + +