= Securing Quarkus APIs
:experimental:

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<JsonString> 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]
... <more claims>
----

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.

