Eclipse MicroProfile JWT Authentication API

Since 4.1.2.181; 5.181 

Provided version of the API: MicroProfile Authentication API 1.0

Background

The JWT Authentication API was designed to provide application callers with the ability to authenticate themselves using a JWT token. A JWT token is essentially a string of JSON with fields for specifying the caller/user name and the groups the caller is in. To prevent tampering, the JSON token is cryptographically signed. Note that it’s only signed, not fully encrypted. This means that the caller/user name and the groups are in plain text, but because of the signature can’t be changed.

For a full overview of the API, review the documentation for the appropriate release.

A key goal of the specification was to not just provide authentication, but to provide a means of getting access to the JSON token that was used for authentication using CDI based injection, with an abundantly large and rich amount of possible conversions.

MicroProfile JWT Authentication 1.0 was released in MicroProfile 1.2

Version 1.0 of the MicroProfile JWT Authentication specification does not define any means for preemptive authentication, that is, authenticating when the target resource (such as a JAX-RS end-point) is not protected / constrained by a security role. Practically this means that if a JWT token is sent alongside a request to a public / unchecked resource, MicroProfile JWT implementations will ignore it. This limitation will be addressed in a later version of the JWT specification.

Java EE Security and JWT

Payara Server and Payara Micro implement the JWT authentication mechanism as a normal Java EE Security (JSR 375) authentication mechanism. This specifically means that when MP JWT authentication is used on Payara Server or Payara Micro, there’s an HttpAuthenticationMechanism CDI bean enabled that can be intercepted or decorated using the standard CDI APIs. Likewise, there’s an IdentityStore CDI bean enabled, which can be intercepted or decorated too. By providing additional IdentityStore beans, an application can, if needed, augment the JWT identity store, for instance by providing extra roles.

Vending tokens

A JWT token, like most API tokens, is typically vended from a website for use with the services provided by that site. For instance, from Google or Faceone can obtain such a token for usage with their respective APIs.

Though JWT tokens are more universal in format, they are still intended for use with a specific service (website / api). This is encoded in the mandatory iss ("issuer") field. The value of the field can be anything that the intended service can recognize (see below).

For example:

{
    "iss": "fish.payara.example",
    "jti": "a-123",
    "sub": "24400320",
    "aud": "s6BhdRkqt3",
    "upn": "test",
    "groups": [
        "architect",
        "master",
        "leader",
        "dev"
    ]
}

A JWT also must be signed, and in the case of MicroProfile JWT this must be RSASSA-PKCS-v1_5 using the SHA-256 hash algorithm.

One way to do this signing is by first generating an SSH keypair as follows:

  1. Open Terminal

  2. Generate the base key by entering: openssl genrsa -out baseKey.pem

  3. From the base key generate the PKCS#8 private key: openssl pkcs8 -topk8 -inform PEM -in baseKey.pem -out privateKey.pem -nocrypt

  4. And generate the public key: openssl rsa -in baseKey.pem -pubout -outform PEM -out publicKey.pem

Then put privateKey.pem on the root of the classpath (for instance, in a Maven project put it in src/main/resources) and use the following code to create the signed JSON token as a string:

    public static String generateJWTString(String jsonResource) throws Exception {
        byte[] byteBuffer = new byte[16384];
        currentThread().getContextClassLoader()
                       .getResource(jsonResource)
                       .openStream()
                       .read(byteBuffer);

        JSONParser parser = new JSONParser(DEFAULT_PERMISSIVE_MODE);
        JSONObject jwtJson = (JSONObject) parser.parse(byteBuffer);

        long currentTimeInSecs = (System.currentTimeMillis() / 1000);
        long expirationTime = currentTimeInSecs + 1000;

        jwtJson.put(Claims.iat.name(), currentTimeInSecs);
        jwtJson.put(Claims.auth_time.name(), currentTimeInSecs);
        jwtJson.put(Claims.exp.name(), expirationTime);

        SignedJWT signedJWT = new SignedJWT(new JWSHeader
                                            .Builder(RS256)
                                            .keyID("/privateKey.pem")
                                            .type(JWT)
                                            .build(), parse(jwtJson));

        signedJWT.sign(new RSASSASigner(readPrivateKey("privateKey.pem")));

        return signedJWT.serialize();
    }

    public static PrivateKey readPrivateKey(String resourceName) throws Exception {
        byte[] byteBuffer = new byte[16384];
        int length = currentThread().getContextClassLoader()
                                    .getResource(resourceName)
                                    .openStream()
                                    .read(byteBuffer);

        String key = new String(byteBuffer, 0, length).replaceAll("-----BEGIN (.*)-----", "")
                                                      .replaceAll("-----END (.*)----", "")
                                                      .replaceAll("\r\n", "")
                                                      .replaceAll("\n", "")
                                                      .trim();

        return KeyFactory.getInstance("RSA")
                         .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key)));
    }

The code here assumes the location of the raw JSON file on the classpath being passed in, the private key on the root of the classpath and the result as a string. If other input/outputs are needed that should be easy to do using the above code as a starting point.

Accepting tokens

In order to validate the signature of a provided token is valid, an application that uses JWT Authentication has to provide the public key to the MicroProfile JWT Authentication implementation. In Payara this is done by placing the public key such as generated above as publicKey.pem on the root of the application’s classpath. For example, when using a Maven project as src/main/resources/publicKey.pem

Version 1.0 of the MicroProfile JWT Authentication specification does not define how this public key is exactly configured, so applications using MicroProfile JWT Authentication are not fully portable.

Next to providing the public key, an application that uses JWT Authentication has to provide the issuer (corresponding to the iss field in the JSON token) it’s willing to accept. In Payara this is done by placing a properties file named payara-mp-jwt.properties on the root of the application’s classpath. For example, when using a Maven project as src/main/resources/publicKey.pem. This properties file should contain the key accepted.issuer with as value the same value that of the iss field in the vended token, e.g. fish.payara.example as per the example JSON token shown above.

Version 1.0 of the MicroProfile JWT Authentication specification does not define how this accepted issuer is exactly configured, so applications using MicroProfile JWT Authentication are not fully portable.

Activating JWT Authentication

An application activates the JWT authentication mechanism and identity store by annotating a class in the application, for instance, the JAX-RS Application class, with @LoginConfig(authMethod = "MP-JWT").

Protecting JAX-RS endpoints

MicroProfile JWT Authentication specifies that JAX-RS endpoints are to be secured by using the javax.annotation.security.RolesAllowed annotation. Note that while this is a general annotation, in Java EE it’s only EJB that interprets this. JAX-RS itself does not specify that this annotation should work on resource classes or methods and hence most implementations do not support it out of the box. Both Payara Server and Payara Micro do support it out of the box since 4.1.2.181 and 5.181. This support holds for all types of authentication mechanisms, e.g. BASIC, and not just JWT.

For example:

@ApplicationScoped
@Path("/resource")
@Produces(TEXT_PLAIN)
public class Resource {

    @Inject
    private Principal principal;

    @GET
    @Path("/protected")
    @RolesAllowed("architect")
    public String protectedResource() {
        return
            "This is a protected resource \n" +
            "web username: " + principal.getName() + "\n";
    }
}

Accessing a protected endpoint

With the generateJWTString() method as presented above and the JWT token residing in a file called jwt-token.json on the classpath, a request to a JWT protected endpoint can be done as follows using the JAX-RS client API:

String response =
            newClient()
                 .target(
                     URI.create(new URL(base, "resource/protected").toExternalForm()))
                 .request(TEXT_PLAIN)
                 .header(AUTHORIZATION, "Bearer " + generateJWTString("jwt-token.json"))
                 .get(String.class);

With base being the context where the application is deployed, e.g. http://example.com/myapp

Switching off @RolesAllowed support in JAX-RS

The out-of-the-box support of @RolesAllowed for JAX-RS resources can be switched off by setting the <jaxrs-roles-allowed-enabled> tag in WEB-INF/glassfish-web.xml to false. For more information see the documentation for the jaxrs-roles-allowed-enabled element.

Full examples

Two full examples of using JWT authentication with a JAX-RS endpoint as well as a Servlet resource are provided here: