Managing JWT Auth with Micronaut v2 (Part 2)

Ruben Mondejar
3 min readApr 16, 2020

In this second part of this journey, we are going to finish our implementation of this backend built with Micronaut v2, which given a valid token is providing our user data thought its API.

Previously

In the previous part, we explored the first steps in this backend creation, resulting in an API that allows users to register and obtain a valid access token.

Requirements

The remaining requirements for this final task are the following :

  1. Secure REST API
  2. Association with JPA
  3. Fetching mechanisms

This time, let’s focus our effort on how the framework could be used in this way and how we can achieve these goals in no time.

Final REST API

By using Micronaut Security, with a valid token, we are able to call the regular endpoints in our backend. To illustrate this we are going to retrieve user-related resources, messages in this case:

  • GET /messages [headers:{authorization:bearer token}]
  • GET /messages/all [headers:{authorization:bearer token}]
  • POST /messages {content}

Notice that only users with the role “admin” are capable to access to all the messages without user visibility checking.

Adding Message Data

Following the documentation and examples regarding Micronaut Data JPA, we are building all the related components of this new resource.

  1. The first step is to implement the message entity, the corresponding DTO, and a mapper to convert between them:
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Entity
public class Message {
@Id
@GeneratedValue
private Long id;
private String content;
@Builder.Default
private LocalDate creationDate = LocalDate.now();
@ManyToOne
private User user;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Introspected
@JsonIgnoreProperties({"user"})
public class MessageDto {
@NotNull
private Long id;
@NotBlank
private String content;
@NotNull
private LocalDate creationDate;
@NotNull
private UserDto user;
public String getUsername() {
return user.getUsername();
}
}
@Singleton
public class MessageMapper {
(...)public MessageDto toDto(Message message) {
return MessageDto.builder()
.id(message.getId())
.content(message.getContent())
.creationDate(message.getCreationDate())
.user(userMapper.toDto(message.getUser()))
.build();
}
}

2. The second step in this implementation is to create the service and the repository to deal with them:

@Repository
public interface MessageRepository extends PageableRepository<Message, UUID> {
List<Message> findAllByUser(User user);
}
@Singleton
public class MessageService {
(...) public List<MessageDto> findAll() {
List<MessageDto> messageDtos = new ArrayList<>();
messagesRepository.findAll().forEach(message -> messageDtos.add(messageMapper.toDto(message)));
return messageDtos;
}
public List<MessageDto> findAllByUsername(String username) { List<MessageDto> messageDtos = new ArrayList<>(); userRepository.findByUsername(username).ifPresent(user ->
messagesRepository.findAllByUser(user).forEach(message ->
messageDtos.add(messageMapper.toDto(message)))
);
return messageDtos;
}
public Optional<MessageDto> create(String content,
String username) {
return userRepository.findByUsername(username).map(user ->
messagesRepository.save(Message.builder()
.content(content).user(user).build()))
.map(messageMapper::toDto);
}
}

3. Finally, we are adding more testing data with dummy message:

@Override
public void onApplicationEvent(ServerStartupEvent event) {
(...)List<Message> messages = Arrays.asList(
Message.builder().content("My name is user1").user(users.get(0)).build(),
Message.builder().content("My name is user2").user(users.get(1)).build(),
Message.builder().content("My name is user3").user(users.get(2)).build()
);
messageRepository.saveAll(messages);
}

Securing our API

Continuing with our Micronaut Security usage, in this “after login” stage we are going to implement a regular endpoint as an example, where the users could access different data depending on their role and visibility:

@Controller("/messages")
@Secured(IS_AUTHENTICATED)
public class MessageController {
(...)@Get("/all")
@Secured("ADMIN")
public Single<List<MessageDto>> getAllMessages() {

return Single.just(messageService.findAll());
}

@Get
@Secured({"ADMIN", "VIEW"})
public Single<HttpResponse<List<MessageDto>>> getMessages(@Nullable Principal principal) {

return Single.just(
userService.findUser(principal.getName()).map(user ->
HttpResponse.ok(messageService.findAllByUsername(user.getUsername()))
)
.orElse(HttpResponse.unauthorized())
);
}

@Post
@Secured({"ADMIN", "VIEW"})
public Single<HttpResponse<MessageDto>> postMessage(@Nullable Principal principal, @Body String content) {

return Single.just(
userService.findUser(principal.getName()).map(user ->
messageService.create(content, user.getUsername())
.map(message -> HttpResponse.created(message))
.orElse(INSTANCE.status(FORBIDDEN))
)
.orElse(HttpResponse.unauthorized())
);
}
}

Testing

The last code related step is to validate the new components in our system, as we did in the previous part.

  1. Revisiting our Spock specs to validate the new message endpoint :
when: 'The message endpoint is called with a valid token'
HttpRequest requestWithAuthorization = HttpRequest.GET('/messages').header(HttpHeaders.AUTHORIZATION, "Bearer $validToken")
HttpResponse<List<MessageDto>> response = client.toBlocking().exchange(requestWithAuthorization, List)
List<MessageDto> messages = response.body()
then: 'The user\'s messages are retrieved correctly'
noExceptionThrown()
response.status == HttpStatus.OK
messages
!messages.empty
messages.first()
messages.first().user
messages.first().user.username == 'user2'
when: 'Accessing to an admin endpoint with a regular role'
String accessToken = rsp.body().accessToken
HttpRequest requestWithAuthorization = HttpRequest.GET('/messages/all').header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
HttpResponse<String> response = client.toBlocking().exchange(requestWithAuthorization, String)
then: 'Forbidden error'
!response
HttpClientResponseException e = thrown(HttpClientResponseException)
e.status == HttpStatus.FORBIDDEN

2. Trying to reproduce it on the terminal using the CLI:

$ ./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.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU4Njk0NTI0MCwicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImV4cCI6MTU4Njk0ODg0MCwiaWF0IjoxNTg2OTQ1MjQwfQ.BkqVSFYOVfdTvPQOwZY1R3j1ZmI4XJXmALA--HElaPE'",...}
$ curl
-H "'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsIm5iZiI6MTU4Njk0NTI0MCwicm9sZXMiOlsiVklFVyJdLCJpc3MiOiJtbi1qd3QtZGF0YSIsImV4cCI6MTU4Njk0ODg0MCwiaWF0IjoxNTg2OTQ1MjQwfQ.BkqVSFYOVfdTvPQOwZY1R3j1ZmI4XJXmALA--HElaPE'"
-X GET http://127.0.0.1:8080/messages
[{"id": 4,
"content": "My name is user1",
"creationDate": [2020,4,15],
"username": "user1"}
}]

To be continued

Nice, now we are able to recover user-related data and expand our API in the same way. The last part of this trilogy will be the deployment on the cloud to make this real.

P.S.: Remember that you can find the final project here: https://github.com/rmondejar/mn-jwt-data

See you on the next part!

--

--

Ruben Mondejar

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