In this tutorial you will learn how to secure your JAX-RS endpoints using Apache Shiro and JSON Web Tokens (JWT).
Prerequisites:
Make sure you have read and understand my previous article, Securing JAX-RS endpoints using Apache Shiro.
Java 17. While this tutorial will only make use of Jakarta EE 7 and Eclipse MicroProfile 2.0 features and would run on Java 8, I use Java 17 to keep the code a bit shorter.
A JakartaEE compliant application server. Throughout this tutorial, IBM OpenLiberty will be used. I find it an excellent and easy-to-use Java/Jakarta/MicroProfile application server.
Goals of this Tutorial
By the end of this tutorial, you will have learned how to set up Apache Shiro and integrate JWTs (JSON Web Tokens) using JJWT. There are a couple of reasons why those two libraries have been chosen.
Shiro excels at resolving roles into fine-granular permissions. Few other frameworks can actually do this.
Shiro brings annotations which are (in some sense) more powerful than other annotations:
Jakarta EE’s Security API does not have annotations for JAX-RS.
But on the other hand Shiro lacks expressions in JAX-RS annotations.
… just to name two random but very obvious differences.
JSON Web Tokens transport signed (and therefore trusted) authentication AND authorization information. Even if intercepted, attackers cannot make much use of it for a few reasons:
JWTs have an expiry date. Even if you can extend them, you can usually only do so with a second extension token, and only a few times before they expire for good.
There can be multiple JWTs for your (web) application, based on what functionality you are using at the moment. Even if an attacker intercepts your JWT, they are usually only able to do a very confined range of things with it.
For a showcase, we are setting up a mock JWT issuing server.
Non-Goals for this tutorial
Explain JWTs and how they are created and used, extended etc.
All the best practices. I will mention where I deviate from best practices, but will not be able to show a state-of-the-art implementation for reasons of brevity.
Setting up the project
All the code is in my GitHub repository, bmarwell/shiro-jwt-showcase. A short description of the repository will follow.
»keystore« module
The »keystore« maven module contains maven code to set up a sample keystore (containing the private key for the issuing server) and a truststore where only the public key is present.
»start« server
The »start« directory contains a sample setup. You can run ./mvnw liberty:dev
to start a development server. It is basically the same server as we left it in the previous tutorial. It does not make any use of either the keystore dependency nor the JJWT library.
»issuer« server
There is a server where you can create your JSON web tokens. Find it in the »issuer« directory. You will need to start it to create your JWTs using a CA. It makes use of the keystore module and its private keystore to sign the requested JWT.
# shell 1
./mvnw -pl issuer -am generate-resources liberty:dev
# shell 2
curl \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{ "username": "me", "password": "me" }' \
--url "http://localhost:9081/login?roles=admin"
The output will be something like this:
{
"token": "ewogICAgImFsZyI6ICJFUzI1NiIKfQ.ewogICAgImlzcyI6ICJodHRwOi8vbG9jYWxob3N0OjkwODEvIiwKICAgICJzdWIiOiAibWUiLAogICAgImlhdCI6IDE2NTA5NTk1NTIsCiAgICAibmJmIjogMTY1MDk1OTU1MiwKICAgICJleHAiOiAxNjUxMDE5NTUyLAogICAgImF1ZCI6ICJzaGlyby1qd3QiLAogICAgInJvbGVzIjogWwogICAgICAgICJhZG1pbiIKICAgIF0KfQ.9Ew6X30zq9t6rUlZ6A28kox4_LJN36dqYZ63eQtQ_ezBOpxeo37VNAmlIjScg7HvJ5VQ5VC0qpb4d_LLnhduLA"
}
If you decode the token fields, you will get the following JSON objects (put into an array for syntax highlighting):
[
{
"alg": "ES256"
},
{
"iss": "http://localhost:9081/",
"sub": "me",
"iat": 1650959552,
"nbf": 1650959552,
"exp": 1651019552,
"aud": "shiro-jwt",
"roles": [
"admin"
]
}
]
Hint: you can modify the claims by adding roles to the query parameter with the same name. Of course this is not a real world example!
»finish« server
The »finish« directory contains the finished project for reference.
Inspecting the start server
You might know the start server from my previous article, Securing JAX-RS endpoints using Apache Shiro.
Please notice I removed the user, password and roles configuration from the shiro.ini
file:
[main]
[urls]
/** = noSessionCreation,authcBasic[permissive]
This is the configuration we start with in this tutorial. Further changes include:
Stormtrooper is now a
record
.Added custom (de-)serialization for the record.
There is a
deleteAllStormTroopers
to set up the tests.Most methods in the
StormtrooperResource
now return ajavax.ws.rs.core.Response
so we can control the http status code.
Other than that, this should be your average JAX-RS server.
Digression: Shiro Terms and Features
Before we start implementing our Realm, we need to set up some general terms we use in Apache Shiro.
Shiro HttpAuthenticationFilter
An HttpAuthenticationFilter is a special filter that Shiro will execute before any attempts are made to check the visitor’s permissions or even before trying to log them in. In fact, the authcBasic
filter from above is such an AuthenticatingFilter which implements the Authentication: Basic xxx
header. Its real name is BasicHttpAuthenticationFilter and the authcBasic
is just a pre-defined variable name. All the filters will do is to parse the authentication header for validity and store its contents into the servlet’s request context. This way, the Realm you are authenticating against will be able to log you in.
Shiro Credentials
Credentials are authentication information or authentication data you send to the server. This can be either a username and password token using the above-mentioned authcBasic
filter, or a Bearer token for example. The default implementation saves a POJO UsernamePasswordToken
into the servlet’s request context, which will (obviously) hold the extracted username and password for later verification.
For our example project we are going to create a specialised version of the BearerHttpAuthenticationFilter which will not store a UsernamePasswordToken
, but instead our own ShiroJsonWebToken
. More on that later.
Shiro Token
A Token is a class that is returned from parsing the Credentials
using an HttpAuthenticationFilter
. This can just be the credentials themselves, wrapped in a POJO. Or it could be our own ShiroJsonWebToken
class with some added fields.
Shiro Realm
A Realm is an entity you authenticate against and/or you get your roles and permissions from. There are many realms you can combine multiple realms, but this is a topic for another day. For this reason (allowing multiple realms to be used in a specific manner and/or order), Realms also have a method which checks if they can parse the given Credentials. By default, most Realms will only allow a UsernamePasswordToken
. Since we do not get a Username nor a password from a JWT, we need a special Realm which can extract information from the previously mentioned ShiroJsonWebToken
.
Shiro CredentialsMatcher
Every Realm must make use of a CredentialsMatcher which will check if the given AuthenticationToken (e.g. the UsernamePasswordToken
or the ShiroJsonWebToken
) will match the credentials we authenticate against. This makes a lot of sense when talking about passwords, because we can just apply the password key derivation function again on the clear text password and check if it matches the stored credentials.
However, JWTs usually come as JWS tokens: Signed pieces of JSON data. We can only verify the signature (which is, strictly speaking, not matching).
Shiro RolePermissionResolver
The RolePermissionResolver in Shiro is a service class which can resolve roles into permissions. Shiro’s permissions are probably the most valuable features next to Shiro’s JAX-RS annotations.
So, how do we resolve roles we get from the JWT/JWS into permissions? That highly depends on the applications. You could either query them from a database, a static text file or hard-wire them as we do in this example.
Implementing JWTs in Shiro
The first thing we are going to need is the new Authentication Filter. While we technically could use the existing BearerHttpAuthenticationFilter, but creating a custom JWT filter has a few advantages:
We can extract and store the host and subject separately.
Validation can occur now or later (or both).
We can alter the behaviour compared to other Bearer tokens. This will effectively allow multiple Bearer realms, depending on the type of the Bearer token.
But let’s start with the primary objective: Parsing the supplied JWT using jjwt.
Implementing the JwtParser
The parser is created in the KeyService
class. It contains a specific method public JwtParser createJwtParser()
. While at it, mpConfig
is used in the constructor to introduce the issuer name, which must match as well. The truststore however is loaded from the shiro.jwt.jaxrs.keystore
dependency. It brings its own loader class to avoid complex Classloader handling. This truststore holds the public key of the signing certificate which we need for validation.
In this case, using CDI is a complete overkill. If you are interested in how to use this class with CDI instead, look at this commit.
Credentials Filter: JwtHttpAuthenticator
The actual filter to read the JWT is implemented in JwtHttpAuthenticator.java
. It only implements one method with the signature protected AuthenticationToken createToken(ServletRequest request, ServletResponse response)
. All it does is to parse the Authorization: Bearer
header for the JWT, checks its validity and stores the result into a ShiroJsonWebToken
(a simple POJO).
One special instruction can be seen, however. We create a JWT parser from the KeyStore
class, see above.
To use the filter for all incoming requests, we add a line to shiro.ini
and change the URL filter:
[main]
authcJWT = io.github.bmarwell.shiro.jwt.shiro.JwtHttpAuthenticator
[urls]
/** = noSessionCreation,authcJWT[permissive]
That works in a sense. We do get the ShiroJsonWebToken
in our request context, but there is no Realm to handle it yet.
Implementing the JwtRealm
This is the single most important class Shiro+JWT implementation: The JwtRealm
is reading and actually using data from the user-supplied JWT.
For the JwtRealm
I decided to extend the AuthorizingRealm
class, because it will handle both Authentication and Authorization.
Let’s start with the obvious.
Overriding getName()
This is less important than the other methods. We just need a single, static name for our Realm. If we had multiple JwtRealms (yes, this is possible!), we could have a config value injected or set via shiro.ini
. But for now, a simple static return will do:
@Override
public String getName() {
return "jwt";
}
Overriding getAuthenticationTokenClass()
This is a very important class. If this method would not return ShiroJsonWebToken.class
, our Realm would be skipped. Shiro will only let handle Realms a Token they can understand.
@Override
public Class<?> getAuthenticationTokenClass() {
return ShiroJsonWebToken.class;
}
With this method in place, we can implement doGetAuthenticationInfo()
.
Overriding doGetAuthenticationInfo(AuthenticationToken token)
The first thing we can do in this method is casting the token
to ShiroJsonWebToken
. This is safe and guaranteed by Apache Shiro to work.
From that point on, we can build a SimpleAuthenticationInfo
as we already have all trustworthy information at hand:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
ShiroJsonWebToken jwt = (ShiroJsonWebToken) token;
return new SimpleAuthenticationInfo(jwt.getPrincipal(), jwt, getName());
}
Next, let’s extract the roles from the token.
Overriding doGetAuthorizationInfo(PrincipalCollection principals)
Oops… You might notice, we lost our token! This method signature does not provide the AuthenticationToken to the method. Shiro was built for applications where – after authentication – the authorization info was available with just the principal name (read: UserName) at hand. This obviously doesn’t work for a JWT, because in their case the token already contains the authorization information.
So, stepping back a little, how can we work around this? Well, at this point you will need some special Java and Shiro knowledge.
Apache Shiro will call both methods in the same thread. This already helps us. We can declare a static ThreadLocal
field, which will hold information even over different instances of our JwtRealm, as long as they are being called from the same Thread. So let’s start with declaring such a field.
public class JwtRealm extends AuthorizingRealm {
private static final ThreadLocal<ShiroJsonWebToken> jwtThreadToken = new ThreadLocal<>();
}
Now let’s get back to the doGetAuthenticationInfo()
method we implemented above. It needs to store the token for later retrieval:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
ShiroJsonWebToken jwt = (ShiroJsonWebToken) token;
jwtThreadToken.set(jwt);
return new SimpleAuthenticationInfo(jwt.getPrincipal(), jwt, getName());
}
NOW we have all the information we need to implement the doGetAuthorizationInfo(PrincipalCollection principals)
method:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
final ShiroJsonWebToken jsonWebToken = jwtThreadToken.get();
final Claims claims = jsonWebToken.getCredentials().getBody();
final List<String> roles = Optional.ofNullable(claims.get("roles", List.class)).orElse(List.<String>of());
return new SimpleAuthorizationInfo(Set.copyOf(roles));
}
We are just ignoring the principals in this code. To make extra sure we are using the correct roles for the correct subject, we could compare the first (primary) principal with the subject form the ShiroJsonWebToken
. This step is skipped for brevity.
Activating JwtRealm
Now, let’s use the JwtRealm:
shiro.ini
for the JwtRealm[main]
jwtRealm = io.github.bmarwell.shiro.jwt.shiro.JwtRealm
… and voilà, our Realm gets the ShiroJsonWebToken
and can log us in. Can it?
CredentialsMatcher for JWTs
No, we cannot log in yet. Each Realm gets a default CredentialsMatcher class which compares the given Token with the token from the generated AuthenticationInfo. Although they are equal in a sense of being a pointer to the same object, the JWS class does not implement an equals
method. Thus, the credential comparison fails and we are not logged in.
To fix this, you have two options:
Deal with the fact that the credentials were already verified early in the Filter and use an
AllowAllCredentialsMatcher
.Implement our own check.
Disabling the CredentialsMatcher (not recommended)
Anti-Pattern: Skipping credential verification Not verifying credentials is an antipattern. We can only do this because we know we verified that very same token earlier in our filter. But as applications are becoming more complex, you might get different tokens from different sources which are less trustworth. |
To use option #1, you can use this code snippet:
[main]
jwtRealm = io.github.bmarwell.shiro.jwt.shiro.JwtRealm
anyMatcher = org.apache.shiro.authc.credential.AllowAllCredentialsMatcher
jwtRealm.credentialsMatcher = $anyMatcher
Implementing a JwtCredentialsMatcher
The class JwtCheckingCredentialsMatcher.java
implements the credential check by comparing their fields to one and another. This is basically the missing equals
method in the JWS class. However, it is still not re-verifying the token when tokens could come unverified from different sources, not just our own JwtHttpAuthenticator.
Do not just copy this class! Please do not copy the |
To use it, just add it to shiro.ini likewise:
[main]
jwtRealm = io.github.bmarwell.shiro.jwt.shiro.JwtRealm
jwtChecker = io.github.bmarwell.shiro.jwt.shiro.JwtCheckingCredentialsMatcher
jwtRealm.credentialsMatcher = $jwtChecker
The CredentialsMatcher I implemented will do the following things:
Check if it previously has been validated. This is important, since we already got the roles from it.
Re-Parse the token (that is a potential performance issue!) and compare the fields header, body and signature for equality.
Conclusion
That’s about it! To wrap it up, we had to create three classes: * A new HttpAuthenticationFilter which intercepts, verifies and wraps the signed JWT into the ShiroJsonWebToken
. * The Realm which can parse the ShiroJsonWebToken
* optionally a CredentialsMatcher which re-checks the signature and JWT. * optionally a RolePermissionResolver class if you use permissions rather than roles.
This is our final shiro.ini
configuration file:
[main]
jwtRealm = io.github.bmarwell.shiro.jwt.shiro.JwtRealm
jwtChecker = io.github.bmarwell.shiro.jwt.shiro.JwtCheckingCredentialsMatcher
jwtRealm.credentialsMatcher = $jwtChecker
jwtStaticRolePermissionResolver = io.github.bmarwell.shiro.jwt.shiro.StaticJwtRolePermissionResolver
jwtRealm.rolePermissionResolver = $jwtStaticRolePermissionResolver
authcJWT = io.github.bmarwell.shiro.jwt.shiro.JwtHttpAuthenticator
[urls]
/** = noSessionCreation,authcJWT[permissive]
If you leave out the optional bits, we are down to two classes and 4 additional lines. I hope this was not too hard to understand. If you have any questions, feel free to reach out to us on Twitter:
@ApacheShiro – the official Apache Shiro Twitter account.
@bmarwell – me
@briandemers – Brian Demers, who wrote the original JAX-RS example.
Or reach out to us on the Apache Shiro user mailing list.
Fun fact: Both Apache Shiro and JJWT were started by Les Hazlewood.
Further reading
- IBM: Configuring the MicroProfile JSON Web Token
https://www.ibm.com/docs/en/was-liberty/base?topic=liberty-configuring-microprofile-json-web-token
- Open Liberty: MicroProfile Config 2.0
https://openliberty.io/blog/2021/03/31/microprofile-config-2.0.html
- jwt.io: Online Debugger and Verifier