./attributes.adoc :extension-name: SmallRye JWT :mp-jwt: MicroProfile JWT RBAC
This guide explains how your Quarkus application can utilize SmallRye JWT
to verify JSON Web Tokens, represent them as MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken
and provide secured access to the Quarkus HTTP endpoints using Bearer Token Authorization and Role-Based Access Control.
Note
|
Quarkus OpenID Connect quarkus-oidc 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.
|
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 {quickstarts-clone-url}
, or download an {quickstarts-archive-url}[archive].
The solution is located in the security-jwt-quickstart
{quickstarts-tree-url}/security-jwt-quickstart[directory].
First, we need a new project. Create a new project with the following command:
mvn io.quarkus.platform:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=security-jwt-quickstart \
-DclassName="org.acme.security.jwt.TokenSecuredResource" \
-Dpath="/secured" \
-Dextensions="resteasy,resteasy-jackson,smallrye-jwt,smallrye-jwt-build"
cd security-jwt-quickstart
This command generates the Maven project with a REST endpoint and imports the smallrye-jwt
extension, which includes the {mp-jwt} 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:
./mvnw quarkus:add-extension -Dextensions="smallrye-jwt, smallrye-jwt-build"
This will add the following to your pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
Open the src/main/java/org/acme/security/jwt/TokenSecuredResource.java
file and see the following content:
package org.acme.security.jwt;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/secured")
public class TokenSecuredResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
This is a basic REST endpoint that does not have any of the {extension-name} specific features, so let’s add some.
package org.acme.security.jwt;
import java.security.Principal;
import javax.annotation.security.PermitAll;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
public class TokenSecuredResource {
@Inject
JsonWebToken jwt; // (1)
@GET()
@Path("permit-all")
@PermitAll // (2)
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx); // (3)
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) { // (4)
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { // (5)
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName(); // (6)
}
return String.format("hello + %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); // (7)
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
-
Here 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.
-
@PermitAll is a JSR 250 common security annotation that indicates that the given endpoint is accessible by any caller, authenticated or not.
-
Here we inject the JAX-RS SecurityContext to inspect the security state of the call and use a
getResponseString()
function to populate a response string. -
Here we check if the call is insecure by checking the request user/caller
Principal
against null. -
Here we check that the Principal and JsonWebToken have the same name since JsonWebToken does represent the current Principal.
-
Here we get the Principal name.
-
The reply we build up makes use of the caller name, the
isSecure()
andgetAuthenticationScheme()
states of the requestSecurityContext
, and whether a non-nullJsonWebToken
was injected.
Now we are ready to run our application. Use:
./mvnw compile quarkus:dev
and you should see output similar to:
$ ./mvnw compile quarkus:dev
[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, resteasy, resteasy-jackson, 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 http://127.0.0.1:8080/secured/permit-all; echo
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:
package org.acme.security.jwt;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
@RequestScoped
public class TokenSecuredResource {
@Inject
JsonWebToken jwt; // (1)
@GET
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx);
}
@GET
@Path("roles-allowed") // (2)
@RolesAllowed({ "User", "Admin" }) // (3)
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); // (4)
}
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;
}
}
-
Here we inject
JsonWebToken
-
This new endpoint will be located at /secured/roles-allowed
-
@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.
-
Here we build the reply the same way as in the
hello
method but also add a value of the JWTbirthdate
claim by directly calling the injectedJsonWebToken
.
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; echo
to attempt to access the new endpoint. Your output should be:
$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
* 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
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 {extension-name} extension with information on how to validate a JWT, and 2) generating a matching JWT with the appropriate claims.
Create a security-jwt-quickstart/src/main/resources/application.properties
with the following content:
mp.jwt.verify.publickey.location=publicKey.pem #(1)
mp.jwt.verify.issuer=https://example.com/issuer #(2)
quarkus.native.resources.includes=publicKey.pem #(3)
-
We are setting public key location to point to a classpath publicKey.pem location. We will add this key in part B, Adding a Public Key.
-
We are setting the issuer to the URL string
https://example.com/issuer
. -
We are including the public key as a resource in the native executable.
The JWT specification defines various levels of security of JWTs that one can use.
The {mp-jwt} 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/publicKey.pem
file.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----
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
:
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;
public class GenerateToken {
/**
* Generate JWT token
*/
public static void main(String[] args) {
String token =
Jwt.issuer("https://example.com/issuer") // (1)
.upn("[email protected]") // (2)
.groups(new HashSet<>(Arrays.asList("User", "Admin"))) // (3)
.claim(Claims.birthdate.name(), "2001-07-13") // (4)
.sign();
System.out.println(token);
}
}
-
The
iss
claim is the issuer of the JWT. This needs to match the server sidemp.jwt.verify.issuer
. in order for the token to be accepted as valid. -
The
upn
claim is defined by the {mp-jwt} spec as preferred claim to use for thePrincipal
seen via the container security APIs. -
The
group
claim provides the groups and top-level roles associated with the JWT bearer. -
The
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/test/resources/privateKey.pem
:
-----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-----
We will use a smallrye.jwt.sign.key.location
property to point to this private signing key.
Note
|
Generating Keys with OpenSSL
It is also possible to generate a public and private key pair using the OpenSSL command line tool. openssl commands for generating keys
An additional step is needed for generating the private key for converting it into the PKCS#8 format. openssl command for converting private key
You can use the generated pair of keys instead of the keys used in this quickstart. |
Now we can generate a JWT to use with TokenSecuredResource
endpoint. To do this, run the following command:
$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem
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.
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:
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 -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
hello + [email protected], isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
Success! We now have:
-
a non-anonymous caller name of [email protected]
-
an authentication scheme of Bearer
-
a non-null JsonWebToken
-
birthdate claim value
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 {mp-jwt} 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 uses the injected birthdate
claim
(as opposed to getting it from JsonWebToken
):
package org.acme.security.jwt;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
@RequestScoped
public class TokenSecuredResource {
@Inject
JsonWebToken jwt; // (1)
@Inject
@Claim(standard = Claims.birthdate)
String birthdate; // (2)
@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; // (3)
}
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;
}
}
-
Here we inject the JsonWebToken.
-
Here we inject the
birthday
claim asString
- this is why the@RequestScoped
scope is now required. -
Here we use the injected
birthday
claim to build the final reply.
Now generate the token again and run:
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
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
hello + [email protected], isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
As usual, the application can be packaged using ./mvnw clean package
and executed using the target/quarkus-app/quarkus-run.jar
file:
.Runner jar Example
$ ./mvnw clean package
[INFO] Scanning for projects...
...
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO [io.quarkus] (main) Quarkus {quarkus-version} started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jackson, security, smallrye-jwt]
You can also generate the native executable with ./mvnw clean package -Pnative
.
.Native Executable Example
$ ./mvnw clean package -Pnative
[INFO] Scanning for projects...
...
[security-jwt-quickstart-runner:25602] universe: 493.17 ms
[security-jwt-quickstart-runner:25602] (parse): 660.41 ms
[security-jwt-quickstart-runner:25602] (inline): 1,431.10 ms
[security-jwt-quickstart-runner:25602] (compile): 7,301.78 ms
[security-jwt-quickstart-runner:25602] compile: 10,542.16 ms
[security-jwt-quickstart-runner:25602] image: 2,797.62 ms
[security-jwt-quickstart-runner:25602] write: 988.24 ms
[security-jwt-quickstart-runner:25602] [total]: 43,778.16 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 51.500 s
[INFO] Finished at: 2019-03-28T14:30:56-07:00
[INFO] ------------------------------------------------------------------------
$ ./target/security-jwt-quickstart-runner
2019-03-28 14:31:37,315 INFO [io.quarkus] (main) Quarkus 0.12.0 started in 0.006s. Listening on: http://[::]:8080
2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jackson, security, smallrye-jwt]
The solution repository located in the security-jwt-quickstart
{quickstarts-tree-url}/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 JsonWebToken
s 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 {extension-name} extension features.
@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 javax.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 in which case setting @RequestScoped
is not necessary.
Please see MP JWT CDI Injection Requirements for more details.
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
If you need to verify the token signature using the assymetric RSA or Elliptic Curve (EC) key then 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 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"
}
]
}
This secret key JWK will also need to be referred to with smallrye.jwt.verify.key.location
.
smallrye.jwt.verify.algorithm
should be set to HS256
/HS384
/HS512
.
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);
You can also use it to customize the way the token is verified or decrypted. For example, one can supply a local SecretKey
:
import javax.crypto.SecretKey;
import javax.ws.rs.GET;
import javax.ws.rs.core.NewCookie;
import javax.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 {
@Inject JWTParser parser;
private String secret = "AyM1SysPpbyDfgZld3umj1qzKObwVMko";
@GET
@Produces("text/plain")
public Response getUserName(@CookieParam("jwt") String jwtCookie) {
Response response = null;
if (jwtCookie == null) {
// Create a JWT token signed using the 'HS256' algorithm
String newJwtCookie = Jwt.upn("Alice").signWithSecret(secret);
// or create a JWT token encrypted 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
.
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.
io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory
is used by default to parse and verify JWT tokens and convert them to JsonWebToken
principals.
It uses MP JWT
and smallrye-jwt
properties listed in the Configuration
section to verify and customize JWT tokens.
If you need to provide your own factory, for example, to avoid verifying the tokens again which have already been verified by the firewall, then you can either use a ServiceLoader
mechanism by providing a META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory
resource or simply have an Alternative
CDI bean implementation like this one:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.annotation.Priority;
import javax.enterprise.context.ApplicationScoped;
import javax.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());
}
}
}
Please see Token Propagation section about the Bearer access token propagation to the downstream services.
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
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
If you work with Keycloak and 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 Keycloak
section but only change the application.properties
to use MP JWT configuration properties instead:
# 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
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
Add the following dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-jwt</artifactId>
<scope>test</scope>
</dependency>
and write a test code like this one:
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 = "[email protected]")
})
public void testJwtWithClaims() {
RestAssured.when().get("test-security-jwt-claims").then()
.body(is("userJwt:viewer:[email protected]"));
}
}
where ProtectedResource
class may 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");
}
}
Note that @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.
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
If you’d like to skip the token verification when the public endpoint methods are invoked then please disable the proactive authentication.
Note that you can’t access the injected JsonWebToken
in the public methods if the token verification has not been done.
If you work with Quarkus extensions which do not support HTTP
(for example, Quarkus GRPC
) or provide their own extension specific HTTP
support conflicting with the one offered by quarkus-smallrye-jwt
and Vert.x HTTP
(example, Quarkus Amazon Lambda
) and you would like to Parse and Verify JsonWebToken with JWTParser then please use smallrye-jwt
directly instead of quarkus-smallrye-jwt
.
Add this Maven dependency:
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-jwt</artifactId>
</dependency>
and update application.properties
to get all the CDI producers provided by smallrye-jwt
included as follows:
quarkus.index-dependency.smallraye-jwt.group-id=io.smallrye
quarkus.index-dependency.smallraye-jwt.artifact-id=smallrye-jwt
Property Name | Default | Description |
---|---|---|
|
|
The |
|
|
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 |
|
|
Signature algorithm. Set it to |
|
|
Config property allows for an external or internal location of Private Decryption Key to be specified. |
|
|
Config property specifies the value of the |
|
|
Comma separated list of the audiences that a token |
|
|
Set this property if another header such as |
|
|
Name of the cookie containing a token. This property will be effective only if |
SmallRye JWT provides more properties which can be used to customize the token processing:
Property Name | Default | Description |
---|---|---|
|
|
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' will be ignored if this property is set. |
|
|
Signature algorithm. Set it to |
|
|
Set this property to a specific key format such as |
|
|
Relax the validation of the verification keys, setting this property to |
|
|
If this property is enabled then 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 in this case. JWK keys must have a 'x5c' (Base64-encoded X509Certificate) property set. |
|
|
Set this property if another header such as |
|
|
Name of the cookie containing a token. This property will be effective only if |
|
|
Set this property to |
|
|
Comma-separated list containing an alternative single or multiple schemes, for example, |
|
|
Key identifier. If it is set then the verification JWK key as well every JWT token must have a matching |
|
|
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. 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 represents a JSON object name only, example: |
|
|
This property can be used to 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, example: |
|
|
Separator for splitting a string which may contain multiple group values. It will only be used if the |
|
|
This property can be used to 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 will be ignored unless the |
|
|
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 |
|
|
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. |
|
|
Comma separated list of the audiences that a token |
|
|
Comma separated list of the claims that a token must contain. |
|
|
Config property allows for an external or internal location of Private Decryption Key to be specified. This property is deprecated - use 'mp.jwt.decrypt.key.location'. |
|
|
Decryption algorithm. |
|
|
Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching |