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:
-
Open Terminal
-
Generate the base key by entering:
openssl genrsa -out baseKey.pem
-
From the base key generate the PKCS#8 private key:
openssl pkcs8 -topk8 -inform PEM -in baseKey.pem -out privateKey.pem -nocrypt
-
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.