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
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.