This tutorial demonstrates providing a JWT authority server.
The example used in this tutorial has the end points:
The configuration is the following:
with the following objects:
<objects> <managed-object source="net.officefloor.web.jwt.authority.JwtAuthorityManagedObjectSource"> <property name="identity.class" value="net.officefloor.tutorial.jwtauthorityhttpserver.Identity" /> </managed-object> <managed-object source="net.officefloor.tutorial.jwtauthorityhttpserver.InMemoryJwtAuthorityRepositoryManagedObjectSource" /> </objects>
As per the objects above, the JwtAuthorityManagedObjectSource provides the JwtAuthority. This provides the necessary means for creating and refreshing the tokens for a JWT authority server.
This requires persisting the keys and depends on a JwtAuthorityRepository to provide this persistence. This allows a cluster of JWT authority servers sharing the same persistent storage of keys.
Login is specific to the JWT authentication server. It can use it's own store of credentials. It may use third party Open ID servers. Once authenticated, then the tokens may be created:
public static final String REFRESH_TOKEN_COOKIE_NAME = "RefreshToken"; @Data @HttpObject @RequiredArgsConstructor @AllArgsConstructor public static class Credentials { private String username; private String password; } @Data @HttpObject @AllArgsConstructor @RequiredArgsConstructor public static class Token { private String token; } public void login(Credentials credentials, JwtAuthority<Identity> authority, ObjectResponse<Token> response, ServerHttpConnection connection) { // Mock authentication // (production solution would restrict tries and check appropriate user store) // (or use potential OpenId third party login) if ((credentials.getUsername() == null) || (!credentials.getUsername().equals(credentials.getPassword()))) { throw new HttpException(HttpStatus.UNAUTHORIZED); } // Create the refresh token from identity Identity identity = new Identity(credentials.username); RefreshToken refreshToken = authority.createRefreshToken(identity); // Provide refresh token connection.getResponse().getCookies().setCookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken.getToken()) .setHttpOnly(true).setSecure(true).setExpires(Instant.ofEpochSecond(refreshToken.getExpireTime())); // Create the access token Claims claims = createClaims(credentials.username); AccessToken accessToken = authority.createAccessToken(claims); // Send response response.send(new Token(accessToken.getToken())); } private static Claims createClaims(String username) { // Mock claims // (claim information should be pulled from user store) String[] roles = new String[] { "tutorial" }; // Provide random value (so access tokens are different) // (not necessary but due to speed of tests, gets same access token) int randomValue = ThreadLocalRandom.current().nextInt(); // Return the claims return new Claims(username, randomValue, roles); }
Access tokens, as per JWT, should be short lived. Refresh tokens maintain the length of the session for the user. Using a refresh token, access tokens can be obtained until the refresh token expires:
public void refreshAccessToken(ServerHttpConnection connection, JwtAuthority<Identity> authority, ObjectResponse<Token> response) { // Obtain the refresh token HttpRequestCookie cookie = connection.getRequest().getCookies().getCookie(REFRESH_TOKEN_COOKIE_NAME); if (cookie == null) { throw new HttpException(HttpStatus.UNAUTHORIZED); } String refreshToken = cookie.getValue(); // Obtain the identity from refresh token Identity identity = authority.decodeRefreshToken(refreshToken); // Create a new access token Claims claims = createClaims(identity.getId()); AccessToken accessToken = authority.createAccessToken(claims); // Send refreshed access token response.send(new Token(accessToken.getToken())); }
Once the refresh token is expired, the user is likely required to re-authenticate. However, this again is application specific, as refresh tokens may be recreated also.
RFC 7517 defines a format for publishing keys. The tutorial uses the default JwksPublishSectionSource that adheres to this format to publish keys.
The following shows the ease of using the JWT authority:
public class JwtAuthorityHttpServerTest { @RegisterExtension public MockWoofServerExtension server = new MockWoofServerExtension(); private String refreshToken; private String accessToken; @Test public void login() throws Exception { // Undertake login Credentials credentials = new Credentials("daniel", "daniel"); MockWoofResponse response = this.server .send(MockWoofServer.mockJsonRequest(HttpMethod.POST, "/login", credentials).secure(true)); assertEquals(200, response.getStatus().getStatusCode(), "Should be successful"); // Extract the refresh token WritableHttpCookie cookie = response.getCookie(JwtTokens.REFRESH_TOKEN_COOKIE_NAME); assertNotNull(cookie, "Should have refresh token"); // Extract the access token Token accessToken = response.getJson(200, Token.class); assertNotNull(accessToken.getToken(), "Should have access token"); // Capture for other tests this.refreshToken = cookie.getValue(); this.accessToken = accessToken.getToken(); } @Test public void refreshAccessToken() throws Exception { // Undertake login to obtain refresh token this.login(); // Attempt to obtain access token without refresh token MockWoofResponse response = this.server .send(MockWoofServer.mockRequest("/refresh").secure(true).method(HttpMethod.POST)); assertEquals(401, response.getStatus().getStatusCode(), "Should not be authorised"); // Obtain new access token with refresh token response = this.server.send(MockWoofServer.mockRequest("/refresh").secure(true).method(HttpMethod.POST) .cookie(JwtTokens.REFRESH_TOKEN_COOKIE_NAME, this.refreshToken)); assertEquals(200, response.getStatus().getStatusCode(), "Should be successful"); // Extract the access token Token token = response.getJson(200, Token.class); assertNotNull(token.getToken(), "Should have access token"); assertNotEquals(this.accessToken, token.getToken(), "Should be new access token"); } @Test public void jwksPublishing() throws Exception { // Publish keys via JWKS MockWoofResponse response = this.server.send(MockWoofServer.mockRequest("/jwks.json").secure(true)); // Should have two keys available (one active and one in future rotation) JwksKeys keys = response.getJson(200, JwksKeys.class); assertEquals(2, keys.getKeys().size(), "Incorrect number of keys"); } @Data public static class JwksKeys { private List<RsaJwksKey> keys; } @Data public static class RsaJwksKey { // As per RFC 7517 for RSA public key private String kty; private String n; private String e; // Additional to allow rotating keys private Long nbf; // epoch start time in seconds private Long exp; // epoch expire time in seconds } }
The next tutorial covers combining JWT security and JWT authority server together for smaller applications.