In today's microservice architecture security is usually based on the following protocols: OAuth2, OpenID Connect, and SAML. These main security protocols use security tokens to propagate the security state from client to server. This stateless approach is usually achieved by passing a JWT token alongside every client request. For convenient use of this kind of token-based authentication, the Eclipse MicroProfile JWT Auth evolved. The specification ensures, that the security token is extracted from the request, validated and a security context is created out of the extracted information.
Learn more about the MicroProfile JWT Auth specification, its annotations, and how to use it in this blog post.
Specification profile: MicroProfile JWT Auth
- Current version: 1.1 in MicroProfile
- GitHub repository
- Latest specification document
- Basic use case: Provide JWT token-based authentication for your application
Securing a JAX-RS application
First, we have to instruct our JAX-RS application, that we'll use the JWTs for authentication and authorization. You can configure this with the @LoginConfig
annotation:
1 2 3 4 | @ApplicationPath("resources") @LoginConfig(authMethod = "MP-JWT") public class JAXRSConfiguration extends Application { } |
Once an incoming request has a valid JWT within the HTTP Bearer header, the groups in the JWT are mapped to roles.
We can now limit the access for a resource to specific roles and achieve authorization with the Common Security Annotations (JSR-250) (@RolesAllowed
, @PermitAll
, @DenyAll
):
1 2 3 4 5 6 7 8 9 10 11 | @GET @RolesAllowed("admin") public Response getBook() { JsonObject secretBook = Json.createObjectBuilder() .add("title", "secret") .add("author", "duke") .build(); return Response.ok(secretBook).build(); } |
Furthermore, we can inject the actual JWT token (alongside the Principal
) with CDI and inject any claim of the JWT in addition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @Path("books") @RequestScoped @Produces(MediaType.APPLICATION_JSON) public class BookResource { @Inject private Principal principal; @Inject private JsonWebToken jsonWebToken; @Inject @Claim("administrator_id") private JsonNumber administrator_id; @GET @RolesAllowed("admin") public Response getBook() { System.out.println("Secret book for " + principal.getName() + " with roles " + jsonWebToken.getGroups()); System.out.println("Administrator level: " + jsonWebToken.getClaim("administrator_level").toString()); System.out.println("Administrator id: " + administrator_id); JsonObject secretBook = Json.createObjectBuilder() .add("title", "secret") .add("author", "duke") .build(); return Response.ok(secretBook).build(); } } |
In this example, I'm injecting the claim administrator_id
and access the claim administrator_level
via the JWT token. These are not part of the standard JWT claims but you can add any additional metadata in your token.
Always make sure to only inject the JWT token and the claims to @RequestScoped
CDI beans, as you'll get a DeploymentExcpetion
otherwise:
1 2 3 4 5 | javax.enterprise.inject.spi.DeploymentException: CWWKS5603E: The claim cannot be injected into the [BackedAnnotatedField] @Inject @Claim private de.rieckpil.blog.BookResource.administrator_id injection point for the ApplicationScoped or SessionScoped scopes. at com.ibm.ws.security.mp.jwt.cdi.JwtCDIExtension.processInjectionTarget(JwtCDIExtension.java:92) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) |
HINT: Depending on the application server you'll deploy this example, you might have to first declare the available roles with @DeclareRoles({"admin", "chief", "duke"})
.
Required configuration for MicroProfile JWT Auth
Achieving validation of the JWT signature requires the public key. Since MicroProfile JWT Auth 1.1, we can configure this with MicroProfile Config (previously it was vendor-specific). The JWT Auth specification allows the following public key formats:
- PKCS#8 (Public Key Cryptography Standards #8 PEM)
- JWK (JSON Web Key)
- JWKS (JSON Web Key Set)
- JWK Base64 URL encoded
- JWKS Base64 URL encoded
For this example, I'm using the PKCS#8 format and specify the path of the .pem
file containing the public key in the microprofile-config.properties
file:
1 2 | mp.jwt.verify.publickey.location=/META-INF/publicKey.pem mp.jwt.verify.issuer=rieckpil |
The configuration of the issuer
is also required and has to match the iss
claim in the JWT. A valid publicKey.pem
file might look like the following:
1 2 3 | -----BEGIN RSA PUBLIC KEY----- YOUR_PUBLIC_KEY -----END RSA PUBLIC KEY----- |
Using JWTEnizer to create tokens for testing
Usually, an identity provider (e.g. Keycloak) issues a JWT. For quick testing, we can use the JWTenizer tool from Adam Bien. This provides a simple way to create a valid JWT token and generates the corresponding public and private keys. Once you downloaded the jwtenizer.jar
you can run it for the first time with the following command:
1 | java -jar jwtenizer.jar |
This will now create a jwt-token.json
file in the folder you executed the command above. We can adjust this .json
file to our needs and model a sample JWT token:
1 2 3 4 5 6 7 8 9 10 11 12 13 | { "iss": "rieckpil", "jti": "42", "sub": "duke", "upn": "duke", "groups": [ "chief", "hacker", "admin" ], "administrator_id": 42, "administrator_level": "HIGH" } |
Once you adjusted the raw jwt-token.json
, you can run java -jar jwtenizer.jar
again and this second run will now pick the existing .json
file for creating the JWT. Alongside the JWT token, the tool generates a microprofile-config.properties
file, from which we can copy the public key and paste it to our publicKey.pem
file.
Furthermore the shell output of running jwtenizer.jar
contains a cURL command we can use to hit our resources:
1 | curl -i -H'Authorization: Bearer GENERATED_JWT' http://localhost:9080/resources/books |
With a valid Bearer header you should get the following response from the backend:
1 2 3 4 5 6 7 8 | HTTP/1.1 200 OK X-Powered-By: Servlet/4.0 Content-Type: application/json Date: Fri, 06 Sep 2019 03:24:16 GMT Content-Language: en-US Content-Length: 34 {"title":"secret","author":"duke"} |
You can now adjust the jwt-token.json
again and remove the admin
group and generate a new JWT. With this generated token you shouldn't be able to get a response from the backend. Rather receive 403 Forbidden, as you are authenticated but don't have the correct role.
For further instructions on how to use this tool, have a look at the README on GitHub or the following video of Adam Bien.
YouTube video for using MicroProfile JWT Auth
Watch the following YouTube video of my Getting started with MicroProfile series to see MicroProfile JWT Auth in action:
You can find the source code with further instructions to run this example on GitHub.
Have fun using MicroProfile JWT Auth,
Phil