Managing JWT Auth with Micronaut v2 (Part 1)
This is the first part on my journey creating a backend at fast-pace with Micronaut, starting with the initial API that covers JWT, user login, and sign-up mechanisms.
Context
Micronaut was released during the Greach 2018 and I had the pleasure to be there in life for those presentations. I’m not here to repeat the great benefits of using it or how well it has evolved, just check the official documentation, which is awesome by the way.
Since I started playing with Flutter recently, instead of using Firebase as a backend, I saw the perfect opportunity to play again with Micronaut early adopting the new 2.0.0 version. Furthermore, we could add more fun by using two essential libraries: Micronaut Data and Micronaut Security.
Follow me then into this first task, creating from scratch a lightweight Java backend, which will provide us a reactive API with JWT support, enabling in our frontends user login and sign-up mechanisms.
Requirements
The main requirements for this initial task are the following :
- Reactive Java REST API
- Database support with JPA
- JWT and Basic Authentication
- Login REST endpoint
- Sign-up REST endpoint
Lucky us, since we are using a super handy framework, most of the job is already provided out-of-the-box by Micronaut and its libraries.
Let’s start our project
First of all, we need to choose our languages and tools, which is pretty flexible when working with Micronaut. Freedom feels great here.
Hence, my personal choice in terms of JVM languages for the project is Java (plus Lombok) for the main code and Groovy (Spock) on the testing side. The last choice is the building tool, which in this case is Gradle.
Clarified that, let’s proceed with the few installation steps that we need, using SDKMAN! and Micronaut CLI:
sdk install java 13.0.2.j9-adptsdk install micronaut 2.0.0.M2mn create-app --features security-jwt,data-hibernate-jpa,spock mn-jwt-data
| Generating Java project…
| Application created at /workspace/java/mn-jwt-data
Nice! Just one addition from my side here: the Lombok dependencies into the build.gradle following the official guide:
ext {
(...)
lombokVersion = "1.18.12"
}
dependencies {
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"(...)
That’s all for the initial skeleton, but if you want to take a sneak peek at the final project, it is available in this repository: https://github.com/rmondejar/mn-jwt-data
Initial REST API
For this first stage on our project, we are going to build the following API:
- POST /login [headers:{authorization:basic, content-type:json}][body:{username, password}]
- POST /signup [headers:{content-type:json}][body:{username,password}]
Adding User Data
Although Micronaut Data gives us support for multiple modes, having PostgreSQL in mind for production, let’s set this up with JPA, Hibernate, and H2 as an in-memory database.
- The first step is to implement the user entity, the corresponding DTO, and a mapper to convert between them :
@NoArgsConstructor
@Data
@Builder
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@Builder.Default
private String role = "VIEW";
}@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class UserDto {
@NotBlank
private String username;
@NotBlank
private String password;
private String role;
}@Singleton
public class UserMapper {
public User toEntity(UserDto userDto) {
return User.builder().username(userDto.getUsername()).password(userDto.getPassword()).role(userDto.getRole()).build();
}
public UserDto toDto(User user) {
return UserDto.builder().username(user.getUsername()).password(user.getPassword().role(user.getRole()).build();
}
}
2. The second step in this implementation is to create the repository and the service to deal with them:
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUsername(String username);
}@Singleton
public class UserService {
(...)
public UserDto createUser(UserDto userDto) {
User user = usersRepository.save(userMapper.toEntity(userDto));
return userMapper.toDto(user);
}
public Optional<UserDto> findUser(String username) {
return usersRepository.findByUsername(username).map(userMapper::toDto);
}
}
3. Finally, we can include a simple way to load some data, basically three dummy users, just for testing purposes:
@Singleton
public class DataLoader implements ApplicationEventListener<ServerStartupEvent> { (...) @Override
public void onApplicationEvent(ServerStartupEvent event) { usersRepository.saveAll(Arrays.asList(
User.builder().username("user1")
.password("password1").build(),
User.builder().username("user2")
.password("password2").build(),
User.builder().username("user3")
.password("password3").build()
));
}
}
Using Authentication
Alternatively, we could start this exercise by just adding mock data into the configuration, but in the end, our goal is to have a database behind the scenes. Therefore, we are ready to retrieve and create users, which is great to continue:
1. Once Micronaut Security is operating, the login endpoint is automatically working, but the remaining step now is to implement our logic inside this provided interceptor:
@Singleton
public class BasicAuthProvider implements AuthenticationProvider {
(...)
@Override
public Publisher<AuthenticationResponse> authenticate(
HttpRequest httpReq, AuthenticationRequest authReq) {
final String username = authReq.getIdentity().toString();
final String password = authReq.getSecret().toString();
Optional<User> existingUser =
userService.findUser(username);
return Flowable.just(
existingUser.map( user -> {
if (user.getPassword().equals(password)) {
return new UserDetails(username,
singletonList(user.getRole()));
}
return new
AuthenticationFailed(CREDENTIALS_DO_NOT_MATCH);
})
.orElse(new AuthenticationFailed(USER_NOT_FOUND))
);
}
}
2. Next, the sign-up process requires to implement our first controller, but avoiding authentication in this special case:
@Controller("/signup")
@Secured(SecurityRule.IS_ANONYMOUS)
public class SignUpController { (...) @Post
public Single<HttpResponse> registerUser(UserDto userDto) {
Optional<UserDto> existingUser =
userService.findUser(userDto.getUsername());
return Single.just(existingUser
.map(HttpResponse::badRequest)
.orElse(HttpResponse.ok(userService.createUser(userDto)))
);
}
}
Testing
The last step in this initial stage is to check how our brand new backend is behaving depending on the different client requests.
- First, as promised we are using Spock as a testing tool, and we can execute the current testbed using the corresponding Gradle command :
$ ./gradlew test
Instead of reviewing the full testbed, let’s focus on the everything-is-going-well scenario for both endpoints :
def "Login with an existing user correctly"() {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> response = client.toBlocking().exchange(request, BearerAccessRefreshToken)then: 'Login endpoint can be accessed'
response.status == HttpStatus.OK
response.body().username == username
response.body().accessToken
JWTParser.parse(response.body().accessToken) instanceof SignedJWT
response.body().refreshToken
JWTParser.parse(response.body().refreshToken) instanceof SignedJWT}def "Sign-up with a non existing user correctly"() {given: 'User data'
String username = 'user4'
String password = 'password4'
when: 'Sign-up endpoint is called with a payload'
HttpRequest request = HttpRequest.POST('/signup', UserDto.builder().username(username).password(password).build())
HttpResponse<BearerAccessRefreshToken> response = client.toBlocking().exchange(request, BearerAccessRefreshToken)
then: 'The user has been created'
response.status == HttpStatus.OK
response.body().username == username}
Certainly, you can find the complete testbed with the rest of the cases on the repository test folder.
2. Instead of using a nice UI (like Postman), if you enjoy having some CLI time, just execute the following commands to recreate the same scenarios using some terminals that you like:
$ ./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.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU4NjY4NzYxMiwicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImV4cCI6MTU4NjY5MTIxMiwiaWF0IjoxNTg2Njg3NjEyfQ.BMONCdqu9e6vLy-ybGXNce1eq9KP4rd3QExdD5arrdk",
"refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU4NjY4NzYxMiwicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImlhdCI6MTU4NjY4NzYxMn0.bmhgBBQA4RoVj9qftlwZCLpZsYAc3MHckEaam1vQNnQ",
"token_type":"Bearer","expires_in":3600}$ curl -d '{"username":"user4", "password":"password4"}'
-H "Content-Type: application/json"
-X POST http://127.0.0.1:8080/signup{“username”: “user4”,“password”: “password4”}
3. Lastly, as you can see, we have finally obtained the JWT that we were looking for, which contains the following information:
{ "sub": "user1", "nbf": 1586687612, "roles": ["VIEW"],
"iss": "mn-jwt-data", "exp": 1586691212, "iat": 1586687612 }
To be continued
At this point, we are able to register new users and to authenticate them into the system, recovering the token, which we will need in the following steps in Part 2.
P.S.: Remember that you can find the final project here: https://github.com/rmondejar/mn-jwt-data
See you on the next part!