Microprofile JSON Web Token (JWT) authentication
Abstract
Providing feedback on Red Hat build of Quarkus documentation
To report an error or to improve our documentation, log in to your Red Hat Jira account and submit an issue. If you do not have a Red Hat Jira account, then you will be prompted to create an account.
Procedure
- Click the following link to create a ticket.
- Enter a brief description of the issue in the Summary.
- Provide a detailed description of the issue or enhancement in the Description. Include a URL to where the issue occurs in the documentation.
- Clicking Submit creates and routes the issue to the appropriate documentation team.
Chapter 1. Using JWT RBAC
This guide explains how to integrate SmallRye JWT into your Quarkus application to implement JSON Web Token (JWT) security in compliance with the MicroProfile JWT specification. You’ll learn how to verify JWTs, represent them as MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken
, and secure Quarkus HTTP endpoints using bearer token authorization and Role-Based Access Control.
The Quarkus OpenID Connect (quarkus-oidc
) extension also supports bearer token authorization and uses smallrye-jwt
to represent bearer tokens as JsonWebToken
. For details, see the OIDC Bearer Token Authentication guide.
If your Quarkus application needs to authenticate users using the OIDC Authorization Code Flow, you must use the OpenID Connect extension. For more information, refer to the OIDC Code Flow Mechanism for Protecting Web Applications.
1.1. Prerequisites
To complete this guide, you need:
- Roughly 15 minutes
- An IDE
-
JDK 17+ installed with
JAVA_HOME
configured appropriately - Apache Maven 3.8.6 or later
- Optionally the Quarkus CLI if you want to use it
- Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
1.2. Quickstart
1.2.1. Solution
We recommend following the instructions in the upcoming sections to create the application step by step. If you prefer, you can skip ahead to the completed example.
To access the example, either clone the Git repository or download an archive:
-
Clone the repository:
git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.20
. - Download the archive.
The completed solution is located in the security-jwt-quickstart
directory.
1.2.2. Creating the Maven project
First, create a new project with the following command:
Using the Quarkus CLI:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow quarkus create app org.acme:security-jwt-quickstart \ --extension='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ --no-code cd security-jwt-quickstart
quarkus create app org.acme:security-jwt-quickstart \ --extension='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ --no-code cd security-jwt-quickstart
To create a Gradle project, add the
--gradle
or--gradle-kotlin-dsl
option.For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow mvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.20.1:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-jwt-quickstart \ -Dextensions='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ -DnoCode cd security-jwt-quickstart
mvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.20.1:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-jwt-quickstart \ -Dextensions='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ -DnoCode cd security-jwt-quickstart
To create a Gradle project, add the
-DbuildTool=gradle
or-DbuildTool=gradle-kotlin-dsl
option.
For Windows users:
-
If using cmd, (don’t use backward slash
\
and put everything on the same line) -
If using Powershell, wrap
-D
parameters in double quotes e.g."-DprojectArtifactId=security-jwt-quickstart"
This command generates the Maven project 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:
Using the Quarkus CLI:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow quarkus extension add smallrye-jwt,smallrye-jwt-build
quarkus extension add smallrye-jwt,smallrye-jwt-build
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'
./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'
This command adds the following dependencies to your build file:
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow implementation("io.quarkus:quarkus-smallrye-jwt") implementation("io.quarkus:quarkus-smallrye-jwt-build")
implementation("io.quarkus:quarkus-smallrye-jwt") implementation("io.quarkus:quarkus-smallrye-jwt-build")
1.2.3. Examine the Jakarta REST resource
Create a REST endpoint in src/main/java/org/acme/security/jwt/TokenSecuredResource.java
with the following content:
REST endpoint V1
package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") public class TokenSecuredResource { @Inject JsonWebToken jwt; @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { return getResponseString(ctx); } private String getResponseString(SecurityContext ctx) { String name; if (ctx.getUserPrincipal() == null) { name = "anonymous"; } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); } else { name = ctx.getUserPrincipal().getName(); } return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); } private boolean hasJwt() { return jwt.getClaimNames() != null; } }
package org.acme.security.jwt;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
public class TokenSecuredResource {
@Inject
JsonWebToken jwt;
@GET
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx);
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) {
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName();
}
return String.format("hello %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
- 1
- The
JsonWebToken
interface is injected, providing access to claims associated with the current authenticated token. This interface extendsjava.security.Principal
. - 2
- The
@PermitAll
is a standard Jakarta security annotation. It indicates that the given endpoint is accessible by all callers, whether authenticated or not. - 3
- The Jakarta REST
SecurityContext
is injected to inspect the security state of the request. ThegetResponseString()
function generates the response. - 4
- Checks if the call is insecure by checking if the request user/caller
Principal
against null. - 5
- Ensures the names in the
Principal
andJsonWebToken
match because theJsonWebToken
represents the currentPrincipal
. - 6
- Retrieves the name of the
Principal
. - 7
- Builds a response containing the caller’s name, the
isSecure()
andgetAuthenticationScheme()
states of the requestSecurityContext
, and whether a non-nullJsonWebToken
was injected.
1.2.4. Run the application in dev mode
Now, you are ready to run the application in dev mode by using one of the following commands:
Using the Quarkus CLI:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow quarkus dev
quarkus dev
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./mvnw quarkus:dev
./mvnw quarkus:dev
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./gradlew --console=plain quarkusDev
./gradlew --console=plain quarkusDev
Then, you should see output similar to the following example:
quarkus:dev
output
[INFO] Scanning for projects... [INFO] [INFO] ----------------------< org.acme:security-jwt-quickstart >----------------------- [INFO] Building security-jwt-quickstart 1.0.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- ... Listening for transport dt_socket at address: 5005 2020-07-15 16:09:50,883 INFO [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, rest, rest-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< org.acme:security-jwt-quickstart >-----------------------
[INFO] Building security-jwt-quickstart 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
...
Listening for transport dt_socket at address: 5005
2020-07-15 16:09:50,883 INFO [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080
2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, rest, rest-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]
Now that the REST endpoint is running, you can access it by using a command line tool such as curl:
curl
command for /secured/permit-all
curl http://127.0.0.1:8080/secured/permit-all; echo
$ curl http://127.0.0.1:8080/secured/permit-all; echo
This command returns the following response:
hello anonymous, isHttps: false, authScheme: null, hasJWT: false
hello anonymous, isHttps: false, authScheme: null, hasJWT: false
You have not provided any JWT in our request, so you would not expect the endpoint to see any security state, and the response is consistent with that:
-
username
is anonymous. -
isHttps
isfalse
becausehttps
is not used. -
authScheme
isnull
. -
hasJWT
isfalse
.
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
package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") public class TokenSecuredResource { @Inject JsonWebToken jwt; @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { return getResponseString(ctx); } @GET @Path("roles-allowed") @RolesAllowed({ "User", "Admin" }) @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowed(@Context SecurityContext ctx) { return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); } private String getResponseString(SecurityContext ctx) { String name; if (ctx.getUserPrincipal() == null) { name = "anonymous"; } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); } else { name = ctx.getUserPrincipal().getName(); } return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); } private boolean hasJwt() { return jwt.getClaimNames() != null; } }
package org.acme.security.jwt;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
public class TokenSecuredResource {
@Inject
JsonWebToken jwt;
@GET
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx);
}
@GET
@Path("roles-allowed")
@RolesAllowed({ "User", "Admin" })
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString();
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) {
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName();
}
return String.format("hello %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
- 1
- The
JsonWebToken
is injected to access claims from the JWT. - 2
- This endpoint is exposed at
/secured/roles-allowed
. - 3
- The
@RolesAllowed
annotation restricts access to users with either the "User" or "Admin" role. - 4
- The response is constructed similarly to the
hello
method, with the addition of thebirthdate
claim retrieved directly from the injectedJsonWebToken
.
After you make this addition to your TokenSecuredResource
, rerun the ./mvnw quarkus:dev
command, and then try curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
to attempt to access the new endpoint.
Your output should be as follows:
curl
command for /secured/roles-allowed
curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
This command returns the following response:
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > GET /secured/roles-allowed HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 401 Unauthorized < Connection: keep-alive < Content-Type: text/html;charset=UTF-8 < Content-Length: 14 < Date: Sun, 03 Mar 2019 16:32:34 GMT < * Connection #0 to host 127.0.0.1 left intact
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /secured/roles-allowed HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 14
< Date: Sun, 03 Mar 2019 16:32:34 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Excellent. You did not provide a JWT in the request, so access to the endpoint was correctly denied. Instead, you received an HTTP 401 Unauthorized error.
To access the endpoint, you must obtain and include a valid JWT in your request. This involves two steps:
- Configuring the SmallRye JWT extension with the necessary information to validate a JWT.
- Generating a JWT with the appropriate claims to match the configuration.
1.2.5. 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
mp.jwt.verify.publickey.location=publicKey.pem mp.jwt.verify.issuer=https://example.com/issuer quarkus.native.resources.includes=publicKey.pem
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://example.com/issuer
quarkus.native.resources.includes=publicKey.pem
- 1
- Specifies the location of the public key file
publicKey.pem
on the classpath. See Adding a public key for adding this key. - 2
- Defines the expected issuer as
https://example.com/issuer
. - 3
- Ensures the
publicKey.pem
file is included as a resource in the native executable.
1.2.6. Adding a public key
The JWT specification defines various levels of security of JWTs that one can use. The MicroProfile JWT RBAC specification requires JWTs signed with the RSA-256 signature algorithm. This in turn requires an 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/publicKey.pem
file.
RSA public key PEM content
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9 AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x nQIDAQAB -----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----
1.2.7. Generating a JWT
Often, one obtains a JWT from an identity manager such as Keycloak. But for this quickstart, you generate our own by using the JWT generation API provided by smallrye-jwt
. For more information, see Generate JWT tokens with SmallRye JWT.
Take the code from the following listing and place it into security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java
:
GenerateToken
main driver class
package org.acme.security.jwt; import java.util.Arrays; import java.util.HashSet; import org.eclipse.microprofile.jwt.Claims; import io.smallrye.jwt.build.Jwt; /** * A utility class to generate and print a JWT token string to stdout. */ public class GenerateToken { /** * Generates and prints a JWT token. */ public static void main(String[] args) { String token = Jwt.issuer("https://example.com/issuer") .upn("jdoe@quarkus.io") .groups(new HashSet<>(Arrays.asList("User", "Admin"))) .claim(Claims.birthdate.name(), "2001-07-13") .sign(); System.out.println(token); System.exit(0); } }
package org.acme.security.jwt;
import java.util.Arrays;
import java.util.HashSet;
import org.eclipse.microprofile.jwt.Claims;
import io.smallrye.jwt.build.Jwt;
/**
* A utility class to generate and print a JWT token string to stdout.
*/
public class GenerateToken {
/**
* Generates and prints a JWT token.
*/
public static void main(String[] args) {
String token = Jwt.issuer("https://example.com/issuer")
.upn("jdoe@quarkus.io")
.groups(new HashSet<>(Arrays.asList("User", "Admin")))
.claim(Claims.birthdate.name(), "2001-07-13")
.sign();
System.out.println(token);
System.exit(0);
}
}
- 1
- Sets the
iss
(issuer) claim in the JWT. This value must match the server-sidemp.jwt.verify.issuer
configuration for the token to be considered valid. - 2
- Specifies the
upn
(User Principal Name) claim, which the MicroProfile JWT RBAC specification defines as the preferred claim for identifying thePrincipal
in container security APIs. - 3
- Defines the
groups
claim, which provides the group memberships and top-level roles assigned to the JWT bearer. - 4
- Adds a
birthdate
claim. Because this can be considered sensitive information, consider encrypting claims as described in Generate JWT tokens with SmallRye JWT.
Note that for this code to work, you need the content of the RSA private key corresponding to the public key you have in the TokenSecuredResource
application. Take the following PEM content and place it into security-jwt-quickstart/src/test/resources/privateKey.pem
:
RSA private key PEM content
-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6 oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv 6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6 96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3 zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS 34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG 5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2 tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09 nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8 f3cg+fr8aou7pr9SHhJlZCU= -----END PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----
Later, you configure the smallrye.jwt.sign.key.location
property to specify the location of the private signing key.
It is also possible to generate a public and private key pair by using the OpenSSL command line tool.
openssl
commands to generate keys
openssl genrsa -out rsaPrivateKey.pem 2048 openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
An additional step is required to generate and convert the private key to the PKCS#8 format, commonly used for secure key storage and transport.
openssl
commands to perform the conversion
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem
You can use the generated key pair instead of those used in this quickstart.
Ensure the application is running before generating the JSON Web Token (JWT) for the TokenSecuredResource
endpoint.
Next, use the following command to generate the JWT:
Sample JWT generation output
mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem
$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem
The JWT string is a Base64 URL-encoded string consisting of three parts, separated by .
characters:
- The header, which contains metadata about the token, such as the signing algorithm.
- The payload, also called "claims", which includes the token’s claims or data.
- The signature, which verifies the token’s integrity.
1.2.8. 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 still running in dev mode, and then run the following command, making sure to use your version of the generated JWT from the previous step:
curl
command for /secured/roles-allowed
with JWT
curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
Make sure to use the generated token as the HTTP Authorization Bearer scheme value.
This command returns the following response:
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
Success! You now have the following:
-
A non-anonymous caller name:
jdoe@quarkus.io
-
An authentication scheme:
Bearer
-
A non-null
JsonWebToken
-
The
birthdate
claim value
1.2.9. Using the JsonWebToken
and claim injection
Now that you can generate a JWT to access our secured REST endpoints, let’s see what more you 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 the object type returned by the jakarta.ws.rs.core.SecurityContext#getUserPrincipal()
call you 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 and arbitrary claims that might 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 uses the injected birthdate
claim (as opposed to getting it from JsonWebToken
):
package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.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 public class TokenSecuredResource { @Inject JsonWebToken jwt; @Inject @Claim(standard = Claims.birthdate) String birthdate; @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { return getResponseString(ctx); } @GET @Path("roles-allowed") @RolesAllowed({ "User", "Admin" }) @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowed(@Context SecurityContext ctx) { return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); } @GET @Path("roles-allowed-admin") @RolesAllowed("Admin") @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowedAdmin(@Context SecurityContext ctx) { return getResponseString(ctx) + ", birthdate: " + birthdate; } private String getResponseString(SecurityContext ctx) { String name; if (ctx.getUserPrincipal() == null) { name = "anonymous"; } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); } else { name = ctx.getUserPrincipal().getName(); } return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); } private boolean hasJwt() { return jwt.getClaimNames() != null; } }
package org.acme.security.jwt;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.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
public class TokenSecuredResource {
@Inject
JsonWebToken jwt;
@Inject
@Claim(standard = Claims.birthdate)
String birthdate;
@GET
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx);
}
@GET
@Path("roles-allowed")
@RolesAllowed({ "User", "Admin" })
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString();
}
@GET
@Path("roles-allowed-admin")
@RolesAllowed("Admin")
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowedAdmin(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + birthdate;
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) {
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName();
}
return String.format("hello %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
- 1
- The
@RequestScoped
scope is required to enable injection of thebirthdate
claim as aString
. - 2
- The
JsonWebToken
is injected here, providing access to all claims and JWT-related information. - 3
- The
birthdate
claim is injected as aString
. This highlights why the@RequestScoped
scope is mandatory. - 4
- The injected
birthdate
claim is directly used to construct the response.
Now generate the token again and run:
curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
Make sure to use the generated token as the HTTP Authorization Bearer scheme value.
This command returns the following response:
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
1.2.10. Run the application in JVM mode
You can run the application as a standard Java application.
Compile the application:
Using the Quarkus CLI:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow quarkus build
quarkus build
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./mvnw install
./mvnw install
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./gradlew build
./gradlew build
Run the application:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow java -jar target/quarkus-app/quarkus-run.jar
java -jar target/quarkus-app/quarkus-run.jar
1.2.11. Run the application in native mode
You can compile this same demo into native mode without 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:Using the Quarkus CLI:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow quarkus build --native
quarkus build --native
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./mvnw install -Dnative
./mvnw install -Dnative
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./gradlew build -Dquarkus.native.enabled=true
./gradlew build -Dquarkus.native.enabled=true
Run the following binary directly:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow ./target/security-jwt-quickstart-1.0.0-SNAPSHOT-runner
./target/security-jwt-quickstart-1.0.0-SNAPSHOT-runner
1.2.12. Explore the solution
The security-jwt-quickstart
directory repository contains all the versions covered in this quickstart guide, along with additional endpoints that demonstrate subresources using injected JsonWebToken
tokens and their claims via CDI APIs.
We encourage you to explore the security-jwt-quickstart
directory and review the quickstart solutions to learn more about the features of the SmallRye JWT extension.
1.3. Reference guide
1.3.1. Supported injection scopes
@ApplicationScoped
, @Singleton
and @RequestScoped
outer bean injection scopes are all supported when an org.eclipse.microprofile.jwt.JsonWebToken
is injected, with the @RequestScoped
scoping for JsonWebToken
enforced to ensure the current token is represented.
However, @RequestScoped
must be used when the individual token claims are injected as simple types such as String
, for example:
package org.acme.security.jwt; import jakarta.inject.Inject; import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.Claims; @Path("/secured") @RequestScoped public class TokenSecuredResource { @Inject @Claim(standard = Claims.birthdate) String birthdate; }
package org.acme.security.jwt;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
@Path("/secured")
@RequestScoped
public class TokenSecuredResource {
@Inject
@Claim(standard = Claims.birthdate)
String birthdate;
}
Note you can also use the injected JsonWebToken
to access the individual claims, but setting @RequestScoped
is unnecessary in this case.
Please see MP JWT CDI Injection Requirements for more details.
1.3.2. Supported public key formats
Public keys can 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
1.3.3. Dealing with verification keys
If you need to verify the token signature by using the asymmetric RSA or Elliptic Curve (EC) key, use the mp.jwt.verify.publickey.location
property to refer to the local or remote verification key.
Use mp.jwt.verify.publickey.algorithm
to customize the verification algorithm (default is RS256
); for example, set it to ES256
when working with the EC keys.
If you need to verify the token signature by using the symmetric secret key, then either a JSON Web Key
(JWK) or JSON Web Key Set
(JWK Set) format must be used to represent this secret key, for example:
{ "keys": [ { "kty":"oct", "kid":"secretKey", "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" } ] }
{
"keys": [
{
"kty":"oct",
"kid":"secretKey",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
]
}
This secret key JWK must also be referred to with smallrye.jwt.verify.key.location
. smallrye.jwt.verify.algorithm
should be set to HS256
/HS384
/HS512
.
1.3.4. Parse and verify 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
:
import org.eclipse.microprofile.jwt.JsonWebToken; import io.smallrye.jwt.auth.principal.JWTParser; ... @Inject JWTParser parser; String token = getTokenFromOidcServer(); // Parse and verify the token JsonWebToken jwt = parser.parse(token);
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
...
@Inject JWTParser parser;
String token = getTokenFromOidcServer();
// Parse and verify the token
JsonWebToken jwt = parser.parse(token);
You can also use it to customize how the token is verified or decrypted. For example, one can supply a local SecretKey
:
package org.acme.security.jwt; import io.smallrye.jwt.auth.principal.ParseException; import jakarta.inject.Inject; import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.jwt.JsonWebToken; import io.smallrye.jwt.auth.principal.JWTParser; import io.smallrye.jwt.build.Jwt; @Path("/secured") public class SecuredResource { private static final String SECRET = "AyM1SysPpbyDfgZld3umj1qzKObwVMko"; @Inject JWTParser parser; @GET @Produces("text/plain") public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException { if (jwtCookie == null) { // Create a JWT token signed by using the 'HS256' algorithm String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET); // or create a JWT token encrypted by using the 'A256KW' algorithm // Jwt.upn("alice").encryptWithSecret(secret); return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build(); } else { // All mp.jwt and smallrye.jwt properties are still effective; only the verification key is customized. JsonWebToken jwt = parser.verify(jwtCookie, SECRET); // or jwt = parser.decrypt(jwtCookie, secret); return Response.ok(jwt.getName()).build(); } } }
package org.acme.security.jwt;
import io.smallrye.jwt.auth.principal.ParseException;
import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
import io.smallrye.jwt.build.Jwt;
@Path("/secured")
public class SecuredResource {
private static final String SECRET = "AyM1SysPpbyDfgZld3umj1qzKObwVMko";
@Inject
JWTParser parser;
@GET
@Produces("text/plain")
public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException {
if (jwtCookie == null) {
// Create a JWT token signed by using the 'HS256' algorithm
String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET);
// or create a JWT token encrypted by using the 'A256KW' algorithm
// Jwt.upn("alice").encryptWithSecret(secret);
return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build();
} else {
// All mp.jwt and smallrye.jwt properties are still effective; only the verification key is customized.
JsonWebToken jwt = parser.verify(jwtCookie, SECRET);
// or jwt = parser.decrypt(jwtCookie, secret);
return Response.ok(jwt.getName()).build();
}
}
}
Please also see the How to Add SmallRye JWT directly section about using JWTParser
without the HTTP
support provided by quarkus-smallrye-jwt
.
1.3.5. Token decryption
If your application needs to accept tokens with encrypted claims or encrypted inner-signed claims, simply set the smallrye.jwt.decrypt.key.location
property to point to the decryption key.
If this is the only key property set, the incoming token is expected to contain only encrypted claims. 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 quickly.
1.3.6. Custom factories
The io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory
is the default implementation used to parse and verify JWT tokens, converting them into JsonWebToken
principals. This factory relies on the MP JWT
and smallrye-jwt
properties, as described in the Configuration
section, to validate and customize JWT tokens.
If you need to implement a custom factory—such as to skip re-verifying tokens that have already been validated by a firewall—you can do so in one of the following ways:
-
Use the
ServiceLoader
mechanism by creating aMETA-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory
resource. -
Provide an
Alternative
CDI bean implementation, like the example below:
import java.nio.charset.StandardCharsets; import java.util.Base64; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.consumer.InvalidJwtException; import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; import io.smallrye.jwt.auth.principal.JWTCallerPrincipal; import io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory; import io.smallrye.jwt.auth.principal.ParseException; @ApplicationScoped @Alternative @Priority(1) public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory { @Override public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException { try { // Token has already been verified; parse the token claims only String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); return new DefaultJWTCallerPrincipal(JwtClaims.parse(json)); } catch (InvalidJwtException ex) { throw new ParseException(ex.getMessage()); } } }
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory;
import io.smallrye.jwt.auth.principal.ParseException;
@ApplicationScoped
@Alternative
@Priority(1)
public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory {
@Override
public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException {
try {
// Token has already been verified; parse the token claims only
String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
return new DefaultJWTCallerPrincipal(JwtClaims.parse(json));
} catch (InvalidJwtException ex) {
throw new ParseException(ex.getMessage());
}
}
}
1.3.7. Blocking calls
quarkus-smallrye-jwt
extension uses SmallRye JWT library which is currently not reactive.
What it means from the perspective of quarkus-smallrye-jwt
, which operates as part of the reactive Quarkus security architecture, is that an IO thread entering the SmallRye JWT verification or decryption code might block in one of the following cases:
-
The default key resolver refreshes the
JsonWebKey
set containing the keys, which involves a remote call to the OIDC endpoint. -
The custom key resolver, such as
AWS Application Load Balancer
(ALB
) key resolver, resolves the keys against the AWS ALB key endpoint by using the current token’s key identifier header value.
In such cases, if connections are slow—for instance, taking more than 3 seconds to respond to the key endpoint—the current event loop thread is likely to become blocked.
To prevent it from blocking, set quarkus.smallrye-jwt.blocking-authentication=true
.
1.3.8. Token propagation
Please see the Token Propagation section about the Bearer access token propagation to the downstream services.
1.3.9. Testing
1.3.9.1. Wiremock
If you configure mp.jwt.verify.publickey.location
to point to HTTPS or HTTP-based JsonWebKey (JWK) set, then you can use the same approach as described in the OpenID Connect Bearer Token Integration testing Wiremock
section but only change the application.properties
to use MP JWT configuration properties instead:
keycloak.url is set by OidcWiremockTestResource
# keycloak.url is set by OidcWiremockTestResource
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
1.3.9.2. Keycloak
If you work with Keycloak and configure mp.jwt.verify.publickey.location
to point to HTTPS or HTTP-based JsonWebKey (JWK) set, you can use the same approach as described in the OpenID Connect Bearer Token Integration testing Keycloak section but only change the application.properties
to use MP JWT configuration properties instead:
keycloak.url is set by DevServices for Keycloak
# keycloak.url is set by DevServices for Keycloak
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
Note that the tokens issued by Keycloak have an iss
(issuer) claim set to the realm endpoint address.
If your Quarkus application runs in a Docker container, it might share a network interface with a Keycloak container started by DevServices for Keycloak. In this scenario, the Quarkus application and Keycloak communicate through an internal shared Docker network.
In such cases, use the following configuration instead:
keycloak.url is set by DevServices for Keycloak, Quarkus accesses it through an internal shared docker network interface. Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property
# keycloak.url is set by DevServices for Keycloak,
# Quarkus accesses it through an internal shared docker network interface.
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
# Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property
mp.jwt.verify.issuer=${client.quarkus.oidc.auth-server-url}
1.3.9.3. Local public key
You can use the same approach as described in the OpenID Connect Bearer Token Integration testing Local public key
section but only change the application.properties
to use MP JWT configuration properties instead:
mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB # set it to the issuer value which is used to generate the tokens mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus # required to sign the tokens smallrye.jwt.sign.key.location=privateKey.pem
mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
# set it to the issuer value which is used to generate the tokens
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
# required to sign the tokens
smallrye.jwt.sign.key.location=privateKey.pem
1.3.9.4. TestSecurity
annotation
Add the following dependency:
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-jwt</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-jwt</artifactId> <scope>test</scope> </dependency>
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow testImplementation("io.quarkus:quarkus-test-security-jwt")
testImplementation("io.quarkus:quarkus-test-security-jwt")
Then, write test code such as this:
import static org.hamcrest.Matchers.is; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.quarkus.test.security.jwt.Claim; import io.quarkus.test.security.jwt.JwtSecurity; import io.restassured.RestAssured; @QuarkusTest @TestHTTPEndpoint(ProtectedResource.class) public class TestSecurityAuthTest { @Test @TestSecurity(user = "userJwt", roles = "viewer") public void testJwt() { RestAssured.when().get("test-security-jwt").then() .body(is("userJwt:viewer")); } @Test @TestSecurity(user = "userJwt", roles = "viewer") @JwtSecurity(claims = { @Claim(key = "email", value = "user@gmail.com") }) public void testJwtWithClaims() { RestAssured.when().get("test-security-jwt-claims").then() .body(is("userJwt:viewer:user@gmail.com")); } }
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.jwt.Claim;
import io.quarkus.test.security.jwt.JwtSecurity;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userJwt", roles = "viewer")
public void testJwt() {
RestAssured.when().get("test-security-jwt").then()
.body(is("userJwt:viewer"));
}
@Test
@TestSecurity(user = "userJwt", roles = "viewer")
@JwtSecurity(claims = {
@Claim(key = "email", value = "user@gmail.com")
})
public void testJwtWithClaims() {
RestAssured.when().get("test-security-jwt-claims").then()
.body(is("userJwt:viewer:user@gmail.com"));
}
}
where the ProtectedResource
class might look like this:
@Path("/web-app") @Authenticated public class ProtectedResource { @Inject JsonWebToken accessToken; @GET @Path("test-security-jwt") public String testSecurityOidc() { return accessToken.getName() + ":" + accessToken.getGroups().iterator().next(); } @GET @Path("test-security-jwt-claims") public String testSecurityOidcUserInfoMetadata() { return accessToken.getName() + ":" + accessToken.getGroups().iterator().next() + ":" + accessToken.getClaim("email"); } }
@Path("/web-app")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
@GET
@Path("test-security-jwt")
public String testSecurityOidc() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
}
@GET
@Path("test-security-jwt-claims")
public String testSecurityOidcUserInfoMetadata() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
+ ":" + accessToken.getClaim("email");
}
}
Note that the @TestSecurity
annotation must always be used, and its user
property is returned as JsonWebToken.getName()
and roles
property - as JsonWebToken.getGroups()
. @JwtSecurity
annotation is optional and can be used to set the additional token claims.
@TestSecurity
and @JwtSecurity
can be combined in a meta-annotation, as follows:
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(introspectionRequired = true, introspection = { @TokenIntrospection(key = "email", value = "user@gmail.com") } ) public @interface TestSecurityMetaAnnotation { }
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public @interface TestSecurityMetaAnnotation {
}
This is particularly useful if the same set of security settings needs to be used in multiple test methods.
1.3.10. How to check the errors in the logs
Please enable io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator
TRACE
level logging to see more details about the token verification or decryption errors:
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE
1.3.11. Proactive authentication
If you’d like to skip the token verification when the public endpoint methods are invoked, disable the proactive authentication.
Note that you can’t access the injected JsonWebToken
through public methods if token verification has not been done.
1.3.12. How to add SmallRye JWT directly
To parse and verify JsonWebToken with JWTParser, use smallrye-jwt
instead of quarkus-smallrye-jwt
directly for the following situations:
-
You work with Quarkus extensions that do not support
HTTP
, such asQuarkus GRPC
. -
You provide an extension-specific
HTTP
, the support of which conflicts with the support of those offered byquarkus-smallrye-jwt
andVert.x HTTP
, such asQuarkus AWS Lambda
.
Start with adding the smallrye-jwt
dependency:
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow <dependency> <groupId>io.smallrye</groupId> <artifactId>smallrye-jwt</artifactId> </dependency>
<dependency> <groupId>io.smallrye</groupId> <artifactId>smallrye-jwt</artifactId> </dependency>
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow implementation("io.smallrye:smallrye-jwt")
implementation("io.smallrye:smallrye-jwt")
Then, update application.properties
to get all the CDI producers provided by smallrye-jwt
included as follows:
quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt
quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye
quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt
1.4. Configuration reference
1.4.1. Quarkus configuration
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property | Type | Default |
The MP-JWT configuration object
Environment variable: | boolean |
|
The name of the
Environment variable: | string |
|
Enable this property if fetching the remote keys can be a time-consuming operation. Do not enable it if you use the local keys.
Environment variable: | boolean |
|
Always create HTTP 401 challenge, even for requests containing no authentication credentials. JWT authentication mechanism will return HTTP 401 when an authentication challenge is required. However if it is used alongside one of the interactive authentication mechanisms then returning HTTP 401 to the users accessing the application from a browser may not be desired. If you prefer you can request that JWT authentication mechanism does not create a challenge in such cases by setting this property to 'true'.
Environment variable: | boolean |
|
1.4.2. MicroProfile JWT configuration
Property Name | Default | Description |
---|---|---|
|
|
The |
|
|
Config property allows for a specified external or internal location of the public key. The value can 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 |
|
|
List of signature algorithms. Set it to |
|
| Config property allows for a specified external or internal location of the Private Decryption Key. |
|
|
List of decryption algorithms. Set it to |
|
|
Config property specifies the value of the |
|
|
Comma-separated list of audiences a token |
|
| Clock skew in seconds used during the token expiration and age verification. An expired token is accepted if the current time is within the number of seconds specified by this property after the token expiration time. The default value is 60 seconds. |
|
|
Number of seconds that must not elapse since the token |
|
|
Set this property if another header, such as |
|
|
Name of the cookie containing a token. This property is effective only if |
1.4.3. Additional SmallRye JWT configuration
SmallRye JWT provides more properties that can be used to customize the token processing:
Property Name | Default | Description |
---|---|---|
|
| Secret key supplied as a string. |
|
| Location of the verification key, which can point to both public and secret keys. Secret keys can only be in the JWK format. Note that 'mp.jwt.verify.publickey.location' is ignored if this property is set. |
|
Signature algorithm. This property should only be used to set a symmetric algorithm such as | |
|
|
Set this property to a specific key format such as |
|
|
By default, PEM, JWK, or JWK key sets can be read from the local file system or fetched from URIs as required by MicroProfile JWT specification. Set this property to |
|
|
Relax the validation of the verification keys; setting this property to |
|
| If this property is enabled, a signed token must contain either 'x5t' or 'x5t#S256' X509Certificate thumbprint headers. Verification keys can only be in JWK or PEM Certificate key formats. JWK keys must have an 'x5c' (Base64-encoded X509Certificate) property set. |
|
|
Set this property if another header, such as |
|
|
Key cache size. Use this property and |
|
|
Key cache entry time-to-live in minutes. Use this property and |
|
|
Name of the cookie containing a token. This property is effective only if |
|
|
Set this property to |
|
|
Comma-separated list containing alternative single or multiple schemes, such as |
|
|
Key identifier. The verification JWK key and every JWT token must have a matching |
|
| The maximum number of seconds a JWT can be issued for use. Effectively, the difference between the expiration date of the JWT and the issued at date must not exceed this value. Setting this property to a non-positive value relaxes the requirement for the token to have a valid 'iat' (issued at) claim. |
|
|
If an application relies on |
|
|
Path to the claim containing the subject name. It starts from the top-level JSON object and can contain multiple segments where each segment only represents a JSON object name, for example, |
|
|
This property can set a default sub claim value when the current token has no standard or custom |
|
|
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, for example: |
|
|
Separator for splitting a string which might contain multiple group values. It is only used if the |
|
| This property can set a default groups claim value when the current token has no standard or custom groups claim available. |
|
|
JWK cache refresh interval in minutes. It is ignored unless the |
|
|
Forced JWK cache refresh interval in minutes, which is used to restrict the frequency of the forced refresh attempts that might happen when the token verification fails due to the cache having no JWK key with a |
|
|
Expiration grace in seconds. By default, an expired token is still accepted if the current time is no more than 1 min after the token expiry time. This property is deprecated. Use |
|
|
Comma-separated list of audiences a token |
|
| Comma-separated list of the claims a token must contain. |
|
|
Config property to specify the external or internal location of Private Decryption Key. This property is deprecated - use |
|
| Decryption algorithm. |
|
| Decryption key supplied as a string. |
|
|
Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching |
|
|
Path to TLS trusted certificate which might need to be configured if the keys have to be fetched over |
|
|
Trust all the hostnames. If the keys have to be fetched over |
|
|
Set of trusted hostnames. If the keys have to be fetched over |
|
| HTTP proxy host. |
|
| HTTP proxy port. |
|
|
This property can be used to customize a keystore type if either |
|
This property can be used to customize a | |
|
Keystore password. If | |
|
This property has to be set to identify a public verification key which is extracted from | |
|
This property has to be set to identify a private decryption key if | |
|
This property can be set if a private decryption key’s password in | |
|
| Set this property to true to resolve the remote keys at the application startup. |
1.5. References
Chapter 2. Build, sign, and encrypt JSON Web Tokens
JSON Web Token (JWT) is defined by the RFC 7519 specification as a compact, URL-safe means of representing claims. These claims are encoded as a JSON object and can be used as the payload of a JSON Web Signature (JWS) structure or the plaintext of a JSON Web Encryption (JWE) structure. This mechanism enables claims to be digitally signed or protected for integrity with a Message Authentication Code (MAC) and encrypted.
Signing the claims is the most common method for securing them. Typically, a JWT token is produced by signing claims formatted as JSON, following the steps outlined in the JSON Web Signature (JWS) specification.
When the claims contain sensitive information, their confidentiality can be ensured by using the JSON Web Encryption (JWE) specification. This approach produces a JWT with encrypted claims.
For enhanced security, you can combine both methods: sign the claims first and then encrypt the resulting nested JWT. This process ensures both the confidentiality and integrity of the claims.
The SmallRye JWT Build API simplifies securing JWT claims by supporting all these options. It uses the Jose4J library internally to provide this functionality.
2.1. Dependency
To use the SmallRye JWT Build API, add the following dependency to your project:
Using Maven:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
Using Gradle:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow implementation("io.quarkus:quarkus-smallrye-jwt-build")
implementation("io.quarkus:quarkus-smallrye-jwt-build")
You can use the SmallRye JWT Build API independently, without creating MicroProfile JWT endpoints supported by the quarkus-smallrye-jwt
extension.
2.2. Create JwtClaimsBuilder and set the claims
The first step is to initialize a JwtClaimsBuilder
by using one of the following options and add some claims to it:
import java.util.Collections; import jakarta.json.Json; import jakarta.json.JsonObject; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.build.JwtClaimsBuilder; import org.eclipse.microprofile.jwt.JsonWebToken; ... // Create an empty builder and add some claims JwtClaimsBuilder builder1 = Jwt.claims(); builder1.claim("customClaim", "custom-value").issuer("https://issuer.org"); // Alternatively, start with claims directly: // JwtClaimsBuilder builder1 = Jwt.upn("Alice"); // Create a builder from an existing claims file JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json"); // Create a builder from a map of claims JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")); // Create a builder from a JsonObject JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build(); JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build(); JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build(); JwtClaimsBuilder builder4 = Jwt.claims(json); // Create a builder from a JsonWebToken @Inject JsonWebToken token; JwtClaimsBuilder builder5 = Jwt.claims(token);
import java.util.Collections;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import org.eclipse.microprofile.jwt.JsonWebToken;
...
// Create an empty builder and add some claims
JwtClaimsBuilder builder1 = Jwt.claims();
builder1.claim("customClaim", "custom-value").issuer("https://issuer.org");
// Alternatively, start with claims directly:
// JwtClaimsBuilder builder1 = Jwt.upn("Alice");
// Create a builder from an existing claims file
JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json");
// Create a builder from a map of claims
JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value"));
// Create a builder from a JsonObject
JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build();
JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build();
JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build();
JwtClaimsBuilder builder4 = Jwt.claims(json);
// Create a builder from a JsonWebToken
@Inject JsonWebToken token;
JwtClaimsBuilder builder5 = Jwt.claims(token);
The API is fluent so you can initialize the builder as part of a fluent sequence.
The builder automatically sets the following claims if they are not explicitly configured:
-
iat
(issued at): Current time -
exp
(expires at): Five minutes from the current time (customizable with thesmallrye.jwt.new-token.lifespan
property) -
jti
(unique token identifier)
You can configure the following properties globally to avoid setting them directly in the builder:
-
smallrye.jwt.new-token.issuer
: Specifies the default issuer. -
smallrye.jwt.new-token.audience
: Specifies the default audience.
After initializing and setting claims, the next step is to decide how to secure the claims.
2.3. Sign the claims
You can sign the claims immediately or after configuring the JSON Web Signature (JWS)
headers:
import io.smallrye.jwt.build.Jwt; ... // Sign the claims using an RSA private key loaded from the location specified by the 'smallrye.jwt.sign.key.location' property. // No 'jws()' transition is required. The default algorithm is RS256. String jwt1 = Jwt.claims("/tokenClaims.json").sign(); // Set the headers and sign the claims by using an RSA private key loaded in the code (the implementation of this method is omitted). // Includes a 'jws()' transition to a 'JwtSignatureBuilder'. The default algorithm is RS256. String jwt2 = Jwt.claims("/tokenClaims.json") .jws() .keyId("kid1") .header("custom-header", "custom-value") .sign(getPrivateKey());
import io.smallrye.jwt.build.Jwt;
...
// Sign the claims using an RSA private key loaded from the location specified by the 'smallrye.jwt.sign.key.location' property.
// No 'jws()' transition is required. The default algorithm is RS256.
String jwt1 = Jwt.claims("/tokenClaims.json").sign();
// Set the headers and sign the claims by using an RSA private key loaded in the code (the implementation of this method is omitted).
// Includes a 'jws()' transition to a 'JwtSignatureBuilder'. The default algorithm is RS256.
String jwt2 = Jwt.claims("/tokenClaims.json")
.jws()
.keyId("kid1")
.header("custom-header", "custom-value")
.sign(getPrivateKey());
Default behaviors:
-
The
alg
(algorithm) header is set toRS256
by default. -
You do not have to set a signing key identifier (
kid
header) if a single JSON Web Key (JWK) containing akid
property is used.
Supported keys and algorithms:
- To sign the claims, you can use RSA private keys, Elliptic Curve (EC) private keys, and symmetric secret keys.
-
RS256
is the default RSA private key signature algorithm. -
ES256
is the default EC private key signature algorithm. -
HS256
is the default symmetric key signature algorithm.
To customize the signature algorithm, use the JwtSignatureBuilder
API. For example:
import io.smallrye.jwt.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; // Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. The algorithm is PS256. String jwt = Jwt.upn("Alice").jws().algorithm(SignatureAlgorithm.PS256).sign();
import io.smallrye.jwt.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;
// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. The algorithm is PS256.
String jwt = Jwt.upn("Alice").jws().algorithm(SignatureAlgorithm.PS256).sign();
Alternatively, you can configure the signature algorithm globally with the following property:
smallrye.jwt.new-token.signature-algorithm=PS256
smallrye.jwt.new-token.signature-algorithm=PS256
This approach gives you a simpler API sequence:
import io.smallrye.jwt.build.Jwt; // Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. The algorithm is PS256. String jwt = Jwt.upn("Alice").sign();
import io.smallrye.jwt.build.Jwt;
// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. The algorithm is PS256.
String jwt = Jwt.upn("Alice").sign();
You can combine the sign
step with the encrypt step to create inner-signed and encrypted
tokens. For more information, see the Sign the claims and encrypt the nested JWT token section.
2.4. Encrypt the claims
You can encrypt claims immediately or after setting the JSON Web Encryption (JWE)
headers, similar to how claims are signed. However, encrypting claims always requires a jwe()
transition to a JwtEncryptionBuilder
because the API is optimized to support signing and inner-signing operations.
import io.smallrye.jwt.build.Jwt; ... // Encrypt the claims using an RSA public key loaded from the location specified by the 'smallrye.jwt.encrypt.key.location' property. // The default key encryption algorithm is RSA-OAEP. String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt(); // Set the headers and encrypt the claims by using an RSA public key loaded in the code (the implementation of this method is omitted). // The default key encryption algorithm is A256KW. String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getSecretKey());
import io.smallrye.jwt.build.Jwt;
...
// Encrypt the claims using an RSA public key loaded from the location specified by the 'smallrye.jwt.encrypt.key.location' property.
// The default key encryption algorithm is RSA-OAEP.
String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt();
// Set the headers and encrypt the claims by using an RSA public key loaded in the code (the implementation of this method is omitted).
// The default key encryption algorithm is A256KW.
String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getSecretKey());
Default behaviors:
-
The
alg
(key management algorithm) header defaults toRSA-OAEP
. -
The
enc
(content encryption) header defaults toA256GCM
.
Supported keys and algorithms:
- You can use RSA public keys, Elliptic Curve (EC) public keys, and symmetric secret keys, to encrypt the claims.
-
RSA-OAEP
is the default RSA public key encryption algorithm. -
ECDH-ES
is the default EC public key encryption algorithm. -
A256KW
is the default symmetric key encryption algorithm.
Note two encryption operations are done when creating an encrypted token:
-
The generated content encryption key is encrypted using the supplied key and a key encryption algorithm such as
RSA-OAEP
. -
The claims are encrypted using the content encryption key and a content encryption algorithm such as
A256GCM
.
You can customize the key and content encryption algorithms by using the JwtEncryptionBuilder
API. For example:
import io.smallrye.jwt.KeyEncryptionAlgorithm; import io.smallrye.jwt.ContentEncryptionAlgorithm; import io.smallrye.jwt.build.Jwt; // Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. // Key encryption algorithm is RSA-OAEP-256. The content encryption algorithm is A256CBC-HS512. String jwt = Jwt.subject("Bob").jwe() .keyAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256) .contentAlgorithm(ContentEncryptionAlgorithm.A256CBC_HS512) .encrypt();
import io.smallrye.jwt.KeyEncryptionAlgorithm;
import io.smallrye.jwt.ContentEncryptionAlgorithm;
import io.smallrye.jwt.build.Jwt;
// Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property.
// Key encryption algorithm is RSA-OAEP-256. The content encryption algorithm is A256CBC-HS512.
String jwt = Jwt.subject("Bob").jwe()
.keyAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256)
.contentAlgorithm(ContentEncryptionAlgorithm.A256CBC_HS512)
.encrypt();
Alternatively, you can configure the algorithms globally by using the following properties:
smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256 smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512
smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256
smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512
This configuration allows for a simpler API sequence:
import io.smallrye.jwt.build.Jwt; // Encrypt the claims by using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. // Key encryption algorithm is RSA-OAEP-256. The content encryption algorithm is A256CBC-HS512. String jwt = Jwt.subject("Bob").encrypt();
import io.smallrye.jwt.build.Jwt;
// Encrypt the claims by using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property.
// Key encryption algorithm is RSA-OAEP-256. The content encryption algorithm is A256CBC-HS512.
String jwt = Jwt.subject("Bob").encrypt();
Recommendations for secure token encryption:
- When a token is directly encrypted with a public RSA or EC key, it cannot be verified which party sent the token. To address this, symmetric secret keys are preferred for direct encryption, especially when using JWT as cookies managed solely by the Quarkus endpoint.
- To encrypt a token with RSA or EC public keys, it is recommended to sign the token first if a signing key is available. For more information, see the Sign the claims and encrypt the nested JWT token section.
2.5. Sign the claims and encrypt the nested JWT token
You can sign the claims and then encrypt the nested JWT token by combining the sign and encrypt steps.
import io.smallrye.jwt.build.Jwt; ... // Sign the claims and encrypt the nested token using the private and public keys loaded from the locations // specified by the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties, respectively. // The signature algorithm is RS256, and the key encryption algorithm is RSA-OAEP-256. String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt();
import io.smallrye.jwt.build.Jwt;
...
// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations
// specified by the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties, respectively.
// The signature algorithm is RS256, and the key encryption algorithm is RSA-OAEP-256.
String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt();
2.6. Fast JWT generation
If the smallrye.jwt.sign.key.location
or smallrye.jwt.encrypt.key.location
properties are set, you can secure existing claims, such as resources, maps, JsonObjects, with a single call:
// More compact than Jwt.claims("/claims.json").sign(); Jwt.sign("/claims.json"); // More compact than Jwt.claims("/claims.json").jwe().encrypt(); Jwt.encrypt("/claims.json"); // More compact than Jwt.claims("/claims.json").innerSign().encrypt(); Jwt.signAndEncrypt("/claims.json");
// More compact than Jwt.claims("/claims.json").sign();
Jwt.sign("/claims.json");
// More compact than Jwt.claims("/claims.json").jwe().encrypt();
Jwt.encrypt("/claims.json");
// More compact than Jwt.claims("/claims.json").innerSign().encrypt();
Jwt.signAndEncrypt("/claims.json");
As mentioned earlier, the following claims are added automatically if they are not already set: iat
(issued at), exp
(expires at), jti
(token identifier), iss
(issuer), and aud
(audience).
2.7. Dealing with the keys
You can use the smallrye.jwt.sign.key.location
and smallrye.jwt.encrypt.key.location
properties to specify the locations of signing and encryption keys. These keys can be located on the local file system, on the classpath, or fetched from remote endpoints. Keys can be in PEM
or JSON Web Key (JWK)
formats. For example:
smallrye.jwt.sign.key.location=privateKey.pem smallrye.jwt.encrypt.key.location=publicKey.pem
smallrye.jwt.sign.key.location=privateKey.pem
smallrye.jwt.encrypt.key.location=publicKey.pem
Alternatively, you can fetch keys from external services, such as HashiCorp Vault or other secret managers, by using MicroProfile ConfigSource
and the smallrye.jwt.sign.key
and smallrye.jwt.encrypt.key
properties:
smallrye.jwt.sign.key=${private.key.from.vault} smallrye.jwt.encrypt.key=${public.key.from.vault}
smallrye.jwt.sign.key=${private.key.from.vault}
smallrye.jwt.encrypt.key=${public.key.from.vault}
In this example, private.key.from.vault
and public.key.from.vault
are PEM
or JWK
formatted key values provided by the custom ConfigSource
.
The smallrye.jwt.sign.key
and smallrye.jwt.encrypt.key
properties can also contain Base64-encoded private or public key values directly.
However, be aware that directly inlining private keys in the configuration is not recommended. Use the smallrye.jwt.sign.key
property only when you need to fetch a signing key value from a remote secret manager.
The keys can also be loaded by the code that builds the token, and then supplied to JWT Build API for token creation.
If you need to sign or encrypt the token by using the symmetric secret key, consider using io.smallrye.jwt.util.KeyUtils
to generate a SecretKey
of the required length.
For example, a 64-byte key is required to sign a token by using the HS512
algorithm (512/8
), and a 32-byte key is needed to encrypt the content encryption key with the A256KW
algorithm (256/8
):
import javax.crypto.SecretKey; import io.smallrye.jwt.KeyEncryptionAlgorithm; import io.smallrye.jwt.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.util.KeyUtils; SecretKey signingKey = KeyUtils.generateSecretKey(SignatureAlgorithm.HS512); SecretKey encryptionKey = KeyUtils.generateSecretKey(KeyEncryptionAlgorithm.A256KW); String jwt = Jwt.claim("sensitiveClaim", getSensitiveClaim()).innerSign(signingKey).encrypt(encryptionKey);
import javax.crypto.SecretKey;
import io.smallrye.jwt.KeyEncryptionAlgorithm;
import io.smallrye.jwt.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.util.KeyUtils;
SecretKey signingKey = KeyUtils.generateSecretKey(SignatureAlgorithm.HS512);
SecretKey encryptionKey = KeyUtils.generateSecretKey(KeyEncryptionAlgorithm.A256KW);
String jwt = Jwt.claim("sensitiveClaim", getSensitiveClaim()).innerSign(signingKey).encrypt(encryptionKey);
You can also consider using a JSON Web Key (JWK) or JSON Web Key Set (JWK Set) format to store a secret key on a secure file system. You can reference the key by using the smallrye.jwt.sign.key.location
or smallrye.jwt.encrypt.key.location
properties.
Example JWK
{ "kty":"oct", "kid":"secretKey", "k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I" }
{
"kty":"oct",
"kid":"secretKey",
"k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
}
Example JWK Set
{ "keys": [ { "kty":"oct", "kid":"secretKey1", "k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I" }, { "kty":"oct", "kid":"secretKey2", "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" } ] }
{
"keys": [
{
"kty":"oct",
"kid":"secretKey1",
"k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
},
{
"kty":"oct",
"kid":"secretKey2",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
]
}
You can also use io.smallrye.jwt.util.KeyUtils
to generate a pair of asymmetric RSA or EC keys. These keys can be stored in JWK
, JWK Set
, or PEM
format.
2.8. SmallRye JWT Builder configuration
SmallRye JWT supports the following properties, which can be used to customize how claims are signed or encrypted:
Property Name | Default | Description |
---|---|---|
|
|
Location of a private key used to sign the claims when either a no-argument |
|
|
Key value used to sign the claims when either a no-argument |
|
| Signing key identifier, checked only when JWK keys are used. |
|
|
Location of the public key used to encrypt claims or the inner JWT when the no-argument |
|
| Relax the validation of the signing keys. |
|
|
Key value used to encrypt the claims or the inner JWT when a no-argument |
|
| Encryption key identifier, checked only when JWK keys are used. |
|
| Relax the validation of the encryption keys. |
|
| Signature algorithm. Checked if the JWT signature builder has not already set the signature algorithm. |
|
| Key encryption algorithm. Checked if the JWT encryption builder has not already set the key encryption algorithm. |
|
| Content encryption algorithm. Checked if the JWT encryption builder has not already set the content encryption algorithm. |
|
|
Token lifespan in seconds used to calculate an |
|
|
Token issuer used to set an |
|
|
Token audience used to set an |
|
|
Set this property to |
|
|
This property can be used to customize a keystore type if either |
|
This property can be used to customize a | |
|
Keystore password. If | |
|
This property must be set to identify the public encryption key that is extracted from | |
|
This property must be set to identify a private signing key if | |
|
This property can be set if a private signing key’s password in |