LDAP and Database authentication providers

Learn how to create a LDAP and a database authentication provider in a Micronaut App.

Authors: Sergio del Amo

Micronaut Version: 1.0.0.M1

1 Getting Started

In this guide, you will create a Micronaut app which uses multiple authentication providers - an LDAP and a database authentication providers.

diagramm

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the Application

Create a Groovy Micronaut app using the Micronaut Command Line Interface.

mn create-app example --features groovy

Due to the --features groovy flag, it generates a Groovy Micronaut app and it uses Gradle build system. However, you could use other build tool such as Maven or other programming languages such as Java or Kotlin.

2.1 Security LDAP

We are going to register an LDAP authentication provider. We use Ldaptive.

Ldaptive is a simple, extensible Java API for interacting with LDAP servers. It was designed to provide easy LDAP integration for application developers.

Modify build.gradle file to add the dependency:

build.gradle
ext {
    ldaptiveVersion = '1.2.3'
}
dependencies {
...
..
.
    compile "org.ldaptive:ldaptive:${ldaptiveVersion}"
}

We are going to use an Online LDAP test server for this guide.

Create several configuration properties matching those of the test LDAP Server.

src/main/resources/application.yml
ldap:
  server: ldap.forumsys.com
  port: 389
  basedn: cn=read-only-admin,dc=example,dc=com

Create an implementation of io.micronaut.security.authentication.AuthenticationProvider

src/main/groovy/example/micronaut/providers/LdapService.groovy
package example.micronaut.providers

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Value
import io.micronaut.security.authentication.AuthenticationFailed
import io.micronaut.security.authentication.AuthenticationProvider
import io.micronaut.security.authentication.AuthenticationRequest
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.UserDetails
import io.reactivex.Flowable
import org.ldaptive.ConnectionConfig
import org.ldaptive.Credential
import org.ldaptive.DefaultConnectionFactory
import org.ldaptive.auth.Authenticator
import org.ldaptive.auth.FormatDnResolver
import org.ldaptive.auth.PooledBindAuthenticationHandler
import org.ldaptive.pool.BlockingConnectionPool
import org.ldaptive.pool.IdlePruneStrategy
import org.ldaptive.pool.PoolConfig
import org.ldaptive.pool.PooledConnectionFactory
import org.ldaptive.pool.SearchValidator
import org.reactivestreams.Publisher

import javax.annotation.PostConstruct
import javax.inject.Singleton
import java.time.Duration

@CompileStatic
@Singleton (1)
class LdapService implements AuthenticationProvider { (2)

    protected final String ldapServer
    protected final Integer ldapPort
    protected final String baseDn
    private Authenticator ldaptiveAuthenticator

    LdapService(@Value('${ldap.server}') String ldapServer, (3)
                @Value('${ldap.port}') Integer port,
                @Value('${ldap.basedn}') String baseDn) {
        this.ldapServer = ldapServer
        this.ldapPort = port
        this.baseDn = baseDn
    }

    @PostConstruct (4)
    void initialize() {
        FormatDnResolver dnResolver = new FormatDnResolver()
        dnResolver.setFormat(baseDn)
        ConnectionConfig connectionConfig = new ConnectionConfig()
        connectionConfig.with {
            setConnectTimeout(Duration.ofSeconds(500))
            setResponseTimeout(Duration.ofSeconds(1000))
            setLdapUrl("ldap://" + ldapServer + ":" + ldapPort)
        }
        DefaultConnectionFactory connectionFactory = new DefaultConnectionFactory()
        connectionFactory.setConnectionConfig(connectionConfig)
        PoolConfig poolConfig = new PoolConfig()
        poolConfig.with {
            setMinPoolSize(1)
            setMaxPoolSize(2)
            setValidateOnCheckOut(true)
            setValidateOnCheckIn(true)
            setValidatePeriodically(false)
        }
        SearchValidator searchValidator = new SearchValidator()
        IdlePruneStrategy pruneStrategy = new IdlePruneStrategy()
        BlockingConnectionPool connectionPool = new BlockingConnectionPool()
        connectionPool.with {
            setPoolConfig(poolConfig)
            setBlockWaitTime(Duration.ofSeconds(1000))
            setValidator(searchValidator)
            setPruneStrategy(pruneStrategy)
            setConnectionFactory(connectionFactory)
            initialize()
        }
        PooledConnectionFactory pooledConnectionFactory = new PooledConnectionFactory()
        pooledConnectionFactory.setConnectionPool(connectionPool)
        PooledBindAuthenticationHandler handler = new PooledBindAuthenticationHandler()
        handler.setConnectionFactory(pooledConnectionFactory)
        ldaptiveAuthenticator = new Authenticator()
        ldaptiveAuthenticator.setDnResolver(dnResolver)
        ldaptiveAuthenticator.setAuthenticationHandler(handler)
    }

    @CompileDynamic
    @Override
    Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) {
        final String username = authenticationRequest.getIdentity() as String
        final String password = authenticationRequest.getSecret() as String
        Credential credential = new Credential(password)
        org.ldaptive.auth.AuthenticationRequest req = new org.ldaptive.auth.AuthenticationRequest(username, credential)
        org.ldaptive.auth.AuthenticationResponse response = ldaptiveAuthenticator.authenticate(req)
        Flowable.just(response.getResult() ? new UserDetails(username, []) (5)
                : new AuthenticationFailed())
    }
}
1 Use javax.inject.Singleton to designate a class as a singleton.
2 Implement io.micronaut.security.authentication.AuthenticationProvider
3 Several configuration values are injected via constructor injection.
4 Use the javax.annotation.PostConstruct annotation to trigger the method execution after dependency injection is done. Typically to perform any initialization.
5 For successful authentication, return in instance of UserDetails.

2.2 GORM

GORM is a powerful Groovy-based data access toolkit for the JVM. GORM is the data access toolkit used by Grails and provides a rich set of APIs for accessing relational and non-relational data including implementations for Hibernate (SQL), MongoDB, Neo4j, Cassandra, an in-memory ConcurrentHashMap for testing and an automatic GraphQL schema generator.

Add a GORM dependency to the project:

build.gradle
ext {
    gormVersion = '6.1.9.RELEASE'
    h2Version = '1.4.196'
    tomcatJdbcVersion = '8.5.28'
}

dependencies {
...
..
.
    compile "io.micronaut.configuration:hibernate-validator"
    compile "io.micronaut.configuration:hibernate-gorm"
    compile "org.grails:grails-datastore-gorm-hibernate5:$gormVersion"
    runtime "com.h2database:h2:$h2Version"
    runtime "org.apache.tomcat:tomcat-jdbc:$tomcatJdbcVersion"
}

2.2.1 Domain Classes

A domain class fulfills the M in the Model View Controller (MVC) pattern and represents a persistent entity that is mapped onto an underlying database table.

2.2.1.1 User

Create User domain class to store users within our application.

package example.micronaut.domain

import grails.gorm.annotation.Entity
import io.micronaut.security.authentication.providers.UserState
import org.grails.datastore.gorm.GormEntity

@Entity (1)
class User implements GormEntity<User>, UserState { (2)
    String username
    String password
    boolean enabled = true
    boolean accountExpired = false
    boolean accountLocked = false
    boolean passwordExpired = false

    static constraints = {
        username nullable: false, blank: false, unique: true
        password nullable: false, blank: false, password: true
    }

    static mapping = {
        password column: '`password`'
    }
}
1 GORM entities should be annotated with grails.persistence.Entity.
2 Use of GormEntity to aid IDE support.

2.2.1.2 Role

Create Role domain class to store authorities within our application.

package example.micronaut.domain

import grails.gorm.annotation.Entity
import org.grails.datastore.gorm.GormEntity

@Entity (1)
class Role implements GormEntity<Role> {  (2)
    String authority

    static constraints = {
        authority nullable: false, unique: true
    }
}
1 GORM entities should be annotated with grails.persistence.Entity.
2 Use of GormEntity to aid IDE support.

2.2.1.3 UserRole

Create a UserRole which stores a many-to-many relationship between User and Role.

package example.micronaut.domain

import grails.gorm.annotation.Entity
import org.grails.datastore.gorm.GormEntity

@Entity (1)
class UserRole implements GormEntity<UserRole> { (2)
    User user
    Role role

    static constraints = {
        user nullable: false
        role nullable: false
    }
}
1 GORM entities should be annotated with grails.persistence.Entity.
2 Use of GormEntity to aid IDE support.

2.2.2 Data Services

GORM Data Services take the work out of implemented service layer logic by adding the ability to automatically implement abstract classes or interfaces using GORM logic.

Create various GORM Data services:

package example.micronaut.services

import example.micronaut.domain.User
import grails.gorm.services.Service

@Service(User) (1)
interface UserGormService {

    User save(String username, String password)

    User findByUsername(String username)

    User findById(Serializable id)

    void delete(Serializable id)
}
1 Annotate with @Service to designate a GORM Data Services which is registered as a Singleton.
package example.micronaut.services

import example.micronaut.domain.Role
import grails.gorm.services.Service

@Service(Role) (1)
interface RoleGormService {
    Role save(String authority)
    Role find(String authority)
    void delete(Serializable id)
}
1 Annotate with @Service to designate a GORM Data Services which is registered as a Singleton.
package example.micronaut.services

import example.micronaut.domain.Role
import example.micronaut.domain.User
import example.micronaut.domain.UserRole
import grails.gorm.services.Query
import grails.gorm.services.Service

@Service(UserRole)
interface UserRoleGormService {

    UserRole save(User user, Role role)

    UserRole find(User user, Role role)

    void delete(Serializable id)

    @Query("""select $r.authority
    from ${UserRole ur}
    inner join ${User u = ur.user}
    inner join ${Role r = ur.role}
    where $u.username = $username""") (2)
    List<String> findAllAuthoritiesByUsername(String username)
}
1 Annotate with @Service to designate a GORM Data Services which is registered as a Singleton.
2 GORM allows Statically-compiled JPA-QL Queries

2.3 Register Service

We are going to register a user when the app starts up.

package example

import example.micronaut.services.RegisterService
import groovy.transform.CompileStatic
import io.micronaut.runtime.Micronaut
import javax.inject.Singleton
import io.micronaut.context.event.ApplicationEventListener
import io.micronaut.runtime.server.event.ServerStartupEvent

@CompileStatic
@Singleton
class Application implements ApplicationEventListener<ServerStartupEvent> { (1)

    protected final RegisterService registerService

    Application(RegisterService registerService) { (2)
        this.registerService = registerService
    }

    @Override
    void onApplicationEvent(ServerStartupEvent event) { (1)
        registerService.register('user','user',['ROLE_GRAILS']) (3)
    }

    static void main(String[] args) {
        Micronaut.run(Application.class)
    }
}
1 Implements ServerStartupEvent which enables to execute a method when the application starts.
2 RegisterService is injected via constructor injection.
3 Register a new user when the app starts.

Create RegisterService

package example.micronaut.services

import example.micronaut.domain.Role
import example.micronaut.domain.User
import example.micronaut.domain.UserRole
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic
import io.micronaut.security.authentication.providers.PasswordEncoder
import javax.inject.Singleton

@CompileStatic
@Singleton
class RegisterService {

    protected final RoleGormService roleGormService
    protected final UserGormService userGormService
    protected final UserRoleGormService userRoleGormService
    protected final PasswordEncoder passwordEncoder

    RegisterService(RoleGormService roleGormService,
                    UserGormService userGormService,
                    PasswordEncoder passwordEncoder,
                    UserRoleGormService userRoleGormService) {
        this.roleGormService = roleGormService
        this.userGormService = userGormService
        this.userRoleGormService = userRoleGormService
        this.passwordEncoder = passwordEncoder
    }

    @Transactional
    void register(String username, String rawPassword, List<String> authorities) {

        User user = userGormService.findByUsername(username)
        if ( !user ) {
            final String encodedPassword = passwordEncoder.encode(rawPassword)
            user = userGormService.save(username, encodedPassword)
        }

        if ( user && authorities ) {

            for ( String authority : authorities ) {
                Role role = roleGormService.find(authority)
                if ( !role ) {
                    role = roleGormService.save(authority)
                }
                UserRole userRole = userRoleGormService.find(user, role)
                if ( !userRole ) {
                    userRoleGormService.save(user, role)
                }
            }
        }
    }
}

2.4 Delegating Authentication Provider

Micronaut ships with DelegatingAuthenticationProvider which can be typically used in environments such as the one described in the next diagramm.

delegating authentication provider

DelegatingAuthenticationProvider is not enabled unless you provide implementations for UserFetcher, PasswordEncoder and AuthoritiesFetcher.

Next, we provide an implementation for those interfaces.

2.4.1 User Fetcher

We provide an implementation for UserFetcher:

src/main/groovy/example/micronaut/services/UserFetcherService.groovy
package example.micronaut.services

import groovy.transform.CompileStatic
import io.micronaut.security.authentication.providers.UserFetcher
import io.micronaut.security.authentication.providers.UserState
import io.reactivex.Flowable
import org.reactivestreams.Publisher

import javax.inject.Singleton

@CompileStatic
@Singleton (1)
class UserFetcherService implements UserFetcher {

    protected final UserGormService userGormService

    UserFetcherService(UserGormService userGormService) { (2)
        this.userGormService = userGormService
    }

    @Override
    Publisher<UserState> findByUsername(String username) {
        UserState user = userGormService.findByUsername(username) as UserState
        (user ? Flowable.just(user) : Flowable.empty()) as Publisher<UserState>
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton.
2 UserGormService is injected via constructor injection.

2.4.2 Authorities Fetcher

Provide an implementation for AuthoritiesFetcher:

src/main/groovy/example/micronaut/services/AuthoritiesFetcherService.groovy
package example.micronaut.services

import io.micronaut.security.authentication.providers.AuthoritiesFetcher
import io.reactivex.Flowable
import org.reactivestreams.Publisher

import javax.inject.Singleton

@Singleton (1)
class AuthoritiesFetcherService implements AuthoritiesFetcher {

    protected final UserRoleGormService userRoleGormService

    AuthoritiesFetcherService(UserRoleGormService userRoleGormService) {  (2)
        this.userRoleGormService = userRoleGormService
    }

    @Override
    Publisher<List<String>> findAuthoritiesByUsername(String username) {
        Flowable.just(userRoleGormService.findAllAuthoritiesByUsername(username))
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton.
2 UserRoleGormService is injected via constructor injection.

2.4.3 Password Encoder

We provide an implementation for PasswordEncoder.

First include a dependency to Spring Security Crypto to ease password encoding.

build.gradle
ext {
...
..
.
    springSecurityCryptoVersion='4.2.5.RELEASE'
}
dependencies {
...
..
.
    compile "org.springframework.security:spring-security-crypto:${springSecurityCryptoVersion}"
}
src/main/groovy/example/micronaut/services/BCryptPasswordEncoderService.groovy
package example.micronaut.services

import io.micronaut.security.authentication.providers.PasswordEncoder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import javax.inject.Singleton

@Singleton (1)
class BCryptPasswordEncoderService implements PasswordEncoder {

    org.springframework.security.crypto.password.PasswordEncoder delegate = new BCryptPasswordEncoder()

    String encode(String rawPassword) {
        return delegate.encode(rawPassword)
    }

    @Override
    boolean matches(String rawPassword, String encodedPassword) {
        return delegate.matches(rawPassword, encodedPassword)
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton.

2.5 LDAP Authentication Provider test

Create a test which verifies an LDAP user can login.

src/test/groovy/example/micronaut/LoginLdapSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpMethod
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
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.generator.claims.JwtClaims
import io.micronaut.security.token.jwt.render.AccessRefreshToken
import io.micronaut.security.token.jwt.validator.JwtTokenValidator
import io.micronaut.security.token.validator.TokenValidator
import io.reactivex.Flowable
import org.reactivestreams.Publisher
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class LoginLdapSpec extends Specification {

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

    @Shared
    @AutoCleanup (1)
    HttpClient client = HttpClient.create(embeddedServer.URL) (3)

    void '/login with valid credentials returns 200 and access token and refresh token'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .body(new UsernamePasswordCredentials('euler', 'password')) (4)
        HttpResponse<AccessRefreshToken> rsp = client.toBlocking().exchange(request, AccessRefreshToken)

        then:
        rsp.status.code == 200
        rsp.body.isPresent()
        rsp.body.get().accessToken
        rsp.body.get().refreshToken
    }

    void '/login with invalid credentials returns UNAUTHORIZED'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .body(new UsernamePasswordCredentials('euler', 'bogus'))  (4)
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 401 (5)
    }

    void 'access token contains expiration date'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .body(new UsernamePasswordCredentials('euler', 'password')) (4)
                HttpResponse<AccessRefreshToken> rsp = client.toBlocking().exchange(request, AccessRefreshToken)

        then:
        rsp.status.code == 200
        rsp.body.isPresent()

        when:
        String accessToken = rsp.body.get().accessToken

        then:
        accessToken

        when:
        Publisher authentication = tokenValidator.validateToken(accessToken)

        then:
        Flowable.fromPublisher(authentication).blockingFirst()

        and:
        Flowable.fromPublisher(authentication).blockingFirst().getAttributes().get(JwtClaims.EXPIRATION_TIME)
    }

    void 'refresh token does not contain expiration date'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .body(new UsernamePasswordCredentials('euler', 'password')) (4)
        HttpResponse<AccessRefreshToken> rsp = client.toBlocking().exchange(request, AccessRefreshToken)

        then:
        rsp.status.code == 200
        rsp.body.isPresent()

        when:
        String refreshToken = rsp.body.get().refreshToken

        then:
        refreshToken

        when:
        Publisher authentication = tokenValidator.validateToken(refreshToken)

        then:
        Flowable.fromPublisher(authentication).blockingFirst()

        and:
        !Flowable.fromPublisher(authentication).blockingFirst().getAttributes().get(JwtClaims.EXPIRATION_TIME)
    }

    TokenValidator getTokenValidator() {
        embeddedServer.applicationContext.getBean(JwtTokenValidator.class) (6)
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 If you attempt to access a secured endpoint without authentication, 401 is returned
6 You can retrieve a bean from the application context easily with getBean(Class)

2.6 Login Testing

Test /login endpoint. We verify both LDAP and DB authentication providers work.

src/test/groovy/example/micronaut/LoginControllerSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpMethod
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
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.AccessRefreshToken
import io.micronaut.security.token.jwt.validator.JwtTokenValidator
import io.micronaut.security.token.validator.TokenValidator
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class LoginControllerSpec extends Specification {

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

    @Shared
    @AutoCleanup (1)
    HttpClient client = HttpClient.create(embeddedServer.URL) (3)

    void 'attempt to access /login without supplying credentials server responds BAD REQUEST'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE) (4)
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    void '/login with valid credentials for a database user returns 200 and access token and refresh token'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .body(new UsernamePasswordCredentials('user', 'user')) (4)
        HttpResponse<AccessRefreshToken> rsp = client.toBlocking().exchange(request, AccessRefreshToken)

        then:
        rsp.status.code == 200
        rsp.body.isPresent()
        rsp.body.get().accessToken
        rsp.body.get().refreshToken
    }

    TokenValidator getTokenValidator() {
        embeddedServer.applicationContext.getBean(JwtTokenValidator.class) (5)
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 You can retrieve a bean from the application context easily with getBean(Class)

2.7 Oauth Testing

Test /oauth/access_token endpoint. We verify it is possible to refresh an access token.

src/test/groovy/example/micronaut/OauthControllerSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpMethod
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
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.endpoints.TokenRefreshRequest
import io.micronaut.security.token.jwt.generator.claims.JwtClaims
import io.micronaut.security.token.jwt.render.AccessRefreshToken
import io.micronaut.security.token.jwt.render.BearerAccessRefreshToken
import io.micronaut.security.token.jwt.validator.JwtTokenValidator
import io.micronaut.security.token.validator.TokenValidator
import io.reactivex.Flowable
import org.reactivestreams.Publisher
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class OauthControllerSpec extends Specification {

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

    @Shared
    @AutoCleanup  (1)
    HttpClient client = HttpClient.create(embeddedServer.URL) (3)

    void '/oauth/access_token endpoint returns BAD request if required parameters not present'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/oauth/access_token')
                .accept(MediaType.APPLICATION_FORM_URLENCODED_TYPE) (4)
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    void 'Users can generate a new token by requesting to /oauth/access_token with refresh token'() {
        when:
        HttpRequest request = HttpRequest.create(HttpMethod.POST, '/login')
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .body(new UsernamePasswordCredentials('euler', 'password'))  (4)
        HttpResponse<BearerAccessRefreshToken> rsp = client.toBlocking().exchange(request, BearerAccessRefreshToken)

        then:
        rsp.status.code == 200
        rsp.body.isPresent()

        when:
        String accessToken = rsp.body.get().accessToken
        String refreshToken = rsp.body.get().refreshToken

        then:
        accessToken
        refreshToken

        when:
        sleep(1_000)
        request = HttpRequest.create(HttpMethod.POST, '/oauth/access_token')
                .contentType(MediaType.APPLICATION_JSON)
                .body(new TokenRefreshRequest("refresh_token", refreshToken)) (4)
        HttpResponse<AccessRefreshToken> tokenRsp = client.toBlocking().exchange(request, AccessRefreshToken)

        then:
        tokenRsp.status.code == 200
        tokenRsp.body.isPresent()

        and: 'a new access token was generated'
        tokenRsp.body.get().accessToken
        tokenRsp.body.get().accessToken != accessToken

        when:
        TokenValidator tokenValidator = embeddedServer.applicationContext.getBean(JwtTokenValidator) (5)
        Publisher authentication = tokenValidator.validateToken(accessToken)

        then:
        Flowable.fromPublisher(authentication).blockingFirst()

        and:
        Flowable.fromPublisher(authentication).blockingFirst().getAttributes().get(JwtClaims.EXPIRATION_TIME)
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 You can retrieve a bean from the application context easily with getBean(Class)

3 Testing the Application

To run the tests:

$ ./gradlew test
$ open build/reports/tests/test/index.html

4 Running the Application

To run the application use the ./gradlew run command which will start the application on a random port.