Handling Refresh Token with Micronaut 2.0.0RC1

Ruben Mondejar
4 min readJun 16, 2020

Micronaut is evolving in the path to reach the 2.0.0 final version. As expected, necessary changes are made, and one of them is how to handle refresh tokens.

After the launch of the 2.0.0 Release Candidate 1, it seems a pretty good moment to redesign our solutions to work with this mechanism following the new way.

Configuration

First of all, an update on our dependencies is needed. Although mainly changing the micronautVersion variable should be enough, let’s take advantage of the new official Micronaut Launch online service to double-check our dependencies quickly:

In addition, we should review our configuration (application.yml) in order to enable the refresh token:

micronaut:
security:
authentication: bearer
endpoints:
login:
enabled: true
oauth:
enabled: true

token:
jwt:
enabled: true
generator:
access-token:
expiration: 3600
refresh-token:
secret: ...

signatures:
secret:
generator:
secret: ...

Persistence

With this new mechanism, we are able to persist refresh tokens in the place that fits it better in our solution, addressing this at the application level.

Following with the previous project, the idea is to take advantage of our current database, storing the refresh token in our User class thought the UserService and UserRepository :

public class User {

(...)
private String token;
}
public interface UserRepository extends CrudRepository<User, Long> { (...)
Optional<User> findByToken(String token);
}
public class UserService { (...)
public Optional<UserDto> findByRefreshToken(
String refreshToken) {
return usersRepository.findByToken(refreshToken)
.map(userMapper::toDto);
}

public void saveRefreshToken(String username,
String refreshToken) {
usersRepository.findByUsername(username)
.ifPresent(user -> {
user.setToken(refreshToken);
usersRepository.update(user);
});
}
}

Once this first step is done, we can proceed to intercept the refresh token mechanism. The framework is providing for this purpose the RefreshTokenPersistence interface, which requires two methods :

Then, we can implement our own class to handle the persistence of the token:

@Singleton
public class RefreshTokenHandler implements RefreshTokenPersistence{

(...)

@Override
@EventListener
public void persistToken(RefreshTokenGeneratedEvent event) {
userService.saveRefreshToken(
event.getUserDetails().getUsername(),
event.getRefreshToken());
}

@Override
public Publisher<UserDetails> getUserDetails(
String refreshToken) {
return userService.findByRefreshToken(refreshToken)
.map(user ->
Flowable.just(new UserDetails(user.getUsername(),
singletonList(user.getRole()))))
.orElse(Flowable.error(TokenNotFoundException::new));
}
}

Lastly, when the provided is token is not found, we can throw a custom exception :

public class TokenNotFoundException extends RuntimeException { }

Error Handling

In this point, we should be interested to handle how and which errors are produced during the refresh token mechanism, instead of returning internal server errors (500 code), as for example:

  • Bad Request: when the provided token is malformed (not a JWT)
  • Forbidden: when the token is not found on any user entity

Micronaut is offering the first one out of the box with the class OauthErrorResponseExceptionHandler, return bad request code response if something is wrong with the provided token by the client.

For the second case, as we did in the previous section, we are triggering this error returning a publisher that emits our TokenNotFoundException when the token is not found during the getUserDetails method execution.

The last step here is the implementation of a proper handler for this, which after registering on the framework, it will create the HTTP response in that failing case:

@Produces
@Singleton
@Requires(classes = {TokenNotFoundException.class,
ExceptionHandler.class})
public class TokenNotFoundExceptionHandler implements ExceptionHandler<TokenNotFoundException, HttpResponse> {

@Override
public HttpResponse handle(HttpRequest request,
TokenNotFoundException exception) {
return HttpResponse.status(HttpStatus.FORBIDDEN);
}
}

Testing

To verify that everything is in place and behaving as expected, we need to test different scenarios:

def "Verify tokens are working"() {

given: 'User data'
String username = 'user1'
String password = 'password1'

when: 'Login endpoint is called with valid credentials'
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password)
HttpRequest request = HttpRequest.POST('/login', credentials)
HttpResponse<BearerAccessRefreshToken> rsp = client.toBlocking().exchange(request, BearerAccessRefreshToken)
String authToken = rsp?.body()?.accessToken
String refreshToken = rsp?.body()?.refreshToken

then: 'JWT token is returned'
rsp
rsp.status == HttpStatus.OK
rsp.body()
rsp.body().username == username
authToken
refreshToken
JWTParser.parse(authToken) instanceof SignedJWT
JWTParser.parse(authToken).getJWTClaimsSet().getSubject() ==
username

when: 'Refresh endpoint is called'
HttpResponse<AccessRefreshToken> response = client.toBlocking().exchange(HttpRequest.POST('/oauth/access_token',
new TokenRefreshRequest("refresh_token", refreshToken)), AccessRefreshToken)

then: 'A valid access token and the refresh token are returned'
response.status == HttpStatus.OK
response.body().accessToken
response.body().refreshToken == refreshToken
}

def "Trying to refresh with wrong tokens returning errors"() {

when: "using a malformed token"
HttpResponse<AccessRefreshToken> response = client.toBlocking().exchange(HttpRequest.POST('/oauth/access_token',
new TokenRefreshRequest("refresh_token", "notJwtTokenLikeAsItIsExpected")), AccessRefreshToken)

then: "bad request response"
!response
HttpClientResponseException e = thrown(HttpClientResponseException)
e.status == HttpStatus.BAD_REQUEST

when: "using a non existing token"
response = client.toBlocking().exchange(HttpRequest.POST('/oauth/access_token',
new TokenRefreshRequest("refresh_token", "eyJhbGciOiJIUzI1NiJ9.MzZjZWQwNzktNmIxNi00OTNlLTg1ZjEtM2RjZTA4NGJiNWY2._grvHNUjh71cJf_e2VWnAGEUJEyJ61aT-1_vcWpk9lc")), AccessRefreshToken)

then: "forbidden response"
!response
e = thrown(HttpClientResponseException)
e.status == HttpStatus.FORBIDDEN
}
$./gradlew test
BUILD SUCCESSFUL in 16s
4 actionable tasks: 1 executed, 3 up-to-date

Command Line

Finally, although the tests are enough to validate this approach, let’s see how the changes are working on the running app, first invoking the login and after that refreshing the token.

$ ./gradlew run$ curl -d '{"username":"user1", "password":"password1"}' -H "Content-Type: application/json" 
-H "Authorization: Basic dXNlcjE6cGFzc3dvcmQx"
-X POST http://127.0.0.1:8080/login
{"username":"user1","roles":["VIEW"],"access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU5MjE2NDg2Nywicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImV4cCI6MTU5MjE2ODQ2NywiaWF0IjoxNTkyMTY0ODY3fQ.NCc34Ku4L6MBiEH-2mmOWnGI8DAJ5yyWFBAwksQFhHU",
"refresh_token":"eyJhbGciOiJIUzI1NiJ9.MzZjZWQwNzktNmIxNi00OTNlLTg1ZjEtM2RjZTA4NGJiNWY2._grvHNUjh71cJf_e2VWnAGEUJEyJ61aT-1_vcWpk9lc",
"token_type":"Bearer","expires_in":3600}
$ curl -d '{"grant_type":"refresh_token", "refresh_token":"eyJhbGciOiJIUzI1NiJ9.MzZjZWQwNzktNmIxNi00OTNlLTg1ZjEtM2RjZTA4NGJiNWY2._grvHNUjh71cJf_e2VWnAGEUJEyJ61aT-1_vcWpk9lc"}'
-H "'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU5MjE2NDg2Nywicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImV4cCI6MTU5MjE2ODQ2NywiaWF0IjoxNTkyMTY0ODY3fQ.NCc34Ku4L6MBiEH-2mmOWnGI8DAJ5yyWFBAwksQFhHU'"
-X POST http://localhost:8080/oauth/access_token
{"username":"user1","roles":["VIEW"],"access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU5MjE2NTE1OSwicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImV4cCI6MTU5MjE2ODc1OSwiaWF0IjoxNTkyMTY1MTU5fQ.rmTYYMXH4xDPYnviEH_37Bl01-l2Nf9-mzFujTMj_XY",
"refresh_token":"eyJhbGciOiJIUzI1NiJ9.MzZjZWQwNzktNmIxNi00OTNlLTg1ZjEtM2RjZTA4NGJiNWY2._grvHNUjh71cJf_e2VWnAGEUJEyJ61aT-1_vcWpk9lc",
"token_type":"Bearer","expires_in":3600}

Conclusions

In this article, we have seen how to configure and implement the refresh token mechanism in our new Micronaut projects.

Adding more Spock tests helps us to check the possible scenarios and improve our solution with some customization in the error handling side of our solution.

As usual, you can find the final project here: https://github.com/rmondejar/mn-jwt-data

P.S: Special thanks to James Kleeh in the official Micronaut Gitter channel, for the feedback and help to improve and simplify the error handling strategy.

--

--

Ruben Mondejar

Director of Engineering at @getuberall . Childhood with puzzles, Lego, and MSX. PhD, Passionate Dev, and Proud Father