Fork me on GitHub

JWT Tutorial

This tutorial demonstrates securing resources with JWT.

The example used in this tutorial is the end point:

  • GET /resource to obtain the secured resource

Tutorial Source

Configuration

The resource is configured with access restriction to prevent unauthorized access. The claims object may also be injected for more specific security logic.

public class SecuredResource {

	@HttpAccess(ifRole = "tutorial")
	public void secure(Claims claims, ServerHttpConnection connection) throws IOException {
		connection.getResponse().getEntityWriter().write("Hello " + claims.getId());
	}

}

The resource path is also configured to require HTTPS:

secure: true

The JWT security is configured with the JwtHttpSecuritySource via the security YAML:

security:
  source: net.officefloor.web.jwt.JwtHttpSecuritySource
  properties:
    claims.class: net.officefloor.tutorial.jwtresourcehttpserver.Claims
  flows:
    EXPIRED_JWT: jwtChallenge_EXPIRED
    INVALID_JWT: jwtChallenge_INVALID
    NO_JWT: jwtChallenge_NO
    RETRIEVE_KEYS: retrieveKeys
    RETRIEVE_ROLES: retrieveRoles
jwtChallenge_EXPIRED:
  source: net.officefloor.web.jwt.DefaultJwtChallengeSectionSource
  input: EXPIRED_JWT
jwtChallenge_INVALID:
  source: net.officefloor.web.jwt.DefaultJwtChallengeSectionSource
  input: INVALID_JWT
jwtChallenge_NO:
  source: net.officefloor.web.jwt.DefaultJwtChallengeSectionSource
  input: NO_JWT
retrieveKeys:
  source: net.officefloor.web.jwt.jwks.JwksSectionSource
  input: retrieveJwtValidateKeys
retrieveRoles:
  class: net.officefloor.tutorial.jwtresourcehttpserver.JwtRoles

Many of the handlers for JWT are already available with default implementations:

These just need to be configured in as per this tutorial.

The claims is application specific so must be coded by the application. The tutorial claims is:

@Data
@RequiredArgsConstructor
@AllArgsConstructor
public class Claims {
	private String id;
	private String[] roles;
}

The translation of JWT claims to roles is also very application specific. Therefore, it needs to be coded specifically for the application. The following is the example tutorial's translation of claims to roles:

public class JwtRoles {

	public void retrieveRoles(@Parameter JwtRoleCollector<Claims> collector) {
		collector.setRoles(Arrays.asList(collector.getClaims().getRoles()));
	}
}

JWKS

RFC 7517 defines a format for publishing keys. The tutorial uses the default JwksSectionSource that adheres to this format to retrieve keys.

It, however, requires connecting to a server to retrieve JWKS content. The following is the mock implementation used by the tutorial. Production implementations would make HTTPS calls to the JWT Authority server to retrieve the keys.

public class MockJwksRetriever implements JwksRetriever {

	@Override
	public InputStream retrieveJwks() throws Exception {

		// For production make HTTPS call to JWT Authority server to obtain JWKS content

		// For tutorial, returning mocked JWKS response
		RSAPublicKey key = (RSAPublicKey) Keys.keyPairFor(SignatureAlgorithm.RS256).getPublic();
		JwksKeys keys = new JwksKeys(Arrays.asList(
				new JwksKey("RSA", base64(key.getModulus()), base64(key.getPublicExponent()), 0, Long.MAX_VALUE)));
		String content = mapper.writeValueAsString(keys);
		return new ByteArrayInputStream(content.getBytes());
	}

	private static ObjectMapper mapper = new ObjectMapper();

	private static String base64(BigInteger value) {
		return Base64.getUrlEncoder().encodeToString(value.toByteArray());
	}

	@Data
	public static class JwksKeys {
		private final List<JwksKey> keys;
	}

	@Data
	public static class JwksKey {
		private final String kty;
		private final String n;
		private final String e;
		private final long nbf;
		private final long exp;
	}

}

Testing

The following shows the ease of testing with JWT security:

public class JwtResourceHttpServerTest {

	// Sets up server to accept created JWT access tokens for testing
	@Order(1)
	@RegisterExtension
	public final MockJwtAccessTokenExtension authority = new MockJwtAccessTokenExtension();

	@Order(2)
	@RegisterExtension
	public final MockWoofServerExtension server = new MockWoofServerExtension();

	@Test
	public void ensureResourceSecured() throws Exception {
		MockHttpResponse response = this.server.send(MockHttpServer.mockRequest("/resource").secure(true));
		response.assertResponse(401, "");
	}

	@Test
	public void accessSecureResource() throws Exception {

		// Create mock access token
		String accessToken = this.authority.createAccessToken(new Claims("daniel", new String[] { "tutorial" }));

		// Access the secured resource
		MockHttpResponse response = this.server.send(
				MockHttpServer.mockRequest("/resource").secure(true).header("authorization", "Bearer " + accessToken));
		response.assertResponse(200, "Hello daniel");
	}

}

JUnit 4 example:

public class JwtResourceHttpServerJUnit4Test {

	// Sets up server to accept created JWT access tokens for testing
	public final MockJwtAccessTokenRule authority = new MockJwtAccessTokenRule();

	public final MockWoofServerRule server = new MockWoofServerRule();

	@Rule
	public final RuleChain orderedRules = RuleChain.outerRule(this.authority).around(this.server);

	@Test
	public void ensureResourceSecured() throws Exception {
		MockHttpResponse response = this.server.send(MockHttpServer.mockRequest("/resource").secure(true));
		response.assertResponse(401, "");
	}

	@Test
	public void accessSecureResource() throws Exception {

		// Create mock access token
		String accessToken = this.authority.createAccessToken(new Claims("daniel", new String[] { "tutorial" }));

		// Access the secured resource
		MockHttpResponse response = this.server.send(
				MockHttpServer.mockRequest("/resource").secure(true).header("authorization", "Bearer " + accessToken));
		response.assertResponse(200, "Hello daniel");
	}

}

Next

The next tutorial covers providing a JWT authority server.