55

JWT authentication with Micronaut

 5 years ago
source link: https://www.tuicool.com/articles/hit/FRNbauu
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Recently I've finally discovered a worthy Spring Boot alternative - Micronaut framework. Micronaut makes creating web applications a breeze. The most interesting thing about it is that it does not use any runtime reflection and still provides a clean and enjoyable API, among others, thanks to compile time annotation based dependency injection.

Today I wanted to show you how easy it is to bootstrap a simple app and configure a JWT authentication. For example's sake we're not going to use a real database, but nothing stops you from plugging in Hibernate , jOOQ or any other library (that's probably a good topic for another post).

Source code for application that I'm going to use as an example is accessible at Github. Feel free to take a look if something is not clear! You can find it here: https://github.com/98elements/micronaut-jwt-demo .

Getting started

Let's start by installing Micronaut using SDKMAN! and boostraping the application using Micronaut CLI. We're going to use Java 11, Gradle and Spock.

λ ~/ sdk install micronaut

Downloading: micronaut 1.0.4

In progress...

######################################################################## 100.0%

Installing: micronaut 1.0.4
Done installing!


Setting micronaut 1.0.4 as default.

λ ~/ mn create-app com._98elements.mnjwtdemo.mnjwtdemo-app -b gradle -l java -f security-jwt,spock
Resolving dependencies..
| Generating Java project...
| Application created at /Users/tomasz.wojcik/mnjwtdemo-app

Micronaut automatically creates a project structure for us (empty directories omitted for the sake of brevity).

λ ~/mnjwtdemo-app/ tree --prune
.
├── Dockerfile
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── micronaut-cli.yml
└── src
    └── main
        ├── java
        │   └── com
        │       └── _98elements
        │           └── mnjwtdemo
        │               └── Application.java
        └── resources
            ├── application.yml
            └── logback.xml

9 directories, 10 files

We've created for ourselves a basic project containing:

  • micronaut-cli.yml with framework's configuration (don't touch ;))
  • an entry point
  • logback config
  • yaml based application config

We've also got a Dockerfile for free (uses Java 8, but it can be easily upgraded to 11), but we're not going to use it in this guide.

Open the project in your favorite IDE and enable annotation processor ! In IntelliJ, just check the appropriate checkbox in preferences.

y2aaeyR.png!web

Annotation processor is required by Micronaut because of compile time dependency injection and lack of runtime reflection.

Creating a controller

Let's create a controller and associated test using CLI once again.

λ ~/mnjwtdemo-app/ mn create-controller Elements
| Rendered template Controller.java to destination src/main/java/com/_98elements/mnjwtdemo/ElementsController.java
| Rendered template ControllerSpec.groovy to destination src/test/groovy/com/_98elements/mnjwtdemo/ElementsControllerSpec.groovy

Micronaut created a stub implementation that doesn't do much. It should serve well for our case, though. Let's just add a @Secured annotation to the class, so later on we can test our authentication.

package com._98elements.mnjwtdemo;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.HttpStatus;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

@Controller("/elements")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class ElementsController {

    @Get("/")
    public HttpStatus index() {
        return HttpStatus.OK;
    }
}

The associated Spock controller test was created in appropriate package.

package com._98elements.mnjwtdemo

import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class ElementsControllerSpec extends Specification {

    @Shared
    @AutoCleanup
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

    void "test index"() {
        given:
        HttpResponse response = client.toBlocking().exchange("/elements")

        expect:
        response.status == HttpStatus.OK
    }
}

It obviously fails when run, because we need to send a valid authentication token along with request body.

λ ~/mnjwtdemo-app/ ./gradlew test

> Task :test

com._98elements.mnjwtdemo.ElementsControllerSpec > test index FAILED
    io.micronaut.http.client.exceptions.HttpClientResponseException

1 test completed, 1 failed

> Task :test FAILED

Let's fix that by implementing authentication and updating the test.

Security

I'm going to create a simple User class, that implements Micronaut's io.micronaut.security.authentication.providers.UserState interface and an accompanying in-memory UserRepository .

package com._98elements.mnjwtdemo;

import io.micronaut.security.authentication.providers.UserState;

public class User implements UserState {

    private final String username;

    private final String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    // getters omitted...
}
package com._98elements.mnjwtdemo;

import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@Singleton // Mark this class as a bean.
public class UserRepository {

    private final ConcurrentMap<String, User> users = new ConcurrentHashMap<>();

    public User create(String username, String password) {
        var user = new User(username, password);
        users.put(username, user);
        return user;
    }

    public Optional<User> findByUsername(String username) {
        return Optional.ofNullable(users.get(username));
    }

}

Now, let's create a config class for authentication.

package com._98elements.mnjwtdemo;

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.security.authentication.providers.AuthoritiesFetcher;
import io.micronaut.security.authentication.providers.PasswordEncoder;
import io.micronaut.security.authentication.providers.UserFetcher;
import io.micronaut.security.authentication.providers.UserState;
import io.reactivex.Flowable;

import javax.inject.Singleton;
import java.util.List;

@Factory
public class SecurityConfiguration {

    private final UserRepository userRepository;

    public SecurityConfiguration(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Bean
    @Singleton
    UserFetcher userFetcher() {
        return username -> userRepository.findByUsername(username)
                .map(Flowable::just)
                .orElseGet(Flowable::empty)
                .cast(UserState.class);
    }

    // We're not using roles, so let's just return an empty list.
    @Bean
    @Singleton
    AuthoritiesFetcher authoritiesFetcher() {
        return username -> Flowable.just(List.of());
    }

    // No-op implementation for the sake of example.
    // Be sure to use a strong hashing algorithm in real life (e.g. bcrypt).
    @Bean
    @Singleton
    PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(String rawPassword) {
                return rawPassword;
            }

            @Override
            public boolean matches(String rawPassword, String encodedPassword) {
                return true;
            }
        };
    }

}

You may have noticed the @Factory annotation and the Flowable type. The former lets us declare @Bean annotated singleton beans by using factory methods, similar to Spring. The latter comes from the fact, that Micronaut is a non-blocking framework and it uses the ReactiveX library. If you're not into reactive programming no one forces you to use this programming model. You'll only have to plug Flowable in some places to bridge the asynchronous and synchronous code, like in the provided example.

Test authentication

Now we're able to fix the controller test by creating an user and logging them in before calling the /elements endpoint.

package com._98elements.mnjwtdemo

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpHeaders
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.security.authentication.UsernamePasswordCredentials
import io.micronaut.security.token.jwt.render.AccessRefreshToken
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class ElementsControllerSpec extends Specification {

    @Shared
    @AutoCleanup
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

    @Shared
    UserRepository userRepository = embeddedServer.applicationContext.getBean(UserRepository)

    String authorization

    void setup() {
        def user = userRepository.create("[email protected]", UUID.randomUUID().toString())

        def request = new UsernamePasswordCredentials(user.username, user.password)
        def tokenPair = client.toBlocking().retrieve(HttpRequest.POST("/login", request), AccessRefreshToken)

        authorization = "Bearer ${tokenPair.accessToken}"
    }

    void "test index"() {
        given:
        def request = HttpRequest.GET("/elements").header(HttpHeaders.AUTHORIZATION, authorization)

        when:
        HttpResponse response = client.toBlocking().exchange(request)

        then:
        response.status == HttpStatus.OK
    }
}

We inject UserRepository from application's context and create a sample user. Then we log the user in using /login endpoint provided by Micronaut (take a look at io.micronaut.security.endpoints.LoginController ) and store the access token for further API calls.

Now the test passes.

λ ~/mnjwtdemo-app/ ./gradlew test

[...]

BUILD SUCCESSFUL in 8s
4 actionable tasks: 4 executed

What now

The next thing to do would be to replace in-memory implementation of UserRepository with real repository accessing a database and to use a proper, strong hashing algorithm instead of no-op PasswordEncoder .

Conclusions

  • Boostrapping applications is very easy thanks to Micronaut's CLI.
  • JWT authenticated can be set up quickly because of security-jwt module provided by Micronaut.
  • Micronaut integrates nicely with Spock framework, allowing us to write concise and readable controller tests.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK