Quarkus - Using JWT RBAC

This guide explains how your Quarkus application can utilize MicroProfile JWT (MP JWT) to verify JSON Web Tokens, represent them as MP JWT org.eclipse.microprofile.jwt.JsonWebToken and provide secured access to the Quarkus HTTP endpoints using Bearer Token Authorization and Role-Based Access Control.

Quarkus OpenId Connect extension also supports Bearer Token Authorization and uses smallrye-jwt to represent the bearer tokens as JsonWebToken, please read the Using OpenID Connect to Protect Service Applications guide for more information. OpenId Connect extension has to be used if the Quarkus application needs to authenticate the users using OIDC Authorization Code Flow, please read Using OpenID Connect to Protect Web Applications guide for more information.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can skip right to the completed example.

Clone the Git repository: git clone [https://github.com/quarkusio/quarkus-quickstarts.git](https://github.com/quarkusio/quarkus-quickstarts.git), or download an archive.

The solution is located in the security-jwt-quickstart directory.

Creating the Maven project

First, we need a new project. Create a new project with the following command:

  1. mvn io.quarkus:quarkus-maven-plugin:1.7.6.Final:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=security-jwt-quickstart \
  4. -DclassName="org.acme.security.jwt.TokenSecuredResource" \
  5. -Dpath="/secured" \
  6. -Dextensions="resteasy-jsonb, jwt"
  7. cd security-jwt-quickstart

This command generates the Maven project with a REST endpoint and imports the smallrye-jwt extension, which includes the MicroProfile JWT RBAC support.

If you already have your Quarkus project configured, you can add the smallrye-jwt extension to your project by running the following command in your project base directory:

  1. ./mvnw quarkus:add-extension -Dextensions="smallrye-jwt"

This will add the following to your pom.xml:

  1. <dependency>
  2. <groupId>io.quarkus</groupId>
  3. <artifactId>quarkus-smallrye-jwt</artifactId>
  4. </dependency>

Examine the JAX-RS resource

Open the src/main/java/org/acme/security/jwt/TokenSecuredResource.java file and see the following content:

Basic REST Endpoint

  1. package org.acme.security.jwt;
  2. import javax.ws.rs.GET;
  3. import javax.ws.rs.Path;
  4. import javax.ws.rs.Produces;
  5. import javax.ws.rs.core.MediaType;
  6. @Path("/secured")
  7. public class TokenSecuredResource {
  8. @GET
  9. @Produces(MediaType.TEXT_PLAIN)
  10. public String hello() {
  11. return "hello";
  12. }
  13. }

This is a basic REST endpoint that does not have any of the SmallRye JWT specific features, so let’s add some.

REST Endpoint V1

  1. package org.acme.security.jwt;
  2. import java.security.Principal;
  3. import javax.annotation.security.PermitAll;
  4. import javax.enterprise.context.RequestScoped;
  5. import javax.inject.Inject;
  6. import javax.ws.rs.GET;
  7. import javax.ws.rs.InternalServerErrorException;
  8. import javax.ws.rs.Path;
  9. import javax.ws.rs.Produces;
  10. import javax.ws.rs.core.Context;
  11. import javax.ws.rs.core.MediaType;
  12. import javax.ws.rs.core.SecurityContext;
  13. import org.eclipse.microprofile.jwt.JsonWebToken;
  14. @Path("/secured")
  15. @RequestScoped (1)
  16. public class TokenSecuredResource {
  17. @Inject
  18. JsonWebToken jwt; (2)
  19. @GET()
  20. @Path("permit-all")
  21. @PermitAll (3)
  22. @Produces(MediaType.TEXT_PLAIN)
  23. public String hello(@Context SecurityContext ctx) {
  24. return getResponseString(ctx); (4)
  25. }
  26. private String getResponseString(SecurityContext ctx) {
  27. String name;
  28. if (ctx.getUserPrincipal() == null) { (5)
  29. name = "anonymous";
  30. } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { (6)
  31. throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
  32. } else {
  33. name = ctx.getUserPrincipal().getName(); (7)
  34. }
  35. return String.format("hello + %s,"
  36. + " isHttps: %s,"
  37. + " authScheme: %s,"
  38. + " hasJWT: %s",
  39. name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); (8)
  40. }
  41. private boolean hasJwt() {
  42. return jwt.getClaimNames() != null;
  43. }
  44. }
1Add a RequestScoped as Quarkus uses a default scoping of ApplicationScoped and this will produce undesirable behavior since JWT claims are naturally request scoped.
2Here we inject the JsonWebToken interface, an extension of the java.security.Principal interface that provides access to the claims associated with the current authenticated token.
3@PermitAll is a JSR 250 common security annotation that indicates that the given endpoint is accessible by any caller, authenticated or not.
4Here we inject the JAX-RS SecurityContext to inspect the security state of the call and use a getResponseString() function to populate a response string.
5Here we check if the call is insecured by checking the request user/caller Principal against null.
6Here we check that the Principal and JsonWebToken have the same name since JsonWebToken does represent the current Principal.
7Here we get the Principal name.
8The reply we build up makes use of the caller name, the isSecure() and getAuthenticationScheme() states of the request SecurityContext, and whether a non-null JsonWebToken was injected.

Run the application

Now we are ready to run our application. Use:

  1. ./mvnw compile quarkus:dev

and you should see output similar to:

quarkus:dev Output

  1. $ ./mvnw compile quarkus:dev
  2. [INFO] Scanning for projects...
  3. [INFO]
  4. [INFO] ----------------------< org.acme:security-jwt-quickstart >-----------------------
  5. [INFO] Building security-jwt-quickstart 1.0-SNAPSHOT
  6. [INFO] --------------------------------[ jar ]---------------------------------
  7. ...
  8. Listening for transport dt_socket at address: 5005
  9. 2020-07-15 16:09:50,883 INFO [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080
  10. 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
  11. 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, resteasy, resteasy-jsonb, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]

Now that the REST endpoint is running, we can access it using a command line tool like curl:

curl command for /secured/permit-all

  1. $ curl http://127.0.0.1:8080/secured/permit-all; echo
  2. hello + anonymous, isHttps: false, authScheme: null, hasJWT: false

We have not provided any JWT in our request, so we would not expect that there is any security state seen by the endpoint, and the response is consistent with that:

  • user name is anonymous

  • isHttps is false as https is not used

  • authScheme is null

  • hasJWT is false

Use Ctrl-C to stop the Quarkus server.

So now let’s actually secure something. Take a look at the new endpoint method helloRolesAllowed in the following:

REST Endpoint V2

  1. package org.acme.security.jwt;
  2. import javax.annotation.security.PermitAll;
  3. import javax.annotation.security.RolesAllowed;
  4. import javax.enterprise.context.RequestScoped;
  5. import javax.inject.Inject;
  6. import javax.ws.rs.GET;
  7. import javax.ws.rs.InternalServerErrorException;
  8. import javax.ws.rs.Path;
  9. import javax.ws.rs.Produces;
  10. import javax.ws.rs.core.Context;
  11. import javax.ws.rs.core.MediaType;
  12. import javax.ws.rs.core.SecurityContext;
  13. import org.eclipse.microprofile.jwt.JsonWebToken;
  14. @Path("/secured")
  15. @RequestScoped
  16. public class TokenSecuredResource {
  17. @Inject
  18. JsonWebToken jwt; (1)
  19. @GET
  20. @Path("permit-all")
  21. @PermitAll
  22. @Produces(MediaType.TEXT_PLAIN)
  23. public String hello(@Context SecurityContext ctx) {
  24. return getResponseString(ctx);
  25. }
  26. @GET
  27. @Path("roles-allowed") (2)
  28. @RolesAllowed({ "User", "Admin" }) (3)
  29. @Produces(MediaType.TEXT_PLAIN)
  30. public String helloRolesAllowed(@Context SecurityContext ctx) {
  31. return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); (4)
  32. }
  33. private String getResponseString(SecurityContext ctx) {
  34. String name;
  35. if (ctx.getUserPrincipal() == null) {
  36. name = "anonymous";
  37. } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
  38. throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
  39. } else {
  40. name = ctx.getUserPrincipal().getName();
  41. }
  42. return String.format("hello + %s,"
  43. + " isHttps: %s,"
  44. + " authScheme: %s,"
  45. + " hasJWT: %s",
  46. name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
  47. }
  48. private boolean hasJwt() {
  49. return jwt.getClaimNames() != null;
  50. }
  51. }
1Here we inject JsonWebToken
2This new endpoint will be located at /secured/roles-allowed
3@RolesAllowed is a JSR 250 common security annotation that indicates that the given endpoint is accessible by a caller if they have either a “User” or “Admin” role assigned.
4Here we build the reply the same way as in the hello method but also add a value of the JWT birthdate claim by directly calling the injected JsonWebToken.

After you make this addition to your TokenSecuredResource, rerun the ./mvnw compile quarkus:dev command, and then try curl -v [http://127.0.0.1:8080/secured/roles-allowed](http://127.0.0.1:8080/secured/roles-allowed); echo to attempt to access the new endpoint. Your output should be:

curl command for /secured/roles-allowed

  1. $ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
  2. * Trying 127.0.0.1...
  3. * TCP_NODELAY set
  4. * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
  5. > GET /secured/roles-allowed HTTP/1.1
  6. > Host: 127.0.0.1:8080
  7. > User-Agent: curl/7.54.0
  8. > Accept: */*
  9. >
  10. < HTTP/1.1 401 Unauthorized
  11. < Connection: keep-alive
  12. < Content-Type: text/html;charset=UTF-8
  13. < Content-Length: 14
  14. < Date: Sun, 03 Mar 2019 16:32:34 GMT
  15. <
  16. * Connection #0 to host 127.0.0.1 left intact
  17. Not authorized

Excellent, we have not provided any JWT in the request, so we should not be able to access the endpoint, and we were not. Instead we received an HTTP 401 Unauthorized error. We need to obtain and pass in a valid JWT to access that endpoint. There are two steps to this, 1) configuring our SmallRye JWT extension with information on how to validate a JWT, and 2) generating a matching JWT with the appropriate claims.

Configuring the SmallRye JWT Extension Security Information

Create a security-jwt-quickstart/src/main/resources/application.properties with the following content:

application.properties for TokenSecuredResource

  1. mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem (1)
  2. mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac (2)
1We are setting public key location to point to a classpath publicKey.pem resource location. We will add this key in part B, Adding a Public Key.
2We are setting the issuer to the URL string https://quarkus.io/using-jwt-rbac.

Adding a Public Key

The JWT specification defines various levels of security of JWTs that one can use. The MicroProfile JWT RBAC specification requires that JWTs that are signed with the RSA-256 signature algorithm. This in turn requires a RSA public key pair. On the REST endpoint server side, you need to configure the location of the RSA public key to use to verify the JWT sent along with requests. The mp.jwt.verify.publickey.location=publicKey.pem setting configured previously expects that the public key is available on the classpath as publicKey.pem. To accomplish this, copy the following content to a security-jwt-quickstart/src/main/resources/META-INF/resources/publicKey.pem file.

Adding publicKey.pem to resources/META-INF/resources ensures that it is available in the native image without having to provide a GraalVM resource file.

RSA Public Key PEM Content

  1. -----BEGIN PUBLIC KEY-----
  2. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
  3. Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
  4. TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
  5. UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
  6. AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
  7. sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
  8. nQIDAQAB
  9. -----END PUBLIC KEY-----

Generating a JWT

Often one obtains a JWT from an identity manager like Keycloak, but for this quickstart we will generate our own using the JWT generation API provided by smallrye-jwt (see Generate JWT tokens with SmallRye JWT for more information).

Take the code from the following listing and place into security-jwt-quickstart/src/main/java/org/acme/security/jwt/GenerateToken.java:

GenerateToken main Driver Class

  1. package org.acme.security.jwt;
  2. import java.util.Arrays;
  3. import java.util.HashSet;
  4. import org.eclipse.microprofile.jwt.Claims;
  5. import io.smallrye.jwt.build.Jwt;
  6. public class GenerateToken {
  7. /**
  8. * Generate JWT token
  9. */
  10. public static void main(String[] args) {
  11. String token =
  12. Jwt.issuer("https://quarkus.io/using-jwt-rbac") (1)
  13. .upn("jdoe@quarkus.io") (2)
  14. .groups(new HashSet<>(Arrays.asList("User", "Admin"))) (3)
  15. .claim(Claims.birthdate.name(), "2001-07-13") (4)
  16. .sign();
  17. System.out.println(token);
  18. }
  19. }
1The iss claim is the issuer of the JWT. This needs to match the server side mp.jwt.verify.issuer. in order for the token to be accepted as valid.
2The upn claim is defined by the MicroProfile JWT RBAC spec as preferred claim to use for the Principal seen via the container security APIs.
3The group claim provides the groups and top-level roles associated with the JWT bearer.
4The birthday claim. It can be considered to be a sensitive claim so you may want to consider encrypting the claims, see Generate JWT tokens with SmallRye JWT.

Note for this code to work we need the content of the RSA private key that corresponds to the public key we have in the TokenSecuredResource application. Take the following PEM content and place it into security-jwt-quickstart/src/main/resources/META-INF/resources/privateKey.pem:

RSA Private Key PEM Content

  1. -----BEGIN PRIVATE KEY-----
  2. MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
  3. PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
  4. OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
  5. qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
  6. nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
  7. uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
  8. oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
  9. 6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
  10. URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
  11. 96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
  12. Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
  13. zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
  14. KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
  15. iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
  16. m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
  17. 34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
  18. 5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
  19. tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
  20. WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
  21. b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
  22. nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
  23. MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
  24. Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
  25. Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
  26. FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
  27. f3cg+fr8aou7pr9SHhJlZCU=
  28. -----END PRIVATE KEY-----

We will use a smallrye.jwt.sign.key-location property to point to this private signing key.

Now we can generate a JWT to use with TokenSecuredResource endpoint. To do this, run the following command:

Sample JWT Generation Output

  1. $ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key-location=privateKey.pem
  2. eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjU5Njc2LCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1OTk3NiwiaWF0IjoxNTUxNjU5Njc2LCJqdGkiOiJhLTEyMyJ9.O9tx_wNNS4qdpFhxeD1e7v4aBNWz1FCq0UV8qmXd7dW9xM4hA5TO-ZREk3ApMrL7_rnX8z81qGPIo_R8IfHDyNaI1SLD56gVX-NaOLS2OjfcbO3zOWJPKR_BoZkYACtMoqlWgIwIRC-wJKUJU025dHZiNL0FWO4PjwuCz8hpZYXIuRscfFhXKrDX1fh3jDhTsOEFfu67ACd85f3BdX9pe-ayKSVLh_RSbTbBPeyoYPE59FW7H5-i8IE-Gqu838Hz0i38ksEJFI25eR-AJ6_PSUD0_-TV3NjXhF3bFIeT4VSaIZcpibekoJg0cQm-4ApPEcPLdgTejYHA-mupb8hSwg

The JWT string is the Base64 URL encoded string that has 3 parts separated by ‘.’ characters. First part - JWT headers, second part - JWT claims, third part - JWT signature.

Finally, Secured Access to /secured/roles-allowed

Now let’s use this to make a secured request to the /secured/roles-allowed endpoint. Make sure you have the Quarkus server running using the ./mvnw compile quarkus:dev command, and then run the following command, making sure to use your version of the generated JWT from the previous step:

  1. curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed; echo

curl Command for /secured/roles-allowed With JWT

  1. $ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
  2. hello + jdoe@quarkus.io, isHttps: false, authScheme: MP-JWT, hasJWT: true, birthdate: 2001-07-13

Success! We now have:

  • a non-anonymous caller name of jdoe@quarkus.io

  • an authentication scheme of Bearer

  • a non-null JsonWebToken

  • birthdate claim value

Using the JsonWebToken and Claim Injection

Now that we can generate a JWT to access our secured REST endpoints, let’s see what more we can do with the JsonWebToken interface and the JWT claims. The org.eclipse.microprofile.jwt.JsonWebToken interface extends the java.security.Principal interface, and is in fact the type of the object that is returned by the javax.ws.rs.core.SecurityContext#getUserPrincipal() call we used previously. This means that code that does not use CDI but does have access to the REST container SecurityContext can get hold of the caller JsonWebToken interface by casting the SecurityContext#getUserPrincipal().

The JsonWebToken interface defines methods for accessing claims in the underlying JWT. It provides accessors for common claims that are required by the MicroProfile JWT RBAC specification as well as arbitrary claims that may exist in the JWT.

All the JWT claims can also be injected. Let’s expand our TokenSecuredResource with another endpoint /secured/roles-allowed-admin which users the injected birthdate claim (as opposed to getting it from JsonWebToken):

  1. package org.acme.security.jwt;
  2. import javax.annotation.security.PermitAll;
  3. import javax.annotation.security.RolesAllowed;
  4. import javax.enterprise.context.RequestScoped;
  5. import javax.inject.Inject;
  6. import javax.ws.rs.GET;
  7. import javax.ws.rs.InternalServerErrorException;
  8. import javax.ws.rs.Path;
  9. import javax.ws.rs.Produces;
  10. import javax.ws.rs.core.Context;
  11. import javax.ws.rs.core.MediaType;
  12. import javax.ws.rs.core.SecurityContext;
  13. import org.eclipse.microprofile.jwt.Claim;
  14. import org.eclipse.microprofile.jwt.Claims;
  15. import org.eclipse.microprofile.jwt.JsonWebToken;
  16. @Path("/secured")
  17. @RequestScoped
  18. public class TokenSecuredResource {
  19. @Inject
  20. JsonWebToken jwt;
  21. @Inject
  22. @Claim(standard = Claims.birthdate)
  23. String birthdate; (1)
  24. @GET
  25. @Path("permit-all")
  26. @PermitAll
  27. @Produces(MediaType.TEXT_PLAIN)
  28. public String hello(@Context SecurityContext ctx) {
  29. return getResponseString(ctx);
  30. }
  31. @GET
  32. @Path("roles-allowed")
  33. @RolesAllowed({ "User", "Admin" })
  34. @Produces(MediaType.TEXT_PLAIN)
  35. public String helloRolesAllowed(@Context SecurityContext ctx) {
  36. return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString();
  37. }
  38. @GET
  39. @Path("roles-allowed-admin")
  40. @RolesAllowed("Admin")
  41. @Produces(MediaType.TEXT_PLAIN)
  42. public String helloRolesAllowedAdmin(@Context SecurityContext ctx) {
  43. return getResponseString(ctx) + ", birthdate: " + birthdate; (2)
  44. }
  45. private String getResponseString(SecurityContext ctx) {
  46. String name;
  47. if (ctx.getUserPrincipal() == null) {
  48. name = "anonymous";
  49. } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
  50. throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
  51. } else {
  52. name = ctx.getUserPrincipal().getName();
  53. }
  54. return String.format("hello + %s,"
  55. + " isHttps: %s,"
  56. + " authScheme: %s,"
  57. + " hasJWT: %s",
  58. name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
  59. }
  60. private boolean hasJwt() {
  61. return jwt.getClaimNames() != null;
  62. }
  63. }
1Here we use the injected birthday claim.
2Here we use the injected birthday claim to build the final reply.

Now generate the token again and run:

  1. curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed-admin; echo
  1. $ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
  2. hello + jdoe@quarkus.io, isHttps: false, authScheme: MP-JWT, hasJWT: true, birthdate: 2001-07-13

Package and run the application

As usual, the application can be packaged using ./mvnw clean package and executed using the -runner.jar file: .Runner jar Example

  1. Scotts-iMacPro:security-jwt-quickstart starksm$ ./mvnw clean package
  2. [INFO] Scanning for projects...
  3. ...
  4. [INFO] [io.quarkus.creator.phase.runnerjar.RunnerJarPhase] Building jar: /Users/starksm/Dev/JBoss/Protean/starksm64-quarkus-quickstarts/security-jwt-quickstart/target/security-jwt-quickstart-runner.jar
  5. Scotts-iMacPro:security-jwt-quickstart starksm$ java -jar target/security-jwt-quickstart-runner.jar
  6. 2019-03-28 14:27:48,839 INFO [io.quarkus] (main) Quarkus 0.12.0 started in 0.796s. Listening on: http://[::]:8080
  7. 2019-03-28 14:27:48,841 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, smallrye-jwt]

You can also generate the native executable with ./mvnw clean package -Pnative. .Native Executable Example

  1. Scotts-iMacPro:security-jwt-quickstart starksm$ ./mvnw clean package -Pnative
  2. [INFO] Scanning for projects...
  3. ...
  4. [security-jwt-quickstart-runner:25602] universe: 493.17 ms
  5. [security-jwt-quickstart-runner:25602] (parse): 660.41 ms
  6. [security-jwt-quickstart-runner:25602] (inline): 1,431.10 ms
  7. [security-jwt-quickstart-runner:25602] (compile): 7,301.78 ms
  8. [security-jwt-quickstart-runner:25602] compile: 10,542.16 ms
  9. [security-jwt-quickstart-runner:25602] image: 2,797.62 ms
  10. [security-jwt-quickstart-runner:25602] write: 988.24 ms
  11. [security-jwt-quickstart-runner:25602] [total]: 43,778.16 ms
  12. [INFO] ------------------------------------------------------------------------
  13. [INFO] BUILD SUCCESS
  14. [INFO] ------------------------------------------------------------------------
  15. [INFO] Total time: 51.500 s
  16. [INFO] Finished at: 2019-03-28T14:30:56-07:00
  17. [INFO] ------------------------------------------------------------------------
  18. Scotts-iMacPro:security-jwt-quickstart starksm$ ./target/security-jwt-quickstart-runner
  19. 2019-03-28 14:31:37,315 INFO [io.quarkus] (main) Quarkus 0.12.0 started in 0.006s. Listening on: http://[::]:8080
  20. 2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, smallrye-jwt]

Explore the Solution

The solution repository located in the security-jwt-quickstart directory contains all of the versions we have worked through in this quickstart guide as well as some additional endpoints that illustrate subresources with injection of JsonWebTokens and their claims into those using the CDI APIs. We suggest that you check out the quickstart solutions and explore the security-jwt-quickstart directory to learn more about the SmallRye JWT extension features.

Configuration Reference

Quarkus configuration

MicroProfile JWT configuration

Property NameDefaultDescription

mp.jwt.verify.publickey

none

The mp.jwt.verify.publickey config property allows the Public Key text itself to be supplied as a string. The Public Key will be parsed from the supplied string in the order defined in section Supported Public Key Formats.

mp.jwt.verify.publickey.location

none

Config property allows for an external or internal location of Public Key to be specified. The value may be a relative path or a URL. If the value points to an HTTPS based JWK set then, for it to work in native mode, the quarkus.ssl.native property must also be set to true, see Using SSL With Native Executables for more details.

mp.jwt.verify.issuer

none

Config property specifies the value of the iss (issuer) claim of the JWT that the server will accept as valid.

Supported Public Key Formats

Public Keys may be formatted in any of the following formats, specified in order of precedence:

  • Public Key Cryptography Standards #8 (PKCS#8) PEM

  • JSON Web Key (JWK)

  • JSON Web Key Set (JWKS)

  • JSON Web Key (JWK) Base64 URL encoded

  • JSON Web Key Set (JWKS) Base64 URL encoded

Additional SmallRye JWT configuration

SmallRye JWT provides more properties which can be used to customize the token processing:

Property NameDefaultDescription

smallrye.jwt.verify.algorithm

RS256

Signature algorithm. Set it to ES256 to support the Elliptic Curve signature algorithm.

smallrye.jwt.verify.key-format

ANY

Set this property to a specific key format such as PEM_KEY, PEM_CERTIFICATE, JWK or JWK_BASE64URL to optimize the way the verification key is loaded.

smallrye.jwt.verify.relax-key-validation

false

Relax the validation of the verification keys, setting this property to true will allow public RSA keys with the length less than 2048 bit.

smallrye.jwt.token.header

Authorization

Set this property if another header such as Cookie is used to pass the token.

smallrye.jwt.token.cookie

none

Name of the cookie containing a token. This property will be effective only if smallrye.jwt.token.header is set to Cookie.

smallrye.jwt.always-check-authorization

false

Set this property to true for Authorization header be checked even if the smallrye.jwt.token.header is set to Cookie but no cookie with a smallrye.jwt.token.cookie name exists.

smallrye.jwt.token.schemes

Bearer

Comma-separated list containing an alternative single or multiple schemes, for example, DPoP.

smallrye.jwt.token.kid

none

Key identifier. If it is set then the verification JWK key as well every JWT token must have a matching kid header.

smallrye.jwt.time-to-live

none

The maximum number of seconds that a JWT may be issued for use. Effectively, the difference between the expiration date of the JWT and the issued at date must not exceed this value.

smallrye.jwt.require.named-principal

false

If an application relies on java.security.Principal returning a name then a token must have a upn or preferred_username or sub claim set. Setting this property will result in SmallRye JWT throwing an exception if none of these claims is available for the application code to reliably deal with a non-null Principal name.

smallrye.jwt.path.sub

none

Path to the claim containing the subject name. It starts from the top level JSON object and can contain multiple segments where each segment represents a JSON object name only, example: realms/subject. This property can be used if a token has no ‘sub’ claim but has the subject set in a different claim. Use double quotes with the namespace qualified claims.

smallrye.jwt.claims.sub

none

This property can be used to set a default sub claim value when the current token has no standard or custom sub claim available. Effectively this property can be used to customize java.security.Principal name if no upn or preferred_username or sub claim is set.

smallrye.jwt.path.groups

none

Path to the claim containing the groups. It starts from the top level JSON object and can contain multiple segments where each segment represents a JSON object name only, example: realm/groups. This property can be used if a token has no ‘groups’ claim but has the groups set in a different claim. Use double quotes with the namespace qualified claims.

smallrye.jwt.groups-separator

‘ ‘

Separator for splitting a string which may contain multiple group values. It will only be used if the smallrye.jwt.path.groups property points to a custom claim whose value is a string. The default value is a single space because a standard OAuth2 scope claim may contain a space separated sequence.

smallrye.jwt.claims.groups

none

This property can be used to set a default groups claim value when the current token has no standard or custom groups claim available.

smallrye.jwt.jwks.refresh-interval

60

JWK cache refresh interval in minutes. It will be ignored unless the mp.jwt.verify.publickey.location points to the HTTPS URL based JWK set and no HTTP Cache-Control response header with a positive max-age parameter value is returned from a JWK HTTPS endpoint.

smallrye.jwt.jwks.forced-refresh-interval

30

Forced JWK cache refresh interval in minutes which is used to restrict the frequency of the forced refresh attempts which may happen when the token verification fails due to the cache having no JWK key with a kid property matching the current token’s kid header. It will be ignored unless the mp.jwt.verify.publickey.location points to the HTTPS URL based JWK set.

smallrye.jwt.expiration.grace

60

Expiration grace in seconds. By default an expired token will still be accepted if the current time is no more than 1 min after the token expiry time.

smallrye.jwt.verify.aud

none

Comma separated list of the audiences that a token aud claim may contain.

smallrye.jwt.required.claims

none

Comma separated list of the claims that a token must contain.

smallrye.jwt.decrypt.key-location

none

Config property allows for an external or internal location of Private Decryption Key to be specified.

smallrye.jwt.decrypt.algorithm

RSA_OAEP

Decryption algorithm.

smallrye.jwt.token.decryption.kid

none

Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching kid header.

Create JsonWebToken with JWTParser

If the JWT token can not be injected, for example, if it is embedded in the service request payload or the service endpoint acquires it out of band, then one can use JWTParser:

  1. import org.eclipse.microprofile.jwt.JsonWebToken;
  2. import io.smallrye.jwt.auth.principal.JWTParser;
  3. ...
  4. @Inject JWTParser parser;
  5. String token = getTokenFromOidcServer();
  6. // Parse and verify the token
  7. JsonWebToken jwt = parser.parse(token);

You can also use it to customize the way the token is verified or decrypted. For example, one can supply a local SecretKey:

  1. import javax.crypto.SecretKey;
  2. import javax.ws.rs.GET;
  3. import javax.ws.rs.core.NewCookie;
  4. import javax.ws.rs.core.Response;
  5. import org.eclipse.microprofile.jwt.JsonWebToken;
  6. import io.smallrye.jwt.auth.principal.JWTParser;
  7. import io.smallrye.jwt.build.Jwt;
  8. @Path("/secured")
  9. public class SecuredResource {
  10. @Inject JWTParser parser;
  11. private SecretKey key = createSecretKey();
  12. @GET
  13. @Produces("text/plain")
  14. public Response getUserName(@CookieParam("jwt") String jwtCookie) {
  15. Response response = null;
  16. if (jwtCookie == null) {
  17. String newJwtCookie = Jwt.upn("Alice").sign(key);
  18. // or newJwtCookie = Jwt.upn("alice").encrypt(key);
  19. return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build();
  20. else {
  21. // All mp.jwt and smallrye.jwt properties are still effective, only the verification key is customized.
  22. JsonWebToken jwt = parser.verify(jwtCookie, key);
  23. // or jwt = parser.decrypt(jwtCookie, key);
  24. return Response.ok(jwt.getName()).build();
  25. }
  26. }

Token Decryption

If your application needs to accept the tokens with the encrypted claims or with the encrypted inner signed claims then all you have to do is to set smallrye.jwt.decrypt.key-location pointing to the decryption key.

If this is the only key property which is set then the incoming token is expected to contain the encrypted claims only. If either mp.jwt.verify.publickey or mp.jwt.verify.publickey.location verification properties are also set then the incoming token is expected to contain the encrypted inner-signed token.

See Generate JWT tokens with SmallRye JWT and learn how to generate the encrypted or inner-signed and then encrypted tokens fast.

How to check the errors in the logs

Set quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE to see more details about the token verification or decryption errors.

Generate JWT tokens with SmallRye JWT

JWT claims can be signed or encrypted or signed first and the nested JWT token encrypted. Signing the claims is used most often to secure the claims. What is known today as a JWT token is typically produced by signing the claims in a JSON format using the steps described in the JSON Web Signature specification. However, when the claims are sensitive, their confidentiality can be guaranteed by following the steps described in the JSON Web Encryption specification to produce a JWT token with the encrypted claims. Finally both the confidentiality and integrity of the claims can be further enforced by signing them first and then encrypting the nested JWT token.

SmallRye JWT provides an API for securing the JWT claims using all of these options.

Create JwtClaimsBuilder and set the claims

The first step is to initialize a JwtClaimsBuilder using one of the options below and add some claims to it:

  1. import java.util.Collections;
  2. import javax.json.Json;
  3. import javax.json.JsonObject;
  4. import io.smallrye.jwt.build.Jwt;
  5. import io.smallrye.jwt.build.JwtClaimsBuilder;
  6. import org.eclipse.microprofile.jwt.JsonWebToken;
  7. ...
  8. // Create an empty builder and add some claims
  9. JwtClaimsBuilder builder1 = Jwt.claims();
  10. builder1.claim("customClaim", "custom-value").issuer("https://issuer.org");
  11. // Or start typing the claims immediately:
  12. // JwtClaimsBuilder builder1 = Jwt.upn("Alice");
  13. // Builder created from the existing claims
  14. JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json");
  15. // Builder created from a map of claims
  16. JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value"));
  17. // Builder created from JsonObject
  18. JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build();
  19. JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build();
  20. JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build();
  21. JwtClaimsBuilder builder4 = Jwt.claims(json);
  22. // Builder created from JsonWebToken
  23. @Inject JsonWebToken token;
  24. JwtClaimsBuilder builder5 = Jwt.claims(token);

The API is fluent so the builder initialization can be done as part of the fluent API sequence.

The builder will also set iat (issued at) to the current time, exp (expires at) to 5 minutes away from the current time (it can be customized with the smallrye.jwt.new-token.lifespan property) and jti (unique token identifier) claims if they have not already been set. One can also configure smallrye.jwt.new-token.issuer property and skip setting the issuer directly with the builder API.

The next step is to decide how to secure the claims.

Sign the claims

The claims can be signed immediately or after the JSON Web Signature headers have been set:

  1. import io.smallrye.jwt.build.Jwt;
  2. ...
  3. // Sign the claims using the private key loaded from the location set with a 'smallrye.jwt.sign.key-location' property.
  4. // No 'jws()' transition is necessary.
  5. String jwt1 = Jwt.claims("/tokenClaims.json").sign();
  6. // Set the headers and sign the claims with an RSA private key loaded in the code (the implementation of this method is omitted). Note a 'jws()' transition to a 'JwtSignatureBuilder'.
  7. String jwt2 = Jwt.claims("/tokenClaims.json").jws().keyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey());

Note the alg (algorithm) header is set to RS256 by default.

Encrypt the claims

The claims can be encrypted immediately or after the JSON Web Encryption headers have been set the same way as they can be signed. The only minor difference is that encrypting the claims always requires a jwe() JwtEncryptionBuilder transition:

  1. import io.smallrye.jwt.build.Jwt;
  2. ...
  3. // Encrypt the claims using the public key loaded from the location set with a 'smallrye.jwt.encrypt.key-location' property.
  4. String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt();
  5. // Set the headers and encrypt the claims with an RSA public key loaded in the code (the implementation of this method is omitted).
  6. String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getPublicKey());

Note the alg (key management algorithm) header is set to RSA-OAEP-256 (it will be changed to RSA-OAEP in a future version of smallrye-jwt) and the enc (content encryption header) is set to A256GCM by default.

Sign the claims and encrypt the nested JWT token

The claims can be signed and then the nested JWT token encrypted by combining the sign and encrypt steps.

  1. import io.smallrye.jwt.build.Jwt;
  2. ...
  3. // Sign the claims and encrypt the nested token using the private and public keys loaded from the locations set with the 'smallrye.jwt.sign.key-location' and 'smallrye.jwt.encrypt.key-location' properties respectively.
  4. String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt();

Fast JWT Generation

If smallrye.jwt.sign.key-location or/and smallrye.jwt.encrypt.key-location properties are set then one can secure the existing claims (resources, maps, JsonObjects) with a single call:

  1. // More compact than Jwt.claims("/claims.json").sign();
  2. Jwt.sign("/claims.json");
  3. // More compact than Jwt.claims("/claims.json").jwe().encrypt();
  4. Jwt.encrypt("/claims.json");
  5. // More compact than Jwt.claims("/claims.json").innerSign().encrypt();
  6. Jwt.signAndEncrypt("/claims.json");

As mentioned above, iat, exp, jti and iss claims will be added if needed.

SmallRye JWT Builder configuration

Smallrye JWT supports the following properties which can be used to customize the way claims are signed and encrypted:

Property NameDefaultDescription

smallrye.jwt.sign.key-location

none

Location of a private key which will be used to sign the claims when either a no-argument sign() or innerSign() method is called.

smallrye.jwt.encrypt.key-location

none

Location of a public key which will be used to encrypt the claims or inner JWT when a no-argument encrypt() method is called.

smallrye.jwt.new-token.lifespan

300

Token lifespan in seconds which will be used to calculate an exp (expiry) claim value if this claim has not already been set.

smallrye.jwt.new-token.issuer

none

Token issuer which can be used to set an iss (issuer) claim value if this claim has not already been set.

References