To main content

Apache Shiro: JWT Realm with JJWT

Published by Benjamin Marwell on

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.

Getting a token
# 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:

Example issuing server output
{
  "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 a javax.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:

One additional line in 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:

  1. Deal with the fact that the credentials were already verified early in the Filter and use an AllowAllCredentialsMatcher.

  2. Implement our own check.

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 JwtCheckingCredentialsMatcher.java file. It lacks signature verification for brevity. Inject the JwtParser again using either CDI or other means.

To use it, just add it to shiro.ini likewise:

Adding a JwtChecker to the JwtRealm
[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.

[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:

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

OpenLiberty: MicroProfile Config 2.0

https://openliberty.io/blog/2021/03/31/microprofile-config-2.0.html

jwt.io: Online Debugger and Verifier

https://jwt.io/