JWT authentication with Micronaut
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.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK