diff --git a/docs/src/main/asciidoc/images/auth0-add-role-action.png b/docs/src/main/asciidoc/images/auth0-add-role-action.png new file mode 100644 index 0000000000000..99476ee32dc1f Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-add-role-action.png differ diff --git a/docs/src/main/asciidoc/images/auth0-add-role-to-user.png b/docs/src/main/asciidoc/images/auth0-add-role-to-user.png new file mode 100644 index 0000000000000..f60e8fcb3b4c4 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-add-role-to-user.png differ diff --git a/docs/src/main/asciidoc/images/auth0-add-user.png b/docs/src/main/asciidoc/images/auth0-add-user.png new file mode 100644 index 0000000000000..f5e2362504475 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-add-user.png differ diff --git a/docs/src/main/asciidoc/images/auth0-allowed-callback.png b/docs/src/main/asciidoc/images/auth0-allowed-callback.png new file mode 100644 index 0000000000000..697373917cb06 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-allowed-callback.png differ diff --git a/docs/src/main/asciidoc/images/auth0-allowed-callbacks.png b/docs/src/main/asciidoc/images/auth0-allowed-callbacks.png new file mode 100644 index 0000000000000..c7a5b33f3d112 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-allowed-callbacks.png differ diff --git a/docs/src/main/asciidoc/images/auth0-allowed-logout.png b/docs/src/main/asciidoc/images/auth0-allowed-logout.png new file mode 100644 index 0000000000000..01d0e260a40a3 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-allowed-logout.png differ diff --git a/docs/src/main/asciidoc/images/auth0-allowed-logouts.png b/docs/src/main/asciidoc/images/auth0-allowed-logouts.png new file mode 100644 index 0000000000000..a266147af1622 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-allowed-logouts.png differ diff --git a/docs/src/main/asciidoc/images/auth0-api-permissions.png b/docs/src/main/asciidoc/images/auth0-api-permissions.png new file mode 100644 index 0000000000000..57285c64edd19 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-api-permissions.png differ diff --git a/docs/src/main/asciidoc/images/auth0-api.png b/docs/src/main/asciidoc/images/auth0-api.png new file mode 100644 index 0000000000000..056d8e2d40cb8 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-api.png differ diff --git a/docs/src/main/asciidoc/images/auth0-authorize.png b/docs/src/main/asciidoc/images/auth0-authorize.png new file mode 100644 index 0000000000000..09d80a9272881 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-authorize.png differ diff --git a/docs/src/main/asciidoc/images/auth0-continuous-testing.png b/docs/src/main/asciidoc/images/auth0-continuous-testing.png new file mode 100644 index 0000000000000..1648b61fef7b0 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-continuous-testing.png differ diff --git a/docs/src/main/asciidoc/images/auth0-create-application.png b/docs/src/main/asciidoc/images/auth0-create-application.png new file mode 100644 index 0000000000000..b3cc6b5258922 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-create-application.png differ diff --git a/docs/src/main/asciidoc/images/auth0-create-role.png b/docs/src/main/asciidoc/images/auth0-create-role.png new file mode 100644 index 0000000000000..282ab8812b078 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-create-role.png differ diff --git a/docs/src/main/asciidoc/images/auth0-created-application.png b/docs/src/main/asciidoc/images/auth0-created-application.png new file mode 100644 index 0000000000000..0da2170d2806b Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-created-application.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devmode-started.png b/docs/src/main/asciidoc/images/auth0-devmode-started.png new file mode 100644 index 0000000000000..37157b901e0ed Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devmode-started.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-accesstoken.png b/docs/src/main/asciidoc/images/auth0-devui-accesstoken.png new file mode 100644 index 0000000000000..0175ad084e5b2 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-accesstoken.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-dashboard-with-name.png b/docs/src/main/asciidoc/images/auth0-devui-dashboard-with-name.png new file mode 100644 index 0000000000000..9e67eeeb99d57 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-dashboard-with-name.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-dashboard-without-name.png b/docs/src/main/asciidoc/images/auth0-devui-dashboard-without-name.png new file mode 100644 index 0000000000000..d90b229ef1ce5 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-dashboard-without-name.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-login-to-spa.png b/docs/src/main/asciidoc/images/auth0-devui-login-to-spa.png new file mode 100644 index 0000000000000..56909d6d7b495 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-login-to-spa.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-test-accesstoken-200.png b/docs/src/main/asciidoc/images/auth0-devui-test-accesstoken-200.png new file mode 100644 index 0000000000000..d05bdfb3058d4 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-test-accesstoken-200.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-test-accesstoken-401.png b/docs/src/main/asciidoc/images/auth0-devui-test-accesstoken-401.png new file mode 100644 index 0000000000000..f0373728846b4 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-test-accesstoken-401.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui-testservice-swagger.png b/docs/src/main/asciidoc/images/auth0-devui-testservice-swagger.png new file mode 100644 index 0000000000000..abde99769fa6a Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-testservice-swagger.png differ diff --git a/docs/src/main/asciidoc/images/auth0-devui.png b/docs/src/main/asciidoc/images/auth0-devui.png new file mode 100644 index 0000000000000..115ce9835719c Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui.png differ diff --git a/docs/src/main/asciidoc/images/auth0-idtoken-with-name.png b/docs/src/main/asciidoc/images/auth0-idtoken-with-name.png new file mode 100644 index 0000000000000..913e627f78183 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-idtoken-with-name.png differ diff --git a/docs/src/main/asciidoc/images/auth0-idtoken-without-name.png b/docs/src/main/asciidoc/images/auth0-idtoken-without-name.png new file mode 100644 index 0000000000000..5466ff81f2011 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-idtoken-without-name.png differ diff --git a/docs/src/main/asciidoc/images/auth0-login-flow.png b/docs/src/main/asciidoc/images/auth0-login-flow.png new file mode 100644 index 0000000000000..636841f836311 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-login-flow.png differ diff --git a/docs/src/main/asciidoc/images/auth0-login.png b/docs/src/main/asciidoc/images/auth0-login.png new file mode 100644 index 0000000000000..b0b438885c697 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-login.png differ diff --git a/docs/src/main/asciidoc/images/auth0-password-grant.png b/docs/src/main/asciidoc/images/auth0-password-grant.png new file mode 100644 index 0000000000000..75e4d9d1f61b3 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-password-grant.png differ diff --git a/docs/src/main/asciidoc/images/auth0-test-failure-403.png b/docs/src/main/asciidoc/images/auth0-test-failure-403.png new file mode 100644 index 0000000000000..a7b100a73e413 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-test-failure-403.png differ diff --git a/docs/src/main/asciidoc/images/auth0-test-success.png b/docs/src/main/asciidoc/images/auth0-test-success.png new file mode 100644 index 0000000000000..a05840ecf1c2f Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-test-success.png differ diff --git a/docs/src/main/asciidoc/images/auth0-well-known-config.png b/docs/src/main/asciidoc/images/auth0-well-known-config.png new file mode 100644 index 0000000000000..eda0956df7398 Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-well-known-config.png differ diff --git a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc new file mode 100644 index 0000000000000..4aa67cc3d8072 --- /dev/null +++ b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc @@ -0,0 +1,1092 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="security-oidc-auth0-tutorial"] += Protect Quarkus web application by using an Auth0 OpenID Connect provider +include::_attributes.adoc[] +:diataxis-type: tutorial +:categories: security,web +:keywords: oidc,sso,auth0 + +xref:security-architecture.adoc[Quarkus Security] provides comprehensive OpenId Connect (OIDC) and OAuth2 support with its `quarkus-oidc` extension, supporting both xref:security-oidc-code-flow-authentication.adoc[Authorization code flow] and xref:security-oidc-bearer-token-authentication.adoc[Bearer token] authentication mechanisms. + +With Quarkus, you can easily configure OIDC providers such as link:https://www.keycloak.org/documentation[Keycloak], link:https://developer.okta.com/[Okta], link:https://auth0.com/docs/[Auth0], and other xref:security-openid-connect-providers.adoc[well-known social OIDC and OAuth2 providers]. + +Learn how to use the Quarkus OpenID Connect extension (`quarkus-oidc`) together with the https://auth0.com/docs/[Auth0] OIDC provider to protect your API endpoints. + +== Prerequisites + +Please review the following documentation before you begin: + +* link:https://auth0.com/docs/[Auth0 docs site] +* xref:security-oidc-code-flow-authentication.adoc[Quarkus OpenID Connect Authorization code flow mechanism for protecting web applications] +* xref:security-oidc-bearer-token-authentication.adoc[Quarkus OpenID Connect (OIDC) Bearer token authentication] + +== Create an Auth0 application + +Go to the Auth0 dashboard and create a regular web application. +For example, create an Auth0 application called `QuarkusAuth0`. + +image::auth0-create-application.png[Create Auth0 application] + +.Result +Your Auth0 application gets created with a client ID, secret, and HTTPS-based domain. +Make a note of these properties because you will need them to complete the Quarkus configuration in the next step. + +image::auth0-created-application.png[Created Auth0 application] + +Next, while still in the Auth0 dashboard, add some users to your application. + +image::auth0-add-user.png[Add Auth0 application users] + +Now that you have successfully created and configured your Auth0 application, you are ready to start creating and configuring a Quarkus endpoint. +In the steps that follow, you will continue to configure and update the Auth0 application as well. + +== Create a Quarkus application + +Use the following Maven command to create a Quarkus RESTEasy Reactive application that can be secured with the Quarkus OIDC extension. + +:create-app-artifact-id: quarkus-auth0 +:create-app-extensions: resteasy-reactive,oidc +include::{includes}/devtools/create-app.adoc[] + +Create the application workspace and import it into your favorite IDE. +Let's add a Jakarta REST endpoint that can only be accessed by authenticated users: + +[source,java] +---- +package org.acme; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @Inject + @IdToken <1> + JsonWebToken idToken; + + @GET + @Authenticated <2> + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello, " + idToken.getName(); + } +} +---- +<1> The injected `JsonWebToken` (JWT) bean has an `@IdToken` qualifier, which means it represents not an access token but OIDC `IdToken`. +`IdToken` provides information in the form of claims about the current user authenticated during the OIDC authorization code flow and you can use `JsonWebToken` API to access these claims. +<2> The `io.quarkus.security.Authenticated` annotation is added to the `hello()` method, which means that only authenticated users can access it. + +[NOTE] +==== +The access token acquired during the authorization code flow, alongside the ID token, is not used directly by the endpoint but is used only to access downstream services on behalf of the currently authenticated user. +More to come on the topic of "access tokens", later in this tutorial. +==== + +Configure OIDC in the Quarkus `application.properties` file by using the properties from the Auth0 application that you created earlier. + +[source,configuration] +---- +# Make sure the application domain is prefixed with 'https://' +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=web-app +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} +---- + +In completing this step, you have just configured Quarkus to use the domain, client ID, and secret of yourAuth0 application. +Setting the property `quarkus.oidc.application-type=web-app` instructs Quarkus to use the OIDC authorization code flow, but there are also other methods, which are discussed later on in the tutorial. + +The endpoint address will be \http://localhost:8080/hello, which must also be registered as an allowed callback URL in your Auth0 application. + +image::auth0-allowed-callback.png[Auth0 allowed callback URL] + +After completing this step, when you access the Quarkus \http://localhost:8080/hello endpoint from a browser, Auth0 redirects you back to the same address after the authentication is completed. + +[NOTE] +==== +By default, Quarkus automatically uses the current request path as the callback path. +But you can override the default behavior and configure a specific callback path by setting the Quarkus `quarkus.oidc.authentication.redirect-path` property. + +In production, your application will most likely have a larger URL space, with multiple endpoint addresses available. +In such cases, you can set a dedicated callback (redirect) path and register this URL in the provider's dashboard, as outlined in the following configuration example: + +`quarkus.oidc.authentication.redirect-path=/authenticated-welcome` + +In the example scenario, Quarkus calls `/authenticated-welcome` after accepting a redirect from Auth0, completing the authorization code flow, and creating the session cookie. +Successfully authenticated users are also allowed to access other parts of the secured application space, without needing to authenticate again. For example, the endpoint callback method can use a JAX-RS API to redirect users to other parts of the secured application where a session cookie will be verified. +==== + +Now you are ready to start testing the endpoint. + +== Test the Quarkus endpoint + +Start Quarkus in dev mode: + +[source,bash] +---- +$ mvn quarkus:dev +---- + +[NOTE] +==== +This is the only time during this tutorial when you are expected to manually start Quarkus in dev mode. +The configuration and code update steps in the remaining sections of this tutorial are automatically observed and processed by Quarkus without you needing to restart the application manually. +==== + +Open the browser and access the following URL: +`http://localhost:8080/hello` +You will be redirected to Auth0 and prompted to log in: + +image::auth0-login.png[Auth0 Login] + +and authorize the `QuarkusAuth0` application to access your account: + +image::auth0-authorize.png[Auth0 Authorize] + +Finally, you will be redirected back to the Quarkus endpoint which will return the following response: +`Hello, auth0|60e5a305e8da5a006aef5471` + +[TIP] +==== +Notice that the current username does not get returned. +To learn more about why this behavior occurs, you can use OIDC Dev UI as explained in the xref:security-openid-connect-dev-services.adoc#dev-ui-all-oidc-providers[Dev UI for all OpenID Connect Providers] section of the "Dev Services and UI for OpenID Connect (OIDC)" guide and the following section. +==== + +== Looking at Auth0 tokens in the OIDC Dev UI + +Quarkus provides a great xref:dev-ui-v2.adoc[Dev UI] experience. +Specifically, Quarkus offers built-in support for developing and testing OIDC endpoints with a Keycloak container. +xref:security-openid-connect-dev-services.adoc#dev-services-for-keycloak[DevService for Keycloak] is automatically started and used if the address of the OIDC provider is not specified for the Quarkus `quarkus.oidc.auth-server-url` configuration property. + +You can continue using the Quarkus OIDC Dev UI when the provider is already configured. +Use the following instructions to update your configuration: + +First, change your Quarkus application type from `web-app` to `hybrid`, as follows: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid <1> +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} +---- +<1> Application type is changed to `hybrid` because OIDC Dev UI currently supports `SPA` (single-page application) mode only. +OIDC Dev UI single-page application, using its own Java Script, authenticates users to the OIDC provider and uses the access token as a Bearer token to access the Quarkus endpoint as a service. + +Typically, Quarkus must be configured with `quarkus.oidc.application-type=service` to support `Bearer` token authentication, but it also supports a `hybrid` application type, which means it can support both the authorization code and bearer token flows at the same time. + +You also need to configure the Auth0 application to allow the callbacks to the OIDC Dev UI. +Use the following URL format: + +`http://localhost:8080/q/dev-ui/io.quarkus.quarkus-oidc/${provider-name}-provider` + +* Where in this example, the `${provider-name}` is `auth0` + +image::auth0-allowed-callbacks.png[Auth0 Allowed Callbacks] + +Now you are ready to use OIDC Dev UI with Auth0. + +Open `http://localhost:8080/q/dev/` in a browser session. An OpenId Connect card that links to an Auth0 provider SPA displays, as follows: + +image::auth0-devui.png[Auth0 DevUI] + +Click *Auth0 provider* followed by *Login into Single Page Application*: + +image::auth0-devui-login-to-spa.png[Auth0 DevUI Login to SPA] + +You will be redirected to Auth0 to log in. +You will then be redirected to the OIDC Dev UI dashboard, as follows: + +image::auth0-devui-dashboard-without-name.png[Auth0 DevUI Dashboard Without Name] + +Here you can look at both ID and access tokens in the encoded and decoded formats, copy them to the clipboard or use them to test the service endpoint. We will test the endpoint later but for now let's check the ID token: + +image::auth0-idtoken-without-name.png[Auth0 IdToken without name] + +As you can see it does not have any claim representing a user name but if you check its `sub` (subject) claim you will see its value matches what you got in the response when you accessed the Quarkus endpoint directly from the browser, `auth0|60e5a305e8da5a006aef5471`. + +Fix it by configuring Quarkus to request a standard OIDC `profile` scope during the authentication process which should result in the ID token including more information: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} + +quarkus.oidc.authentication.scopes=profile <1> +---- +<1> Request `profile` scope in addition to the default `openid` scope. + +Go back to `http://localhost:8080/q/dev/`, repeat the process of logging in to `Auth0` and check the ID token again, now you should see the ID token containing the `name` claim: + +image::auth0-idtoken-with-name.png[Auth0 IdToken with name] + +You should get the name when you access the Quarkus endpoint directly from the browser. Clear the browser cookie cache, access `http://localhost:8080/hello` and yet again, you get `Hello, auth0|60e5a305e8da5a006aef5471` returned. Hmm, what is wrong ? + +The answer lies with the specifics of the `org.eclipse.microprofile.jwt.JsonWebToken#getName()` implementation, which, according to the https://github.com/eclipse/microprofile-jwt-auth[MicroProfile MP JWT RBAC specification], checks an MP JWT specific `upn` claim, trying `preferred_username` next and finally `sub` which explains why you get the `Hello, auth0|60e5a305e8da5a006aef5471` answer even with the ID token containing the `name` claim. We can fix it easily by changing the endpoint `hello()` method's implementation to return a specific claim value: + +[source,java] +---- +package org.acme; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @GET + @Authenticated + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello, " + idToken.getClaim("name"); + } +} +---- + +Now clear the browser cache again, access `http://localhost:8080/hello` and finally the user name is returned. + +== Logout support + +Now that you have the users signing in to Quarkus with the help from Auth0, you will likely would like to support a user initiated logout. Quarkus supports https://quarkus.io/guides/security-oidc-code-flow-authentication#logout-and-expiration[RP-initiated and other standard OIDC logout mechanisms, as well as the local session logout]. + +Currently, Auth0 does not support the standard OIDC RP-initiated logout and does not provide an end session endpoint URL in its discoverable metadata, but it provides its own logout mechanism which works nearly exactly the same as the standard one. + +It is easy to support it with Quarkus OIDC. You must configure an Auth0 end session endpoint URL and have Quarkus include both the `client-id` query parameter and the post logout URL as the `returnTo` query parameter in the RP-initated logout redirect request to Auth0: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} +quarkus.oidc.authentication.scopes=openid,profile + +quarkus.oidc.end-session-path=v2/logout <1> +quarkus.oidc.logout.post-logout-uri-param=returnTo <2> +quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} <3> +quarkus.oidc.logout.path=/logout <4> +quarkus.oidc.logout.post-logout-path=/hello/post-logout <5> + +quarkus.http.auth.permission.authenticated.paths=/logout +quarkus.http.auth.permission.authenticated.policy=authenticated <6> +---- +<1> Auth0 does not include the end sessiion URL in its metadata, so complement it with manually configuring the Auth0 end session endpoint URL. +<2> Auth0 will not recognize a standard `post_logout_redirect_uri` query parameter and expects a parameter `returnTo' instead. +<3> Auth0 expects `client-id` in the logout request. +<4> Authenticated requests to `/logout` path will be treated as RP-inititated logout requests. +<5> This is a public resource to where the logged out user should be returned to. +<6> Make sure the `/logout` path is protected. + +Here we have customized the Auth0 end session endpoint URL and indicated to Quarkus that an `http://localhost:8080/logout` request must trigger a logout of the currently authenticated user. An interesting thing about the `/logout` path is that it is `virtual`, it is not supported by any method in the JAX-RS endpoint, so for Quarkus OIDC to be able to react to `/logout` requests we attach an `authenticated` https://quarkus.io/guides/security-authorize-web-endpoints-reference#authorization-using-configuration[HTTP security policy] to this path directly in the configuration. + +We also have configured Quarkus to return the logged out user to the public `/hello/post-logout` resource, with this path included in the logout request as the Auth0 specific `returnTo` query parameter. Finally, the Quarkus application's `client-id` is included in the logout URL as well. + +Update the endpoint to accept the post logout redirects: + +[source,java] +---- +package org.acme; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @GET + @Authenticated + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello, " + idToken.getClaim("name"); + } + + @GET + @Path("post-logout") + @Produces(MediaType.TEXT_PLAIN) + public String postLogout() { + return "You were logged out"; + } +} +---- + +Note the addition of the public `/hello/post-logout` resource method. + +Before we test the logout, make sure the `Auth0` application is configured to allow this post logout redirect back to Quarkus after the user has been logged out: + +image::auth0-allowed-logout.png[Auth0 Allowed Logout] + +Now, clear the browser cookie cache, access `http://localhost:8080/hello`, login to Quarkus with Auth0, get the user name returned, and go to `http://localhost:8080/logout`. You'll see the `You were logged out` message displayed in the browser. + +Next, go to the Dev UI, `http://localhost:8080/q/dev/`, login to Auth0 from the Dev UI SPA and notice you can now logout from the OIDC Dev UI too, see the symbol representing the logout next to the `Logged in as Sergey Beryozkin` text: + +image::auth0-devui-dashboard-with-name.png[Auth0 Dashboard with name and Logout] + +For the logout to work from OIDC DevUI, the Auth0 application's list of allowed logout callbacks have to be updated to include the OIDC DevUI endpoint: + +image::auth0-allowed-logouts.png[Auth0 Allowed Logouts] + +Now logout directly from OIDC Dev UI and login as a new user - add more users to the registered Auth0 application if required. + +[[role-based-access-control]] +== Role Based Access Control + +We have confirmed that the Quarkus endpoint can be accessed by users who have authenticated with the help of `Auth0`. + +The next step is to introduce Role Based Access Control (RBAC) to have users in a specific role only, such as `admin`, be able to access the endpoint. + +See also the <> section below. + +Auth0 tokens do not include any claims containing roles by default, so, first, you must customize the `Login` flow of the `Auth0` application with a custom action which will add the roles to tokens. Select `Actions/Flows/Login` in the `Auth0` dashboard, choose `Add Action/Build Custom`, name it as `AddRoleClaim`: + +image::auth0-add-role-action.png[Auth0 Add Role Action] + +Add the following action script to it: + +[source,javascript] +---- +exports.onExecutePostLogin = async (event, api) => { + const namespace = 'https://quarkus-security.com'; + if (event.authorization) { + api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles); + api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles); + } +}; +---- + +Note a custom Auth0 claim has to be namespace qualified, so the claim which will contain roles will be named as "https://quarkus-security.com/roles". Have a look at the ID token content we analyzed in the previous sections and you will see how this claim is represented, for example: + +[source,json] +---- +{ + "https://quarkus-security.com/roles": [ + "admin" + ] +} +---- + +The `Auth0` Login Flow diagramm should look like this now: + +image::auth0-login-flow.png[Auth0 Login Flow] + +You must add a role such as `admin` to the users registered in the `Auth0` application. + +Create an `admin` role: + +image::auth0-create-role.png[Auth0 Create Role] + +and add it to the registered user: + +image::auth0-add-role-to-user.png[Auth0 Add Role to User] + +Next, update the Quarkus endpoint to require that only users with the `admin` role can access the endpoint: + +[source,java] +---- +package org.acme; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @GET + @RolesAllowed("admin") + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello, " + idToken.getClaim("name"); + } + + @GET + @Path("post-logout") + @Produces(MediaType.TEXT_PLAIN) + public String postLogout() { + return "You were logged out"; + } +} +---- + +Open `http://localhost:8080/hello`, authenticate to Auth0 and get `403`. The reason you get `403` is because Quarkus OIDC does not know which claim in the `Auth0` tokens represents the roles information, by default a `groups` claim is checked, while Auth0 tokens are now expected to have an "https://quarkus-security.com/roles" claim. + +Fix it by telling Quarkus OIDC which claim must be checked to enforce RBAC: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.authentication.scopes=profile +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} + +quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles" <1> + +# Logout +quarkus.oidc.end-session-path=v2/logout +quarkus.oidc.logout.post-logout-uri-param=returnTo +quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} +quarkus.oidc.logout.path=/logout +quarkus.oidc.logout.post-logout-path=/hello/post-logout +quarkus.http.auth.permission.authenticated.paths=/logout +quarkus.http.auth.permission.authenticated.policy=authenticated +---- +<1> Point to the custom roles claim. The path to the roles claim is in double quotes because the claim is namespace qualified. + +Now, clear the browser cookie cache, access `http://localhost:8080/hello` again, authenticate to Auth0 and get an expected user name. + +[[opaque-access-tokens]] +== Access Quarkus with opaque Auth0 access tokens + +[NOTE] +==== +The main goal of this section is to explain how Quarkus can be tuned to accept `opaque` bearer Auth0 access tokens as opposed to Auth0 JWT access tokens because Auth0 access tokens issued during the authorization code flow are opaque by default and they can only be used to request `UserInfo` in addition to the information about the current user which is already available in ID token. Learning how to verify opaque tokens can be useful because many OIDC and OAuth2 providers will issue opaque access tokens only. + +Please see the <> and <> sections below for more information on how to configure Auth0 and Quarkus to have authorization code access tokens issued in the JWT format and propagated to service endpoints. +==== + +So far we have only tested the Quarkus endpoint using OIDC authorization code flow. In this flow you use the browser to access the Quarkus endpoint, Quarkus itself manages the authorization code flow, a user is redirected to Auth0, logs in, is redirected back to Quarkus, Quarkus completes the flow by exchanging the code for the ID, access, and refresh tokens, and works with the ID token representing the successful user authentication. The access token is not relevant at the moment. As mentioned earlier, in the authorization code flow, Quarkus will only use the access token to access downstream services on behalf of the currently authenticated user. + +Lets imagine though that the Quarkus endpoint we have developed has to accept `Bearer` access tokens too: it may be that the other Quarkus endpoint which is propagating it to this endpoint or it can be SPA which uses the access token to access the Quarkus endpoint. And Quarkus OIDC DevUI SPA which we already used to analyze the ID token fits perfectly for using the access token available to SPA to test the Quarkus endpoint. + +Lets go again to `http://localhost:8080/q/dev`, select the `OpenId Connect` card, login to Auth0, and check the Access token content: + +image::auth0-devui-accesstoken.png[Auth0 DevUI Access Token] + +This access token, as opposed to the ID token we looked at earlier, can not be verified by Quarkus directly. This is because the access token is in `JWE` (encrypted) as opposed to `JWS` (signed) format. You can see from the decoded token headers that it has been encrypted directly with a secret key known to Auth0 only, and therefore its content can not be decrypted by Quarkus. From the Quarkus's perspective this access token is an `opaque` one, Quarkus can not use public Auth0 asymmetric verification keys to verify it. + +To confirm it, enter `/hello` as the `Service Address` in the `Test Service` area and press `With Access Token` and you will get the HTTP `401` status: + +image::auth0-devui-test-accesstoken-401.png[Auth0 Dev UI Test Access token 401] + +For Quarkus be able to accept such access tokens, one of the two options should be available. +The first option is to introspect the opaque token remotely using a provider's introspection endpoint. Token introspection is typically supported at the `OAuth2` level, and since `OIDC` is built on top of `OAuth2`, some OIDC providers such as Keycloak support the token introspection as well. However, Auth0 does not support the token introspection, you can check it by looking at the publicly available Auth0 metadata, add `/.well-known/openid-configuration` to the address of your configured Auth0 provider, and open the resulting URL, `https://dev-3ve0cgn7.us.auth0.com/.well-known/openid-configuration`. You will see that Auth0 does not have an introspection endpoint: + +image::auth0-well-known-config.png[Auth0 Well Known Config] + +Therefore the other option, indirect access token verification, where the access token is used to acquire `UserInfo` from Auth0 can be used to accept and verify opaque Auth0 tokens. This option works because OIDC providers have to verify access tokens before they can issue `UserInfo` and Auth0 has a `UserInfo` endpoint. + +So lets configure Quarkus to request that the access tokens must be verified by using them to acquite `UserInfo`: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.authentication.scopes=profile +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} + +# Point to the custom roles claim +quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles" + +# Logout +quarkus.oidc.end-session-path=v2/logout +quarkus.oidc.logout.post-logout-uri-param=returnTo +quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} +quarkus.oidc.logout.path=/logout +quarkus.oidc.logout.post-logout-path=/hello/post-logout +quarkus.http.auth.permission.authenticated.paths=/logout +quarkus.http.auth.permission.authenticated.policy=authenticated + +quarkus.oidc.token.verify-access-token-with-user-info=true <1> +---- +<1> Verify access tokens indirectly by using them to request `UserInfo`. + +Update the endpoint code to expect `UserInfo` as opposed to `ID token`: + +[source,java] +---- +package org.acme; + +import io.quarkus.oidc.UserInfo; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @Inject + UserInfo userInfo; + + @GET + @RolesAllowed("admin") + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello, " + userInfo.getName(); + } + + @GET + @Path("post-logout") + @Produces(MediaType.TEXT_PLAIN) + public String postLogout() { + return "You were logged out"; + } +} +---- + +This code will now work both for the authorization code and bearer access token flows. + +Let's go to the OIDC Dev UI where we looked at the access token, enter `/hello` as the `Service Address` in the `Test Service` area and press `With Access Token` and you will get `200`: + +image::auth0-devui-test-accesstoken-200.png[Auth0 Dev UI Test Access token] + +To confirm that it really does work, update the test endpoint to allow a `user` role only with `@RolesAllowed("user")`. Try to access the endpoint from OIDC Dev UI again, and you will get the HTTP `403` error. Revert the code back to `@RolesAllowed("admin")` to get the reassuring HTTP `200` status again. + +When verifying the opaque access token indirecly, by using it to request `UserInfo`, Quarkus will use `UserInfo` as the source of the roles information, if any. As it happens, Auth0 includes the custom role claim which was created earlier in the `UserInfo` response as well. + +[NOTE] +==== +As has already been mentioned in the introduction to this section, the main goal of this section is to explain how Quarkus can verify opaque access tokens. In general, propagating access tokens whose only purpose is to allow retrieving `UserInfo` to services should be avoided unless the frontend JAX-RS endpoint or SPA prefers to delegate UserInfo retrieval to the trusted service. + +Please see the following <> and <> sections for a recommended approach of working with Auth0 access tokens. +==== + +[NOTE] +==== +Typically one uses access tokens to access remote services but OIDC DevUI SPA dashboard also offers an option to test with the ID token. This option is only available to emulate the cases where SPA delegates to the endpoint to verify and retrieve some information from the ID token for SPA to use - but ID token will still be sent to the endppont as Bearer token by OIDC DevUI. Prefer testing with the access token in most cases. +==== + +[NOTE] +==== +You can use SwaggerUI or GraphQL from OIDC DevUI for testing the service, instead of manually entering the service path to test. +For example, if you add + +[source,xml] +---- + + io.quarkus + quarkus-smallrye-openapi + +---- + +to your application's pom then you will see a Swagger link in OIDC Dev UI: + +image::auth0-devui-testservice-swagger.png[Auth0 Dev UI Test with Swagger] + +Click on the Swagger link and start testing the service. +==== + +[[token-propagation]] +== Propagate access tokens to microservices + +Now that we have managed to use OIDC authorization code flow and used both ID token and UserInfo to access the user information, the next typical task is to propagate the current Auth0 access token to access the downstream service on behalf of the currently authenticated user. + +In fact, the last code example, showing the injected `UserInfo`, is a concrete example of the access token propagation, in this case, Quarkus propagates the Auth0 access token to the Auth0 `UserInfo` endpoint to acquire `UserInfo`. Quarkus does it without users having to do anything themselves. + +But what about propagating access tokens to some custom services ? It is very easy to achieve in Quarkus, both for the authorization code and bearer token flows. All you need to do is to create a Reactive REST Client interface for calling the service requiring a Bearer token access and annotate it with `@AccessToken` and the access token arriving to the frontend endpoint as the Auth0 Bearer access token or acquired by Quarkus after completing the Auth0 authorization code flow, will be propagated to the target microservice. This is as easy as it can get. + +Please see xref:security-openid-connect-client-reference.adoc#reactive-token-propagation[OIDC token propagation] for more information about the token propagation and the following sections in this tutoal for a concrete example. + +[[jwt-access-tokens]] +=== Access tokens in JWT format + +We have already looked in detail at how Quarkus OIDC can handle <>, but we don't want to propagate Auth0 opaque tokens to micro services which do something useful on behalf on the currently authenticated user, beyond checking its UserInfo. + +A microservice which the frontend Quarkus application will access by propagating authorization code flow access tokens to it is represented in the Auth0 dashboard as an `API`. Lets add it in the `Applications/APIs`: + +image::auth0-api.png[Auth0 API] + +The `https://quarkus-auth0` identifier of the created `QuarkusAuth0API` will serve as this API's `audience`. Providing this audience as a query parameter in the authorization code flow redirect to Auth0 will ensure that Auth0 issues access tokens in the JWT format. + +=== API microservice + +Add the following dependencies to the project to support OIDC token propagation and REST clients: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-rest-client-reactive-jackson + + + io.quarkus + quarkus-oidc-token-propagation-reactive + +---- + +Create `ApiEchoService` service class: + +[source,java] +---- +package org.acme; + +import io.quarkus.security.Authenticated; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/echo") +public class ApiEchoService { + + @POST + @Authenticated + @Produces(MediaType.TEXT_PLAIN) + public String echoUserName(String username) { + return username; + } +} +---- + +And configure it as an OIDC `service` application which will only fetch public verification keys from Auth0. +The configuration for this microservice should only have a single line: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +---- + +which is all what is needed for the OIDC `service` application to fetch Auth0 public verification keys and use them to verify Auth0 access tokens in JWT format. + +[[NOTE]] +==== +In this tutorial you have already configured the OIDC `hybrid` application which can handle both authorization code and bearer token authentication flows. In production you will run microservices as separate servers but for the sake of simplicity `ApiEchoService` will not have to be started as a second server with its own configuration containing `quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com` only, and therefore the current configuration which already has the Auth0 dev tenant address configured will be reused. + +The `hybrid` OIDC application type will ensure that `http://localhost:8080/hello` requests to `GreetingResource` initiate an Authorization code flow while `http://localhost:8080/echo` requests to `ApiEchoService`, initiated by `GreetingResource`, will lead to the authorization code flow tokens being propagated and accepted by `ApiEchoService` as bearer JWT access tokens. +==== + +Next, add a REST client interface representing `ApiEchoService`: + +[source,java] +---- +package org.acme; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.token.propagation.AccessToken; + +@RegisterRestClient +@AccessToken +@Path("/echo") +public interface ApiEchoServiceClient { + + @POST + @Produces(MediaType.TEXT_PLAIN) + String echoUserName(String username); +} +---- + +And update the configuration for the Quarkus frontend application, `GreetingResource`, which has been created earlier, to request that an authorization code flow access token (as opposed to ID token) includes an `aud` (audience) claim targeting `ApiEchoService`, as well as configure the base URL for the `ApiEchoService` REST client: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.authentication.scopes=profile +quarkus.oidc.authentication.extra-params.audience=https://quarkus-auth0 <1> +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} + +# Point to the custom roles claim +quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles" + +# Logout +quarkus.oidc.end-session-path=v2/logout +quarkus.oidc.logout.post-logout-uri-param=returnTo +quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} +quarkus.oidc.logout.path=/logout +quarkus.oidc.logout.post-logout-path=/hello/post-logout +quarkus.http.auth.permission.authenticated.paths=/logout +quarkus.http.auth.permission.authenticated.policy=authenticated + +quarkus.oidc.token.verify-access-token-with-user-info=true + +org.acme.ApiEchoServiceClient/mp-rest/url=http://localhost:${port} <2> + +quarkus.test.native-image-profile=test +%prod.port=8080 +%dev.port=8080 +%test.port=8081 +---- +<1> Pass an extra `audience` query parameter to the Auth0 authorization endpoint during the authorization code flow redirect from Quarkus to Auth0. +It will ensure that the access token is issued in the JWT format and includes an `aud` (audience) claim which will contain `https://quarkus-auth0`. +<2> Point `ApiEchoServiceClient` to the `ApiEchoService` endpoint. HTTP port in the `org.acme.ApiEchoServiceClient/mp-rest/url=http://localhost:${port}` property is parameterized to ensure the correct URL is built in dev, test and prod modes. + +Finally update `GreetingResource` to request that `ApiEchoService` echoes a user name: + +[source,java] +---- +package org.acme; + +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/hello") +public class GreetingResource { + @Inject + @RestClient + ApiEchoServiceClient echoClient; <1> + + @Inject + UserInfo userInfo; + + @GET + @RolesAllowed("admin") + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello, " + echoClient.echoUserName(userInfo.getName()); <2> + } + + @GET + @Path("post-logout") + @Produces(MediaType.TEXT_PLAIN) + public String postLogout() { + return "You were logged out"; + } +} +---- +<1> Inject `ApiEchoServiceClient` REST client +<2> Use `ApiEchoServiceClient` to echo the user name. + +Open a browser, access `http://localhost:8080/hello` and get your name displayed in the browser. + +[[permission-based-access-control]] +=== Permission Based Access Control + +We have discussed in the <> section how to get Quarkus to check a namespace qualified claim containing user roles and use this information to enforce Role Based Access Control. You have configured Auth0 to add the custom roles claim to both ID and access tokens. + +However, Permission Based Access Control is better suited to the case where an access token is propagated by the frontend endpoint to a microservice which will check if a given access token has been authorized for this service to perform a concrete action, as opposed to this token vouching for a user be in a specific role. For example, being in the admin role does not necessarily mean the user is allowed to have a read and write access to some of this microservice's content. + +Let's see how Permission Based Access Control constraints can be applied to `ApiEchoService`. + +Go to the Auth0 dashboard, add an `echo:name` permission to the `QuarkusAuth0API` API: + +image::auth0-api-permissions.png[Auth0 API permissions] + +The `echo:name` permission will be included in the access token as a standard OAuth2 `scope` claim value if this scope will also be requested during the authorization code flow. Update the configuration as follows: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.authentication.scopes=profile,echo:name <1> +quarkus.oidc.authentication.extra-params.audience=https://quarkus-auth0 +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} + +# Point to the custom roles claim +quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles" + +# Logout +quarkus.oidc.end-session-path=v2/logout +quarkus.oidc.logout.post-logout-uri-param=returnTo +quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} +quarkus.oidc.logout.path=/logout +quarkus.oidc.logout.post-logout-path=/hello/post-logout +quarkus.http.auth.permission.authenticated.paths=/logout +quarkus.http.auth.permission.authenticated.policy=authenticated + +quarkus.oidc.token.verify-access-token-with-user-info=true + +org.acme.ApiEchoServiceClient/mp-rest/url=http://localhost:8080 +---- +<1> An extra `echo:name` scope will be requested during the authorization code flow. + +Now update `ApiEchoService` to enforce Permission Based Access Control: + +[source,java] +---- +package org.acme; + +import io.quarkus.security.PermissionsAllowed; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/echo") +public class ApiEchoService { + + @POST + @PermissionsAllowed("echo:name") + @Produces(MediaType.TEXT_PLAIN) + String echoUserName(String username) { + return username; + } +} +---- + +This is all what is needed as Quarkus OIDC automatically associates `scope` claim values as permissions with the current security identity. + +[NOTE] +==== +You can enforce both Role Based and Permission Based Access Controls in Quarkus by combining `@RolesAllowed` and `@PermissionsAllowed` annotations. +==== + +Open a browser, access `http://localhost:8080/hello` and get the name displayed in the browser. + +To confirm the permission is correctly enforced, change it to `echo.name`: `@PermissionsAllowed("echo.name")`. Clear the browser cache, access `http://localhost:8080/hello` again and you will get `403` reported by `ApiEchoService`. Now revert it back to `@PermissionsAllowed("echo:name")`. + +== Integration testing + +You have already used OIDC DevUI SPA to login to Auth0 and test the Quarkus endpoint with the access token, updating the endpoint code along the way. + +However, running tests is also essential, lets see how we can test the endpoint and configuration which you have developed during the course of this tutorial, using xref:continuous-testing.adoc[Quarkus Continuous Testing] feature. + +Start with the following test code : + +[source,java] +---- +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class GreetingResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("Hello, Sergey Beryozkin")); + } + +} +---- + +If you recall, when the application was started in devmode, the following could be seen in the CLI window: + +image::auth0-devmode-started.png[Auth0 DevMode started] + +Press `r` and notice this test failing with `403` which is expected because the test does not send a token to the endpoint: + +image::auth0-test-failure-403.png[Auth0 test failure 403] + +Before fixing the test, let's review the options available for testing Quarkus endpoints secured by OIDC. These options may vary depending on what flow your application supports and how you prefer to test. Endpoints which use OIDC authorization code flow can be tested using xref:security-oidc-code-flow-authentication#integration-testing[one of these options] and endpoints which use Bearer token authentication can be tested using xref:security-oidc-bearer-token-authentication#integration-testing[one of these options]. + +As you can see, testing of the endpoints secured with Auth0 can be done with the help of `Wiremock`, or `@TestSecurity` annotation. Please experiment with writing such tests on your own and reach out if you encounter any problems. + +In this tutorial though, we will use a recently added `OidcTestClient` to support testing endpoints which use live Auth0 development tenants. + +Here is a related fragment of the configuration: + +[source,configuration] +---- +quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com +quarkus.oidc.application-type=hybrid +quarkus.oidc.authentication.scopes=profile +quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly +quarkus.oidc.credentials.secret=${client-secret} +---- + +In production, you will distinguish between prod and test level configuration with `%prod.` and `%test.` qualifiers. Lets assume that the above configuration will indeed be prefixed with `%test.` in your real application, with this configuration also including the `%prod.` qualified Auth0 production tenant configuration. + +Using `OidcTestClient` to test such configuration requires acquiring a token from the Auth0 dev tenant, using either OAuth2 `password` or `client_credentials` grant, we will try a `password` grant. Make sure the application registered in the Auth0 dashboard allows the `password` grant: + +image::auth0-password-grant.png[Auth0 password grant] + +[NOTE] +==== +It is important to clarify that we do not recommend using the deprecated OAuth2 `password` token grant in production. However using it can help testing the endpoint with tokens acquired from the live dev Auth0 tenant. +==== + +`OidcTestClient` should be used to test applications accepting bearer tokens which will work for the endpoint developed in this tutorial as it supports both authorization code flow and bearer token authentication. You would need to use OIDC WireMock or `HtmlUnit` directly against the Auth0 dev tenant if only authorization code flow was supported - in the latter case `HtmlUnit` test code would have to be aligned with how Auth0 challenges users to enter their credentials - please copy and paste an xref:security-oidc-code-flow-authentication#integration-testing-wiremock[HtmlUnit test fragment] from the documentation and experiment if you would like. + +In meantime we will now proceed with fixing the currently failing test using `OidcTestClient`. + +First you must add the following dependency: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-test-oidc-server + test + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +testImplementation("io.quarkus:quarkus-test-oidc-server") +---- + +which provides a utility class `io.quarkus.test.oidc.client.OidcTestClient` which can be used in tests for acquiring access tokens (This dependency also offers an OIDC WireMock support - please review the documentation how to use it for testing if you would like). + +Now update the test code like this: + +[source,java] +---- +package org.acme; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.oidc.client.OidcTestClient; + +@QuarkusTest +public class GreetingResourceTest { + + static OidcTestClient oidcTestClient = new OidcTestClient(); + + @AfterAll + public static void close() { + client.close(); + } + + @Test + public void testHelloEndpoint() { + given() + .auth().oauth2(getAccessToken(`sberyozkin@gmail.com`, "userpassword")) + .when().get("/hello") + .then() + .statusCode(200) + .body(is("Hello, Sergey Beryozkin")); + } + + private String getAccessToken(String name, String secret) { + return oidcTestClient.getAccessToken(name, secret, <1> + Map.of("audience", "https://quarkus-auth0", + "scope", "openid profile")); + } +} +---- +<1> `OidcTestClient` is used to acquire an access token, using one of the registered user's name and password, as well as the `audience` and `scope` parameters. + +`OidcTestClient` will itself find out the `Auth0` token endpoint address, client id and secret. + +Press `r` again and have the test passing: + +image::auth0-test-success.png[Auth0 test success] + +By the way, if you would like you can run the tests in Continuous mode directly from DevUI: + +image::auth0-continuous-testing.png[Auth0 Continuous testing] + +[[production-mode]] +== Production mode + +You have developed and tested the Quarkus endpoint secured with Auth0 in the development mode. +The next step is to run your application in the production mode. +Choose between JVM and native modes. + +=== Run the Application in JVM mode + +Compile the application: + +include::{includes}/devtools/build.adoc[] + +Run the application: + +[source,bash] +---- +java -jar target/quarkus-app/quarkus-run.jar +---- + +Open a browser, access `http://localhost:8080/hello` and get the name displayed in the browser. + +=== Run the application in native mode + +You can compile this same demo into native mode without needing any modifications. +This implies that you no longer need to install a JVM on your production environment. +The runtime technology is included in the produced binary and optimized to run with minimal resources required. + +Compilation takes a bit longer, so this step is disabled by default. + +Build your application again by enabling the `native` profile: + +include::{includes}/devtools/build-native.adoc[] + +Next run the following binary directly: + +[source,bash] +---- +./target/quarkus-auth0-1.0.0-SNAPSHOT-runner +---- + +Open a browser, access `http://localhost:8080/hello` and get the name displayed in the browser. + +== Troubleshooting + +The steps described in this tutorial should work exactly as the tutorial describes. You may have to clear the browser cookies when accessing the updated Quarkus endpoint if you have already completed the authentication. You might need to restart the Quarkus application manually in devmode but it is not expected. Please get in touch with the Quarkus team if you need help completing this tutorial. + +== Summary + +This tutorial demonstrated how Quarkus endpoints can be secured with the `quarkus-oidc` extension and Auth0 using authorization code and bearer token authentication flows, whereby both flows are supported by the same endpoint code. +Without writing a single line of code, you have added support for the custom Auth0 logout flow and enabled role-based access control with a custom Auth0 namespace qualified claim. +Token propagation from the frontend endpoint to the microservice endpoint has been achieved by adding the `@AccessToken` annotation to the microservice REST client. +Microservice endpoint activated the ermission-based access control with the `@PermissionsAllowed` annotation. +You used Quarkus dev mode to update the code and configuration without restarting the endpoint, and you also used the OIDC Dev UI to visualize and test Auth0 tokens. +You used the continuous testing feature of Quarkus to complement OIDC Dev UI tests with integration tests against the live Auth0 development tenant. + +Enjoy! + +== References +* xref:security-overview.adoc[Quarkus Security overview] +* xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] +* xref:security-openid-connect-providers.adoc[Configuring well-known OpenID Connect providers] diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc index 2b0e92d266e69..9ae22c5fe98e3 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc @@ -271,6 +271,7 @@ After you have completed this tutorial, explore xref:security-oidc-bearer-token- * xref:security-authentication-mechanisms.adoc#oidc-jwt-oauth2-comparison[Choosing between OpenID Connect, SmallRye JWT, and OAuth2 authentication mechanisms] * xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] * https://www.keycloak.org/documentation.html[Keycloak Documentation] +* xref:security-oidc-auth0-tutorial.adoc[Protect Quarkus web application by using Auth0 OpenID Connect provider] * https://openid.net/connect/[OpenID Connect] * https://tools.ietf.org/html/rfc7519[JSON Web Token]