## Securing Quarkus APIs Bearer Token Authorization is the process of authorizing HTTP requests based on the existence and validity of a bearer token representing a subject and her access context, where the token provides valuable information to determine the subject of the call as well whether or not a HTTP resource can be accessed. This is commonly used in OAuth-based identity and access management systems like https://keycloak.org[Keycloak], a popular open source project. In this exercise we'll show you how to use https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.pdf[Microprofile JSON Web Token (JWT) RBAC], https://keyloak.org[Keycloak] and https://en.wikipedia.org/wiki/OAuth[OAuth] to secure your Quarkus applications. ## Add JWT to Quarkus Like other exercises, we'll need another extension to enable the use of MicroProfile JWT. Install it with: [source,sh,role="copypaste"] ---- mvn quarkus:add-extension -Dextensions="jwt" ---- This will add the necessary entries in your `pom.xml` to bring in JWT support. ## Configure Quarkus for MicroProfile JWT Some configuration of the extension is required. Add this to your `application.properties`: [source,properties,role="copypaste"] ---- mp.jwt.verify.publickey.location={{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/certs # <1> mp.jwt.verify.issuer={{KEYCLOAK_URL}}/auth/realms/quarkus # <2> quarkus.smallrye-jwt.auth-mechanism=MP-JWT # <3> quarkus.smallrye-jwt.realmName=quarkus quarkus.smallrye-jwt.enabled=true ---- <1> Sets public key location for JWT authentication. Keycloak exports this for you at the URL. <2> Issuer URL. This must match the incoming JWT `iss` _claims_ or else authentication fails. <3> Sets authentication mechanism name to `MP-JWT`, the MicroProfile JWT RBAC specification standard name for the token based authentication mechanism. # Create protected endpoints We'll create 2 JWT-protected endpoints. Create a new class `JWTResource` in the `org.acme.people.rest` package with the following code: [source,java,role="copypaste"] ---- package org.acme.people.rest; import java.security.Principal; import java.util.Optional; import javax.annotation.security.RolesAllowed; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.json.JsonString; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") @RequestScoped // <1> public class JWTResource { @Inject JsonWebToken jwt; // <2> @Inject @Claim(standard = Claims.iss) Optional issuer; // <3> @GET @Path("/me") @RolesAllowed("user") @Produces(MediaType.TEXT_PLAIN) public String me(@Context SecurityContext ctx) { // <4> Principal caller = ctx.getUserPrincipal(); String name = caller == null ? "anonymous" : caller.getName(); boolean hasJWT = jwt != null; return String.format("hello %s, isSecure: %s, authScheme: %s, hasJWT: %s\n", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT); } @GET @Path("/me/jwt") @RolesAllowed("admin") @Produces(MediaType.TEXT_PLAIN) public String meJwt(@Context SecurityContext ctx) { // <4> Principal caller = ctx.getUserPrincipal(); String name = caller == null ? "anonymous" : caller.getName(); boolean hasJWT = jwt != null; final StringBuilder helloReply = new StringBuilder(String.format("hello %s, isSecure: %s, authScheme: %s, hasJWT: %s\n", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT)); if (hasJWT && (jwt.getClaimNames() != null)) { helloReply.append("Injected issuer: [" + issuer.get() + "]\n"); <5> jwt.getClaimNames().forEach(n -> { helloReply.append("\nClaim Name: [" + n + "] Claim Value: [" + jwt.getClaim(n) + "]"); }); } return helloReply.toString(); } } ---- <1> Adds a `@RequestScoped` as Quarkus uses a default scoping of `ApplicationScoped` and this will produce undesirable behavior since JWT claims are naturally request scoped. <2> `JsonWebToken` provides access to the claims associated with the incoming authenticated JWT token. <3> When using JWT Authentication, claims encoded in tokens can be `@Inject` ed into your class for convenient access. <4> The `/me` and `/me/jwt` endpoints demonstrate how to access the security context for Quarkus apps secured with JWT. Here we are using a `@RolesAllowed` annotation to make sure that only users granted a specific role can access the endpoint. <5> Use of injected JWT Claim to print the all the claims ## Rebuild and redeploy app First, re-build the app using the command palette and selecting **Create Executable JAR**. Once that's done, run the following command to re-deploy: [source,sh,role="copypaste"] ---- oc start-build people --from-file target/*-runner.jar --follow ---- ## Confirm deployment Run and wait for the app to complete its rollout: [source,sh,role="copypaste"] ---- oc rollout status -w dc/people ---- ## Test endpoints The first thing to do to test any endpoint is obtain an access token from your authentication server in order to access the application resources. We've pre-created a few users in Keycloak for you to use: * `alice` is an ordinary user (will have the `user` role) whose password is `alice` * `admin` is an Administrator (has the `admin` and `user` role) and their password is `admin` * `jdoe` is an ordinary user (has the `user` role) but has also been granted access to `confidential` endpoints in Keycloak, and their password is `jdoe` Try to access the endpoint as an anonymous unauthenticated user: [source,sh,role="copypaste"] ---- curl -i http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/me ---- It should fail with: [source,none] ---- HTTP/1.1 401 Unauthorized Connection: keep-alive Content-Type: text/html;charset=UTF-8 Content-Length: 14 Date: Tue, 16 Jul 2019 00:40:07 GMT Not authorized ---- Let's try with an authenticated user next. ### Test Alice Get a token for user `alice` with this command: [source,sh,role="copypaste"] ---- export ALICE_TOKEN=$(\ curl -X POST {{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \ --user backend-service:secret \ -H 'content-type: application/x-www-form-urlencoded' \ -d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \ ) echo $ALICE_TOKEN ---- This issues a `curl` command to Keycloak (using `backend-service` credentials which is a special user that is allowed acess to the Keycloak REST API), and fetches a token for Alice using their credentials. Try out the JWT-secured API as Alice: [source,sh,role="copypaste"] ---- curl -i http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/me \ -H "Authorization: Bearer $ALICE_TOKEN" ---- You should see: [source,none] ---- HTTP/1.1 200 OK Connection: keep-alive Content-Type: text/plain;charset=UTF-8 Content-Length: 63 Date: Tue, 16 Jul 2019 00:40:44 GMT hello alice, isSecure: false, authScheme: MP-JWT, hasJWT: true ---- Now try to access the `/me/admin` endpoint as `alice`: [source,sh,role="copypaste"] ---- curl -i http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/me/admin \ -H "Authorization: Bearer $ALICE_TOKEN" ---- You'll get: [source,none] ---- HTTP/1.1 403 Forbidden Connection: keep-alive Content-Type: text/html;charset=UTF-8 Content-Length: 34 Date: Tue, 16 Jul 2019 00:41:37 GMT Access forbidden: role not allowed ---- Alice is not an admin. Let's try with admin! ### Test Admin Obtain an Admin token: [source,sh,role="copypaste"] ---- export ADMIN_TOKEN=$(\ curl -X POST {{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \ --user backend-service:secret \ -H 'content-type: application/x-www-form-urlencoded' \ -d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \ ) ---- And try again with your new token: [source,sh,role="copypaste"] ---- curl -i http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/me/admin \ -H "Authorization: Bearer $ADMIN_TOKEN" ---- You should see: [source,none] ---- HTTP/1.1 200 OK Connection: keep-alive Content-Type: text/plain;charset=UTF-8 Content-Length: 2272 Date: Tue, 16 Jul 2019 00:14:54 GMT hello admin, isSecure: false, authScheme: MP-JWT, hasJWT: true Injected issuer: ["{{KEYCLOAK_URL}}/auth/realms/quarkus"] Claim Name: [sub] Claim Value: [af134cab-f41c-4675-b141-205f975db679] Claim Name: [groups] Claim Value: [[admin, user]] Claim Name: [typ] Claim Value: ["Bearer"] Claim Name: [preferred_username] Claim Value: [admin] ... ---- Success! We dump all of the claims from the JWT token for inspection. ## Using Keycloak Authentication Keycloak provides similar features as other OAuth/Open ID Connect servers, but can also do implicit authentication without you as a developer needing to explicitly declare protection on endpoints. First, you'll need to enable the Keycloak extension: [source,sh,role="copypaste"] ---- mvn quarkus:add-extension -Dextensions="keycloak" ---- ### Configuring Keycloak Next, add these to your `application.properties` for Keycloak: [source,none,role="copypaste"] ---- quarkus.keycloak.realm=quarkus quarkus.keycloak.auth-server-url={{ KEYCLOAK_URL }}/auth quarkus.keycloak.resource=backend-service quarkus.keycloak.bearer-only=true quarkus.keycloak.credentials.secret=secret quarkus.keycloak.policy-enforcer.enable=true quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE ---- This configures the extension with the necessary configuration ( https://www.keycloak.org/docs/latest/securing_apps/index.html#_java_adapter_config[read more] about what these do). ### Create Keycloak endpoints Create a new class called `KeycloakResource` in the `org.acme.people.rest` package with the following code: [source,java,role=copypaste] ---- package org.acme.people.rest; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.keycloak.KeycloakSecurityContext; @Path("/secured") public class KeycloakResource { @Inject KeycloakSecurityContext keycloakSecurityContext; // <1> @GET @Path("/confidential") // <2> @Produces(MediaType.TEXT_PLAIN) public String confidential() { return ("confidential access for: " + keycloakSecurityContext.getToken().getPreferredUsername() + " from issuer:" + keycloakSecurityContext.getToken().getIssuer()); } } ---- <1> The `KeycloakSecurityContext` is an object produced by the Keycloak extension that you can use to obtain information from tokens sent to your application. <2> Note that we do not use any `@RolesAllowed` or any other instrumentation on the endpoint. It looks like an ordinary endpoint. [NOTE] ==== There are other APIs you can use if you try to auto-complete the method name using Che, e.g. `getBirthDate()` or `getPicture()`. Place the cursor just after `keycloakSecurityContext.getToken().get` and press kbd:[Ctrl+Space] to see them: image::secapis.png[apis, 800] ==== ## Rebuild and redeploy app First, re-build the app using the command palette and selecting **Create Executable JAR**. Once that's done, run the following command to re-deploy: [source,sh,role="copypaste"] ---- oc start-build people --from-file target/*-runner.jar --follow ---- ## Confirm deployment Run and wait for the app to complete its rollout: [source,sh,role="copypaste"] ---- oc rollout status -w dc/people ---- ### Test confidential The `/secured/confidential` endpoint is protected with a policy defined in the Keycloak Server. The policy only grants access to the resource if the user is granted with a `confidential` role. The difference here is that the application is delegating the access decision to Keycloak, so no explicit source code instrumentation is required. First make sure even `admin` can't access the endpoint: [source,sh,role="copypaste"] ---- curl -v -X GET \ http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/confidential \ -H "Authorization: Bearer $ADMIN_TOKEN" ---- You should see in the returned HTTP headers: [source,none] ---- HTTP/1.1 403 Forbidden Connection: keep-alive Content-Length: 0 Date: Tue, 16 Jul 2019 00:59:56 GMT ---- Failed as expected! To access the confidential endpoint, you should obtain an access token for user `jdoe`: [source,sh,role="copypaste"] ---- export JDOE_TOKEN=$(\ curl -X POST {{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \ --user backend-service:secret \ -H 'content-type: application/x-www-form-urlencoded' \ -d 'username=jdoe&password=jdoe&grant_type=password' | jq --raw-output '.access_token' \ ) ---- And access the confidential endpoint with your new token: [source,sh,role="copypaste"] ---- curl -X GET \ http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/confidential \ -H "Authorization: Bearer $JDOE_TOKEN" ---- You should see: [source,none] ---- HTTP/1.1 200 OK Connection: keep-alive Content-Type: text/plain;charset=UTF-8 Content-Length: 142 Date: Tue, 16 Jul 2019 00:50:49 GMT confidential access for: jdoe from issuer:{{KEYCLOAK_URL}}/auth/realms/quarkus ---- Success! Even though our code did not explicitly protect the `/secured/confidential` endpoint, we can protect arbitrary URLs in Quarkus apps when using Keycloak. ## Congratulations! This exercise demonstrated how your Quarkus application can use MicroProfile JWT in conjunction with Keycloak to protect your JAX-RS applications using JWT claims and bearer token authorization.