5. Implement simple and reliable JWT authentication with Java Micronaut

In this chapter, we implement Java JWT Authentication in our Micronaut project + check against user table.

You can find the summary of this post in this youtube video

Implement JWT Authentication with Micronaut

In the previous chapter, we’ve set up Micronaut with Postgres db persistence and few other base libraries.

The github repository is updated with all changes for a full fledged template ready to use https://github.com/yazoo321/micronaut_template

This implementation is created with the help of this Micronaut documentation document, the changes include authentication based on Postgres users table + tests.

Let’s go step by step, first we add the following to our build.gradle file:

    // security
    compileOnly "io.micronaut.security:micronaut-security-annotations"
    implementation "io.micronaut.security:micronaut-security-jwt"

Then navigate to src/main/resources/application.yml and add security references:

micronaut:
  security:
    authentication: bearer  
    token:
      jwt:
        signatures:
          secret:
            generator: 
              secret: pleaseChangeThisSecretForANewOne 

This part is very cool, it configures micronaut to add security and specifies that authentication can be done with Bearer token. Furthermore you specify the JWT secret key. Note, this key is very important to keep safe. So apply it in encrypted file or store as environment variable in memory, whatever you do, just keep it secure.

Authentication Provider

Now we create the authentication provider, this will look similar to the one in tutorial, but it’s different. We’re going to authenticate against our users table in Postgres now.

We’re going to create this file: java/server/security/AuthenticationProviderUserPassword.java

package server.security;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.*;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;
import server.account.repository.AccountRepository;

import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
public class AuthenticationProviderUserPassword implements AuthenticationProvider {

    @Inject
    AccountRepository accountRepository;

    @Override
    public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) {
        return Flowable.create(emitter -> {

            String username = (String) authenticationRequest.getIdentity();
            String pw = (String) authenticationRequest.getSecret();
            // consider sanitisation
            boolean validCredentials = accountRepository.validCredentials(username, pw);

            if (validCredentials) {
                emitter.onNext(new UserDetails(username, accountRepository.getRolesForUser(username)));
                emitter.onComplete();
            } else {
                emitter.onError(new AuthenticationException(new AuthenticationFailed()));
            }
        }, BackpressureStrategy.ERROR);
    }
}

So what does this do? We’re simply creating a class which implements the AuthenticationProvider interface. The only thing we need to worry about is authenticate function.

Unlike in the tutorial, we’re going to inject our repository into this class and use it to cross reference the credentials data. e.g. does user exist where username = ? and password = ?.

If the credentials are valid, we also do a lookup for the roles for the user and link it.

new UserDetails(username, accountRepository.getRolesForUser(username))

Remember we’ve added a migration file with a seeded user specifically for this test;

insert into users(username, email, password, enabled, created_at, updated_at, last_logged_in_at) values ('username', 'email', 'password', true, NOW(), NOW(), NOW());

insert into user_roles(username, role) values ('username', 'ROLE_USER');

I’ve modified data here very slightly to make it a bit more readable. Instead of using these migrations, you should consider just creating a (unsecured) registration endpoint, but I’d like to keep things minimal as different people will have different requirements for registering (e.g. different user models etc).

Seeding only admin users can be standard.

Now you can add a secured annotation to your controllers to make them require user to be authenticated in order to access the resource.

e.g.

@Secured(SecurityRule.IS_AUTHENTICATED)
@Controller("/account")
public class AccountController {
...

The IS_AUTHENTICATED only requires that the user has credentials – it does not check what role the user has.

Logging in

What’s cool is that you don’t even need to create a dedicated login controller – that’s actually coming out of the box with all of the above done. There’s now an available login POST mapping which will use the authentication provider that you’ve created in order to authenticate the user. Furthermore, this is also used if you’re using basic (credentials) authentication in headers – this is covered in the tests below.

Test JWT

Tests are very important to ensure features are working as expected, let’s create the following file java/server/security/JwtAuthenticationTest.java

package server.security;

import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.security.authentication.UsernamePasswordCredentials;
import io.micronaut.security.token.jwt.render.BearerAccessRefreshToken;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.reactivex.Flowable;
import org.jooq.DSLContext;
import org.junit.jupiter.api.*;
import server.account.dto.Account;

import javax.inject.Inject;
import java.text.ParseException;
import java.time.LocalDateTime;

import static com.org.mmo_server.repository.model.tables.UserRoles.*;
import static com.org.mmo_server.repository.model.tables.Users.USERS;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@MicronautTest
public class JwtAuthenticationTest {

    @Inject
    EmbeddedServer embeddedServer;
    RxHttpClient client;

    @Inject
    DSLContext dslContext;

    private final static String GET_USER_PATH = "/account/get-user?username=username";
    private static final String VALID_USER = "username";
    private static final String VALID_PW = "password";

    @BeforeAll
    void setupDatabase() {
        LocalDateTime now = LocalDateTime.now();
        dslContext.insertInto(USERS)
                .columns(USERS.USERNAME, USERS.EMAIL, USERS.PASSWORD,
                        USERS.ENABLED, USERS.CREATED_AT, USERS.UPDATED_AT,
                        USERS.LAST_LOGGED_IN_AT)
                .values("username", "email", "password",
                        true, now, now, now);

        dslContext.insertInto(USER_ROLES)
                .columns(USER_ROLES.USERNAME, USER_ROLES.ROLE)
                .values("username", "role");

        client  = embeddedServer.getApplicationContext()
                .createBean(RxHttpClient.class, embeddedServer.getURL());
    }

    @AfterAll
    void cleanUp() {
        embeddedServer.stop();
        client.stop();
    }

    @Test
    void testProtectedEndpointThrowsOnUnauthorized() {
        // when
        Flowable<Account> response = client.retrieve(
                HttpRequest.GET(GET_USER_PATH), Account.class
        );

        // then
        Assertions.assertThrows(HttpClientResponseException.class, response::blockingFirst);
    }

    @Test
    void testProtectedEndpointReturnsExpectedWhenAuthorizedWithBasicAuth() {
        // when
        Flowable<Account> response = client.retrieve(
                HttpRequest.GET(GET_USER_PATH).basicAuth(VALID_USER, VALID_PW), Account.class);

        // then
        Assertions.assertEquals("username", response.blockingFirst().getUsername());
        Assertions.assertEquals("email", response.blockingFirst().getEmail());
    }

    @Test
    void testLoginRequestWorkingAsExpected() throws ParseException {
        // given
        UsernamePasswordCredentials creds = new UsernamePasswordCredentials("username", "password");

        // when
        HttpRequest request = HttpRequest.POST("/login", creds);
        HttpResponse<BearerAccessRefreshToken> rsp =
                client.toBlocking().exchange(request, BearerAccessRefreshToken.class);

        // then
        Assertions.assertEquals(HttpStatus.OK, rsp.getStatus());
        BearerAccessRefreshToken bearerAccessRefreshToken = rsp.body();
        Assertions.assertEquals("username", bearerAccessRefreshToken.getUsername());
        Assertions.assertNotNull(bearerAccessRefreshToken.getAccessToken());
        Assertions.assertTrue(JWTParser.parse(bearerAccessRefreshToken.getAccessToken()) instanceof SignedJWT);
    }

    @Test
    void testLoginAccessTokenCanBeUsedOnSecuredEndpoint() {
        // given
        UsernamePasswordCredentials creds = new UsernamePasswordCredentials("username", "password");

        // when
        HttpRequest request = HttpRequest.POST("/login", creds);
        HttpResponse<BearerAccessRefreshToken> rsp =
                client.toBlocking().exchange(request, BearerAccessRefreshToken.class);

        BearerAccessRefreshToken bearerAccessRefreshToken = rsp.body();
        String accessToken = bearerAccessRefreshToken.getAccessToken();

        Flowable<Account> response = client.retrieve(
                HttpRequest.GET(GET_USER_PATH).bearerAuth(accessToken), Account.class);

        Assertions.assertEquals("username", response.blockingFirst().getUsername());
        Assertions.assertEquals("email", response.blockingFirst().getEmail());
    }

}

I will go through just the last test to describe main desired behaviour:

HttpRequest request = HttpRequest.POST("/login", creds);
HttpResponse<BearerAccessRefreshToken> rsp =
    client.toBlocking().exchange(request, BearerAccessRefreshToken.class);

First, we post to our standard out of the box /login endpoint with our credentials. If the user exists (using our authentication provider) then we get a response with a bearer token.

We then apply this token to our next API call, to get user.

Flowable<Account> response = client.retrieve(
    HttpRequest.GET(GET_USER_PATH).bearerAuth(accessToken), Account.class);

Assertions.assertEquals("username", response.blockingFirst().getUsername());
Assertions.assertEquals("email", response.blockingFirst().getEmail());

This ensures that when we make a GET request and set the bearer authentication token with one received from /login request, then the request is processed as expected.

We can also run the service locally and use Postman to confirm.

Copy the access token from the response and apply it to the get user request

The above just creates a header with content Authorization: Bearer <token> so you may use that instead.

3 Comments

Comments are closed