Table of Contents

Micronaut Security

Official Security Solution for Micronaut

Version:

1 Introduction

Micronaut Security is a fully featured and customizable security solution for your applications.

Micronaut Security 2.x requires Micronaut 2.x.

2 What's New

Micronaut Security 2.0.2 includes the following new features and improvements.

Annotation Processing

  • The @Secured annotation has been moved to a separate module in order to reduce the number of classes in the annotation processor scope. If you currently have micronaut-security in the classpath of your compiler, the entry can be changed to micronaut-security-annotations.

Improvements

  • A new constructor for AuthenticationFailed that takes a string message has been added to allow for custom error messages.

  • It is now possible to require all AuthenticationProviders to return a successful authentication response. Set micronaut.security.authentication-provider-strategy: ALL to enable this functionality.

  • The token propagation implementation will now no longer override an existing token.

  • It is now possible to allow for requests that would normally result in a 404 to return with 404 instead of almost always returning a 401 or 403. The current behavior is still the default because it prevents attackers from discovering what endpoints are available in your application. To enable the alternative behavior, set micronaut.security.reject-not-found: false in your configuration.

  • It is now supported out of the box to redirect back to the prior URL after a successful login. If a user requests a URL that returns an unauthenticated response, after logging in successfully, the user can then be returned to the URL that previously returned unauthorized. This setting can be enabled with micronaut.security.redirect.prior-to-login: true in your configuration. It is disabled by default.

  • Support for using tokens directly from OpenID providers has been added. By default the information from the OpenID JWT token is used to create a new token that uses Micronaut’s JWT settings. This allows for a common format across providers as well as standardized expiration. Some users may want to use the token coming from the provider directly. That is now supported with the configuration option and value micronaut.security.authentication: idtoken. Note that this authentication mode requires each application to implement any necessary refresh token functionality. The refresh token functionality in this library is specific to the tokens created by this library.

  • The TokenValidator API now has access to the current request.

3 Breaking Changes

This section will document breaking changes that may happen during milestone or release candidate releases, as well as major releases eg (1.x.x → 2.x.x).

2.0.0

New Maven Group ID

The Maven Group ID has changed from io.micronaut to io.micronaut.security.

Modules Enabled by default

  • Modules are now enabled by default. The configuration keys micronaut.security.enabled, micronaut.security.token.jwt.enabled, micronaut.security.oauth2.enabled, and micronaut.security.session.enabled are now all true by default.

Endpoints enabled by default

  • You don’t need to enable Endpoints. The configuration keys micronaut.security.endpoints.keys.enabled, micronaut.security.endpoints.login.enabled, micronaut.security.endpoints.logout.enabled, micronaut.security.endpoints.oauth.enabled are now all true by default. However, You will need to provide the required beans for each endpoint.

Authentication Provider

  • The AuthenticationProvider API has changed to include the HTTP request as a parameter. Thus, it is now possible to gain access to the request in your AuthenticationProvider. Simply override the default method in the interface that takes the request as an argument with your own implementation. The old method is no longer called from the framework.

  • It is no longer assumed that a failed authentication response is an instance of AuthenticationFailed. That now allows for any subclass of AuthenticationResponse to be emitted in the case of an authentication failure.

  • Previously AuthenticationProviders were allowed to return hot observables, meaning the logic to authenticate the user could be done upon execution of the method. Allowing for that behavior caused the logic to execute the authenticators to be difficult to maintain. Now providers must return cold observables. The authenticate method will be executed for every authentication provider for every authentication request. If you were previously doing the authentication work then using something like Flowable.just, you must use something like Flowable.create to create a cold observable.

Delegating Authentication Provider

  • The DelegatingAuthenticationProvider and it’s related interfaces has been removed. If you provided implementations for UserFetcher, UserState, PasswordEncoder, and AuthoritiesFetcher, integrate them into your own implementation of AuthenticationProvider. This API was removed because it only served a niche use case and increased the surface of this library’s API. None of the classes mentioned were directly used in this library and thus belong in the realm of user’s applications. Check LDAP and Database authentication providers tutorial to learn how to write a DelegatingAuthenticationProvider.

LdapAuthenticationProvider

The constructor for LdapAuthenticationProvider has been changed to accept an executor to schedule the LDAP lookup operation on. By default it is now scheduled on the IO thread pool.

LoginHandler

Migration Example A

If you were using the default bearer authentication, you should set:

micronaut:
    security:
        authentication: bearer

Migration Example B

For a previous configuration such as:

micronaut:
    security:
        token:
            jwt:
                bearer:
                     enabled: false
                cookie:
                     enabled: true

use instead:

micronaut:
    security:
        authentication: cookie

LogoutHandler

Refresh Token

  • The story around refresh tokens has been changed. There were a couple of core issues with the old implementation that needed resolved.

    • The refresh tokens were JWT tokens which also allowed them to be used to access resources.

    • Because they never expire (by default), it requires a list of invalid tokens to be maintained in perpetuity if the ability to revoke tokens is a requirement. JwtGeneratorConfiguration#getRefreshTokenExpiration is deprecated and it is no longer used.

    • The refresh mechanism simply copied the claims from the refresh token JWT back into a new access token. This made any changes to the state of the user (new roles, etc) were not be applied at the time of refresh.

      The functionality has now been changed to generate a token that is not a JWT, but is instead a signed token of a unique key. The default implementation signs a UUID with a secret provided via configuration. After the token is generated, it is up to each application to provide an implementation of RefreshTokenPersistence to store the token and link it to the user it was created for. That is essential to generate new claims information in new access tokens created by the refresh endpoint. See the refresh section for more information.

Rejection Handler

  • The legacy rejection handler in the session module, which was enabled by default, has been removed. The setting micronaut.security.session.legacy-rejection-handler no longer has any effect and is equivalent to the behavior of false in previous versions.

  • The entire RejectionHandler API and its dependents have been removed in favor of using the built in exception handling API in Micronaut. An AuthorizationException will now be emitted when access to a resource has been rejected. A default exception handler has been created that will redirect on forbidden or unauthorized if the accept header of the request allows for text/html. For bearer authentication, that behavior can be disabled with:

    micronaut.security.redirect.forbidden.enabled: false
    micronaut.security.redirect.unauthorized.enabled: false

Basic Auth

  • Basic authentication configuration has moved from micronaut.security.token.basic-auth to micronaut.security.basic-auth. The implementation surrounding basic authentication has changed and some classes have been deleted. If you have not overridden any beans related to basic authentication, the functionality will remain the same as it was. Previously a TokenValidatedEvent was triggered as a result of basic authentication, however that is no longer the case.

TokenPropagation

Several classes related to token propagation have been renamed and relocated:

Old New

io.micronaut.security.token.writer.HttpHeaderTokenWriter

HttpHeaderTokenPropagator

io.micronaut.security.token.writer.HttpHeaderTokenWriterConfiguration

HttpHeaderTokenPropagatorConfiguration

io.micronaut.security.token.writer.HttpHeaderTokenWriterConfigurationProperties

HttpHeaderTokenPropagatorConfigurationProperties

io.micronaut.security.token.writer.TokenWriter

TokenPropagator

Read token propagation section to learn more.

Misc

  • Several APIs that lacked generics have been changed to add them. For example HttpRequestHttpRequest<?>.

  • Several APIs have been changed to return MutableHttpResponse instead of HttpResponse.

  • The SecurityFilterOrderProvider API has been deleted in favor of the standard ServerFilterPhase class. The security filter will now run on the order defined by the SECURITY phase.

  • The io.micronaut.security.token.jwt.signature.secret.SecretSignatureFactory was deleted because the beans can be created directly from the SecretSignature class.

  • AccessRefreshTokenGenerator is now the interface which defines how generate a AccessRefreshToken for a particular user. The implementation has been moved to DefaultAccessRefreshTokenGenerator.

Deprecated

  • All deprecations present in 1.3.x have been removed.

  • JwtGeneratorConfiguration has been deprecated in favor of AccessTokenConfiguration. The configuration has been changed from micronaut.security.token.jwt.generator.access-token-expiration to micronaut.security.token.jwt.generator.access-token.expiration.

Redirection

  • Redirection configuration has been simplified. The properties in the first column are deprecated and will be removed in a future version. The properties on the right should be used instead. All properties are prefixed with micronaut.security.

Old New

micronaut.security.session.login-success-target-url

micronaut.security.redirect.login-success

micronaut.security.session.login-failure-target-url

micronaut.security.redirect.login-failure

micronaut.security.session.logout-target-url

micronaut.security.redirect.logout

micronaut.security.session.unauthorized-target-url

micronaut.security.redirect.unauthorized.url

micronaut.security.session.forbidden-target-url

micronaut.security.redirect.forbidden.url

micronaut.security.session.redirect-on-rejection

micronaut.security.redirect.unauthorized.enabled and micronaut.security.redirect.forbidden.enabled

micronaut.security.token.jwt.cookie.logout-target-url

micronaut.security.redirect.logout

micronaut.security.token.jwt.cookie.login-success-target-url

micronaut.security.redirect.login-success

micronaut.security.token.jwt.cookie.login-failure-target-url

micronaut.security.redirect.login-failure

4 Getting Started

Using the CLI

If you are creating your project using the Micronaut CLI, supply either the security-jwt or security-session features configure the security support in your project:

$ mn create-app my-app --features security-jwt

To use the Micronaut’s security capabilities you must have the security dependency on your classpath:

implementation("io.micronaut.security:micronaut-security-annotations")
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-annotations</artifactId>
</dependency>

implementation("io.micronaut.security:micronaut-security-jwt")
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-jwt</artifactId>
</dependency>

The micronaut-security-annotations dependency is only required to use JSR 250 annotations

By default Micronaut returns HTTP Status Unauthorized (401) for any endpoint invocation. Routes must be explicitly allowed through the provided mechanisms.

secured by default

5 Security Configuration

The following global configuration options are available:

🔗
Table 1. Configuration Properties for SecurityConfigurationProperties
Property Type Description

micronaut.security.enabled

boolean

If Security is enabled. Default value true

micronaut.security.intercept-url-map

java.util.List

Map that defines the interception patterns.

micronaut.security.ip-patterns

java.util.List

Allowed IP patterns. Default value (["0.0.0.0"])

micronaut.security.authentication-provider-strategy

AuthenticationStrategy

Determines how authentication providers should be processed. Default value ANY. Possible values: ANY or ALL.

micronaut.security.reject-not-found

boolean

Set to true if the server should respond with 404 for requests that do not

micronaut.security.authentication

AuthenticationMode

Defines which authentication to use. Defaults to null. Possible values bearer, session, cookie. Should only be supplied if the service handles login and logout requests.

5.1 Reject Not Found Routes

By default, when you include Micronaut security the app returns 401 or 403 even for non existing routes. The behavior prevents attackers from discovering what endpoints are available in your application. However, if you wish to return 404 for non found routes you can set micronaut.security.reject-not-found: false in your configuration.

5.2 Authentication Strategy

By default, Micronaut requires just one Authentication Provider to return a successful authentication response. You can set micronaut.security.authentication-provider-strategy: ALL to require all AuthenticationProviders to return a successful authentication response.

5.3 Login Handler

The LoginHandler API defines how to respond to a successful or failed login attempt. For example, with the Login Controller or with OAuth 2.0 support.

You can provide your own implementation of LoginHandler. However, Micronaut security modules ship with several implementations which you can enable by setting the configuration micronaut.security.authentication

Configuration Value Required Module Bean Registered

cookie

micronaut-jwt

JwtCookieLoginHandler

session

micronaut-session

SessionLoginHandler

bearer

micronaut-jwt

AccessRefreshTokenLoginHandler

idtoken

micronaut-oauth

IdTokenLoginHandler

5.4 Logout Handler

The LogoutHandler API defines how to respond to a logout attempt. For example, with the Logout Controller.

You can provide your own implementation of LogoutHandler. However, Micronaut security modules ship with several implementations which you can enable by setting the configuration property micronaut.security.authentication with values:

Configuration Value Required Module Bean Registered

cookie

micronaut-jwt

JwtCookieClearerLogoutHandler

idtoken

micronaut-jwt

JwtCookieClearerLogoutHandler

session

micronaut-session

SessionLogoutHandler

5.5 Redirection Configuration

Several security flows (e.g. session based authentication, Cookie Token authentication) may involve redirection after the user logs in.

You can configure the redirection destinations with:

🔗
Table 1. Configuration Properties for RedirectConfigurationProperties
Property Type Description

micronaut.security.redirect.login-success

java.lang.String

Where the user is redirected to after a successful login. Default value ("/").

micronaut.security.redirect.login-failure

java.lang.String

Where the user is redirected to after a failed login. Default value ("/").

micronaut.security.redirect.logout

java.lang.String

URL where the user is redirected after logout. Default value ("/").

micronaut.security.redirect.prior-to-login

boolean

If true, the user should be redirected back to the unauthorized request that initiated the login flow. Supersedes the <code>login-success</code> configuration for those cases. Default value false.

🔗
Table 2. Configuration Properties for RedirectConfigurationProperties$ForbiddenRedirectConfigurationProperties
Property Type Description

micronaut.security.redirect.forbidden.enabled

boolean

Whether it should redirect on forbidden rejections. Default value (true).

micronaut.security.redirect.forbidden.url

java.lang.String

Where the user is redirected to after trying to access a secured route which he is forbidden to access. Default value ("/").

🔗
Table 3. Configuration Properties for RedirectConfigurationProperties$UnauthorizedRedirectConfigurationProperties
Property Type Description

micronaut.security.redirect.unauthorized.enabled

boolean

Whether it should redirect on unauthorized rejections. Default value (true).

micronaut.security.redirect.unauthorized.url

java.lang.String

Where the user is redirected to after trying to access a secured route. Default value ("/").

6 Authentication Providers

To authenticate users you must provide implementations of AuthenticationProvider.

The following code snippet illustrates a naive implementation:

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.*;
import io.reactivex.Maybe;
import org.reactivestreams.Publisher;

import javax.inject.Singleton;
import java.util.ArrayList;
@Singleton
public class AuthenticationProviderUserPassword implements AuthenticationProvider {

    @Override
    public Publisher<AuthenticationResponse> authenticate(HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) {
        return Maybe.<AuthenticationResponse>create(emitter -> {
            if (authenticationRequest.getIdentity().equals("user") && authenticationRequest.getSecret().equals("password")) {
                emitter.onSuccess(new UserDetails("user", new ArrayList<>()));
            } else {
                emitter.onError(new AuthenticationException(new AuthenticationFailed()));
            }
        }).toFlowable();
    }
}

The built-in Login Controller uses every available authentication provider. The first provider that returns a successful authentication response will have its value used as the basis for the JWT token or session state.

Basic authentication which is implemented as an AuthenticationFetcher will also trigger the available AuthenticationProviders.

Micronaut comes with authentication providers for LDAP and the OAuth 2.0 password grant authentication flow. For any custom authentication, an authentication provider must be created.

7 Security Rules

The decision to allow access to a particular endpoint to anonymous or authenticated users is determined by a collection of Security Rules. Micronaut ships with several built-in security rules. If they don’t fulfil your needs, you can implement your own SecurityRule.

Security rules implement the ordered interface and they are executed in order. All of the existing rules have a static variable ORDER that stores the order of that rule. You can use those variables to place your own rule before or after any of the existing rules.

Security rules return a SecurityRuleResult. See the following table for a description of each result.

Result Description

ALLOWED

Access to the resource should be granted and no further rules will be considered.

REJECTED

Access to the resource should be rejected and no further rules will be considered.

UNKNOWN

The rule doesn’t apply to the request resource, or it cannot be determined either way. This result will cause other security rules to be considered.

If all security rules return UNKNOWN, the request will be rejected!

7.1 IP Pattern Rule

When you turn on security, traffic coming from any ip address is allowed by default.

You can however reject traffic not coming from a white list of IP Patterns as illustrated below:

micronaut:
  security:
    ip-patterns:
      - 127.0.0.1
      - 192.168.1.*

In the previous code, the IpPatternsRule rejects traffic not coming either 127.0.0.1 or 192.168.1.* range.

The IP patterns rule never explicitly allows requests, it only rejects requests if the address does not match. There must be other security rules that determine whether a resource should be accessed.

If the desired behavior is to allow access to all resources as long as the address matches, create a security rule that executes after this one that returns ALLOWED.

7.2 Secured Annotation

As illustrated below, you can use the @Secured annotation to control access to controllers or controller methods.

@Controller("/example")
@Secured(SecurityRule.IS_AUTHENTICATED) (1)
public class ExampleController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/admin")
    @Secured({"ROLE_ADMIN", "ROLE_X"}) (2)
    public String withroles() {
        return "You have ROLE_ADMIN or ROLE_X roles";
    }

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/anonymous")
    @Secured(SecurityRule.IS_ANONYMOUS)  (3)
    public String anonymous() {
        return "You are anonymous";
    }

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/authenticated") (1)
    public String authenticated(Authentication authentication) {
        return authentication.getName() + " is authenticated";
    }
}
1 Authenticated users are able to access authenticated Controller’s action.
2 Users granted role ROLE_ADMIN or ROLE_X roles can access withroles Controller’s action.
3 Anonymous users (authenticated and not authenticated users) can access anonymous Controller’s action.

Alternatively, you can use JSR_250 annotations:

  • javax.annotation.security.PermitAll

  • javax.annotation.security.RolesAllowed

  • javax.annotation.security.DenyAll

@Controller("/example")
public class ExampleController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/admin")
    @RolesAllowed({"ROLE_ADMIN", "ROLE_X"}) (1)
    public String withroles() {
        return "You have ROLE_ADMIN or ROLE_X roles";
    }

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/anonymous")
    @PermitAll  (2)
    public String anonymous() {
        return "You are anonymous";
    }
}
1 Users granted role ROLE_ADMIN or ROLE_X roles can access withroles Controller’s action.
2 Anonymous users (authenticated and not authenticated users) can access anonymous Controller’s action.
The use of JSR 250 annotations requires io.micronaut.security:micronaut-security-annotations to be in the annotation processor classpath (annotationProcessor, kapt, compileOnly) respectively for Java, Kotlin, Groovy.

7.3 Intercept URL Map

Moreover, you can configure endpoint authentication and authorization access with an Intercept URL Map:

micronaut:
  security:
    intercept-url-map:
      -
        pattern: /images/*
        http-method: GET
        access:
          - isAnonymous() (1)
      -
        pattern: /books
        access:
          - isAuthenticated() (2)
      -
        pattern: /books/grails
        http-method: POST
        access:
          - ROLE_GRAILS (3)
          - ROLE_GROOVY
      -
        pattern: /books/grails
        http-method: PUT
        access:
          - ROLE_ADMIN
1 Enable access to authenticated and not authenticated users
2 Enable access for everyone authenticated
3 Enable access for users who are granted any of the specified roles.

As you see in the previous code listing, any endpoint is identified by a combination of pattern and an optional HTTP method.

If a given request URI matches more than one intercept url map, the one that specifies an http method that matches the request method will be used. If there are multiple mappings that do not specify a method and match the request URI, then the first mapping will be used. For example:

The example below defines that all HTTP requests to URIs matching the pattern /v1/myResource/** and using HTTP method GET will be accessible to everyone. Requests matching the same URI pattern but using a different HTTP method than GET require fully authenticated access.

micronaut:
  security:
    intercept-url-map:
      - pattern: /v1/myResource/**
        httpMethod: GET (1)
        access:
          - isAnonymous()
      - pattern: /v1/myResource/**
        access:
          - isAuthenticated() (2)
1 Accessing /v1/myResource/** with a GET request does not require authentication.
2 Accessing /v1/myResource/** with a request that isn’t GET requires authentication.

7.4 Built-In Endpoints Security

When you turn on security, Built-in endpoints are secured depending on their sensitive value.

endpoints:
  beans:
    enabled: true
    sensitive: true (1)
  info:
    enabled: true
    sensitive: false  (2)
1 /beans endpoint is secured
2 /info endpoint is open for unauthenticated access.

8 Authorization Strategies

8.1 Basic Authentication

Out-of-the-box, Micronaut supports RFC7617 which defines the "Basic" Hypertext Transfer Protocol (HTTP) authentication scheme, which transmits credentials as user-id/password pairs, encoded using Base64. Basic authentication is enabled by default. You can disable it by setting micronaut.security.basic-auth.enabled to false.

The following sequence illustrates the authentication flow:

basicauth

Below is a sample of a cURL command using basic auth:

cURL command using Basic Auth
curl "http://localhost:8080/info" \
     -u 'user:password'

After credentials are read from the HTTP Header, they are feed into an Authenticator which attempts to validate them.

The code snippet below illustrates how to send credentials using the basicAuth method from MutableHttpRequest method:

Accessing a protected resource using an Authorization request header
HttpRequest request = HttpRequest.GET("/home").basicAuth('sherlock', 'password')
Read the Basic Authentication Micronaut Guide to learn more.

8.2 Session Authorization

Micronaut supports Session based authentication.

Using the CLI

If you are creating your project using the Micronaut CLI, supply either the security-session feature to configure session security in your project:

$ mn create-app my-app --features security-session

To use the Micronaut’s session based authentication capabilities you must have the security-session dependency on your classpath. For example:

implementation("io.micronaut.security:micronaut-security-annotations")
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-annotations</artifactId>
</dependency>

implementation("io.micronaut.security:micronaut-security-session")
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-session</artifactId>
</dependency>

The micronaut-security-annotations dependency is only required to use JSR 250 annotations

The following sequence illustrates the authentication flow:

sesssion based auth

Check the Redirection configuration to customize session based authentication behaviour.

Example of Session-Based Authentication configuration

micronaut:
  security:
    authentication: session
    redirect:
      login-failure: /login/authFailed

8.3 JSON Web Token

The following configuration properties are available to customize token based authentication:

🔗
Table 1. Configuration Properties for TokenConfigurationProperties
Property Type Description

micronaut.security.token.enabled

boolean

Sets whether the configuration is enabled. Default value true.

micronaut.security.token.roles-name

java.lang.String

Authentication attributes map key for the user’s roles. Default value "roles".

micronaut.security.token.name-key

java.lang.String

Authentication attributes map key for the user’s name. Default value "username".

Micronaut ships with security capabilities based on Json Web Token (JWT). JWT is an IETF standard which defines a secure way to encapsulate arbitrary data that can be sent over unsecure URL’s.

Using the CLI

If you are creating your project using the Micronaut CLI, supply the security-jwt feature to configure JWT support in your project:

$ mn create-app my-app --features security-jwt

To use the Micronaut’s JWT based authentication capabilities you must have the security-jwt dependency on your classpath. For example:

implementation("io.micronaut.security:micronaut-security-annotations")
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-annotations</artifactId>
</dependency>

implementation("io.micronaut.security:micronaut-security-jwt")
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-jwt</artifactId>
</dependency>

The micronaut-security-annotations dependency is only required to use JSR 250 annotations

The following configuration properties are available to customize JWT based authentication behaviour:

🔗
Table 2. Configuration Properties for JwtConfigurationProperties
Property Type Description

micronaut.security.token.jwt.enabled

boolean

Sets whether JWT security is enabled. Default value (true).

What does a JWT look like?

jwt

Header

A base64-encoded JSON like:

JWT header
{
    "alg": "HS256",
    "typ": "JWT"
}

Claims

A base64-encoded JSON like:

JWT claims
{
    "exp": 1422990129,
    "sub": "jimi",
    "roles": [
        "ROLE_ADMIN",
        "ROLE_USER"
    ],
    "iat": 1422986529
}

Signature

Depends on the algorithm specified on the header, it can be a digital signature of the base64-encoded header and claims, or an encryption of them.

8.3.1 Reading JWT Token

8.3.1.1 Bearer Token Reader

Micronaut supports the RFC 6750 Bearer Token specification for transmitting JWT tokens. The following sequence illustrates the RFC 6750 authentication flow:

jwt bearer token

The following configuration properties are available to customize how the Bearer Token will be read:

🔗
Table 1. Configuration Properties for BearerTokenConfigurationProperties
Property Type Description

micronaut.security.token.jwt.bearer.enabled

boolean

Set whether to enable bearer token authentication. Default value true.

micronaut.security.token.jwt.bearer.header-name

java.lang.String

Sets the header name to use. Default value Authorization.

micronaut.security.token.jwt.bearer.prefix

java.lang.String

Sets the prefix to use for the auth token. Default value Bearer.

Sending tokens in the request

The code snippet below illustrates how to send a JWT token in the Authorization request header, using the bearerAuth method from MutableHttpRequest method:

Accessing a protected resource using an Authorization request header
String accessToken = rsp.body().accessToken
List<Book> books = gatewayClient.toBlocking().retrieve(HttpRequest.GET("/api/gateway")
        .bearerAuth(accessToken), Argument.listOf(Book))
Example of HTTP GET request using Authorization request header
GET /protectedResource HTTP/1.1
Host: micronaut.example`
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjI5OTU5MjIsInN1YiI6ImppbWkiLCJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sImlhdCI6MTQyMjk5MjMyMn0.rA7A2Gwt14LaYMpxNRtrCdO24RGrfHtZXY9fIjV8x8o
Check out the Micronaut JWT Authentication guide for a tutorial on Micronaut’s JWT support.

8.3.1.2 Cookie Token Reader

You can send/read a JWT token from a Cookie too.

The following sequence illustrates the authentication flow:

jwt cookie

Reading tokens from Cookies is disabled by default. Note that using JWT tokens from cookies requires JWT Authentication to be enabled.

🔗
Table 1. Configuration Properties for JwtCookieConfigurationProperties
Property Type Description

micronaut.security.token.jwt.cookie.cookie-domain

java.lang.String

Sets the domain name of this Cookie.

micronaut.security.token.jwt.cookie.cookie-path

java.lang.String

Sets the path of the cookie. Default value ("/".

micronaut.security.token.jwt.cookie.cookie-http-only

java.lang.Boolean

Whether the Cookie can only be accessed via HTTP. Default value (true.

micronaut.security.token.jwt.cookie.cookie-secure

java.lang.Boolean

Sets whether the cookie is secured. Default value (true.

micronaut.security.token.jwt.cookie.cookie-max-age

java.time.Duration

Sets the maximum age of the cookie.

micronaut.security.token.jwt.cookie.cookie-same-site

SameSite

Sets the same-site setting of the cookie. Default value null.

micronaut.security.token.jwt.cookie.enabled

boolean

Sets whether JWT cookie configuration is enabled. Default value (true).

micronaut.security.token.jwt.cookie.cookie-name

java.lang.String

Cookie Name. Default value ("JWT").

micronaut.security.token.jwt.cookie.logout-target-url

java.lang.String

Deprecated. user micronaut.security.redirect.logout instead

micronaut.security.token.jwt.cookie.login-success-target-url

java.lang.String

Deprecated. user micronaut.security.redirect.login-success instead

micronaut.security.token.jwt.cookie.login-failure-target-url

java.lang.String

Deprecated. user micronaut.security.redirect.login-failure instead

8.3.2 JWT Token Generation

Micronaut relies on Nimbus JOSE + JWT library to provide JWT token signature and encryption.

The following configuration options are available:

🔗
Table 1. Configuration Properties for AccessTokenConfigurationProperties
Property Type Description

micronaut.security.token.jwt.generator.access-token-expiration

java.lang.Integer

deprecated Use micronaut.security.token.jwt.generator.access-token.expiration instead.

micronaut.security.token.jwt.generator.access-token.expiration

java.lang.Integer

Access token expiration. Default value (3600).

8.3.2.1 JWT Signature

Micronaut security capabilities use signed JWT’s as specified by the JSON Web Signature specification.

To enable a JWT signature in token generation, you need to have in your app a bean of type SignatureGeneratorConfiguration qualified with name generator.

You can setup a SecretSignatureConfiguration named generator easily via configuration properties:

micronaut:
  security:
    token:
      jwt:
        signatures:
          secret:
            generator:
              secret: pleaseChangeThisSecretForANewOne (1)
              jws-algorithm: HS256 (2)
1 Change this for your own secret and keep it safe.
2 Json Web Token Signature name. In this example, HMAC using SHA-256 hash algorithm.

You can supply the secret with Base64 encoding.

micronaut:
  security:
    token:
      jwt:
        signatures:
          secret:
            generator:
              secret: 'cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU=' (1)
              base64: true (2)
              jws-algorithm: HS256
1 Secret Base64 encoded
2 Signal that the secret is Base64 encoded

A programmatic setup of a RSA signature generation may look like

@Factory
class MySignatureGeneratorConfigurationFactory {
    @Bean
    @Named("generator") (1)
    SignatureGeneratorConfiguration signatureGeneratorConfiguration(RSASignatureGeneratorConfiguration configuration) {(2)
        return new RSASignatureGenerator(configuration)
    }
}
1 Name the SignatureGeneratorConfiguration generator to make it participate in JWT token generation.
2 Register an additional bean of type RSASignatureGeneratorConfiguration which is injected here

8.3.2.2 Encrypted JWTs

Signed claims prevents an attacker to tamper its contents to introduce malicious data or try a privilege escalation by adding more roles. However, the claims can be decoded just by using Base 64.

If the claims contains sensitive information, you can use a JSON Web Encryption algorithm to prevent them to be decoded.

To enable a JWT encryption in token generation, you need to have in your app a bean of type RSAEncryptionConfiguration, ECEncryptionConfiguration, SecretEncryptionConfiguration qualified with name generator.

Example of JWT Signed with Secret and Encrypted with RSA

Setup a SecretSignatureConfiguration through configuration properties

micronaut:
  security:
    token:
      jwt:
        signatures:
          secret:
            generator:
              secret: pleaseChangeThisSecretForANewOne (1)
              jws-algorithm: HS256 (2)
pem:
  path: /home/user/rsa-2048bit-key-pair.pem (2)
1 Name the Signature configuration generator to make it participate in JWT token generation.
2 Location of PEM file

Generate a 2048-bit RSA private + public key pair:

openssl genrsa -out rsa-2048bit-key-pair.pem 2048
@Named("generator") (1)
@Singleton
class RSAOAEPEncryptionConfiguration implements RSAEncryptionConfiguration {

    private RSAPrivateKey rsaPrivateKey
    private RSAPublicKey rsaPublicKey
    JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256
    EncryptionMethod encryptionMethod = EncryptionMethod.A128GCM

    RSAOAEPEncryptionConfiguration(@Value('${pem.path}') String pemPath) {
        Optional<KeyPair> keyPair = KeyPairProvider.keyPair(pemPath)
        if (keyPair.isPresent()) {
            this.rsaPublicKey = (RSAPublicKey) keyPair.get().getPublic()
            this.rsaPrivateKey = (RSAPrivateKey) keyPair.get().getPrivate()
        }
    }

    @Override
    RSAPublicKey getPublicKey() {
        return rsaPublicKey
    }

    @Override
    RSAPrivateKey getPrivateKey() {
        return rsaPrivateKey
    }

    @Override
    JWEAlgorithm getJweAlgorithm() {
        return jweAlgorithm
    }

    @Override
    EncryptionMethod getEncryptionMethod() {
        return encryptionMethod
    }
}
1 Name Bean generator to designate this bean as participant in the JWT Token Generation.

To parse the PEM key, use a collaborator as described in OpenSSL key generation.

@Slf4j
class KeyPairProvider {
    /**
     *
     * @param pemPath Full path to PEM file.
     * @return returns KeyPair if successfully for PEM files.
     */
    static Optional<KeyPair> keyPair(String pemPath) {
        // Load BouncyCastle as JCA provider
        Security.addProvider(new BouncyCastleProvider())

        // Parse the EC key pair
        PEMParser pemParser
        try {
            pemParser = new PEMParser(new InputStreamReader(Files.newInputStream(Paths.get(pemPath))))
            PEMKeyPair pemKeyPair = (PEMKeyPair) pemParser.readObject()

            // Convert to Java (JCA) format
            JcaPEMKeyConverter converter = new JcaPEMKeyConverter()
            KeyPair keyPair = converter.getKeyPair(pemKeyPair)
            pemParser.close()

            return Optional.of(keyPair)

        } catch (FileNotFoundException e) {
            log.warn("file not found: {}", pemPath)

        } catch (PEMException e) {
            log.warn("PEMException {}", e.getMessage())

        } catch (IOException e) {
            log.warn("IOException {}", e.getMessage())
        }
        return Optional.empty()
    }
}

8.3.3 JWT Token Validation

Micronaut’s JWT validation support multiple Signature and Encryption configurations.

Any beans of type RSASignatureConfiguration, ECSignatureConfiguration, SecretSignatureConfiguration participate as signature configurations in the JWT validation.

Any beans of type RSAEncryptionConfiguration, ECEncryptionConfiguration, SecretEncryptionConfiguration participate as encryption configurations in the JWT validation.

8.3.3.1 Validation with remote JWKS

A JSON Web Key (JWK) is a JSON object that represents a cryptographic key. You can use a remote JWK Set, A JSON object that represents a set of JWKs, to validate JWT signatures.

You can configure a remote JWKS as a signature validator:

micronaut:
  security:
    token:
      jwt:
        signatures:
          jwks:
            awscognito:
              url: 'https://cognito-idp.eu-west-1.amazonaws.com/eu-west-XXXX/.well-known/jwks.json'

The previous snippet creates a JwksSignature bean with a awscognito name qualifier.

If you want to expose your own JWK Set, read the Keys Controller section.

8.3.4 Claims Generation

If the built-in JWTClaimsSetGenerator, does not fulfil your needs you can provide your own replacement of ClaimsGenerator.

For example, if you want to add the email address of the user to the JWT Claims you could create a class which extends UserDetails:

public class EmailUserDetails extends UserDetails {

    private String email;

    public EmailUserDetails(String username, Collection<String> roles) {
        super(username, roles);
    }


    public EmailUserDetails(String username, Collection<String> roles, String email) {
        super(username, roles);
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Configure your AuthenticationProvider to respond such a class:

@Singleton
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Publisher<AuthenticationResponse> authenticate(HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) {
        return Flowable.create(emitter -> {
            emitter.onNext(new EmailUserDetails("sherlock", Collections.emptyList(), "sherlock@micronaut.example"));
                emitter.onComplete();
        }, BackpressureStrategy.ERROR);
    }
}

And then replace JWTClaimsSetGenerator with a bean that overrides the method populateWithUserDetails:

@Singleton
@Replaces(bean = JWTClaimsSetGenerator.class)
public class CustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator {

    public CustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration,
                                       @Nullable JwtIdGenerator jwtIdGenerator,
                                       @Nullable ClaimsAudienceProvider claimsAudienceProvider,
                                       @Nullable ApplicationConfiguration applicationConfiguration) {
        super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider, applicationConfiguration);
    }

    @Override
    protected void populateWithUserDetails(JWTClaimsSet.Builder builder, UserDetails userDetails) {
        super.populateWithUserDetails(builder, userDetails);
        if (userDetails instanceof EmailUserDetails) {
            builder.claim("email", ((EmailUserDetails)userDetails).getEmail());
        }
    }
}

8.3.5 Token Render

When you use bearer authentication and the built-in LoginController, the JWT tokens are returned to the client as part of an OAuth 2.0 RFC6749 access token response.

An example of such a response is:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
    "access_token":"eyJhbGciOiJIUzI1NiJ9...",
    "token_type":"Bearer",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA...",
    "username": "euler",
    "roles": [
        "ROLE_USER"
    ],
}

If you wish to customize the previous JSON payload, you may want to provide a bean replacement for BearerTokenRenderer. If that is not enough, check the AccessRefreshTokenLoginHandler to accommodate it to your needs.

8.4 LDAP Authentication

Micronaut supports authentication with LDAP out of the box. To get started, add the security-ldap dependency to your application.

build.gradle
compile "io.micronaut.configuration:micronaut-security-ldap"
Read the LDAP and Database authentication providers to see an example.

LDAP authentication can be globally disabled by setting micronaut.security.ldap.enabled to false, or on a provider basis, eg micronaut.security.ldap.default.enabled: false.

8.4.1 Configuration

The LDAP authentication in Micronaut supports configuration of one or more LDAP servers to authenticate with. Each server has it’s own settings and can be enabled or disabled.

🔗
Table 1. Configuration Properties for LdapConfiguration
Property Type Description

micronaut.security.ldap.*.enabled

boolean

Sets whether this configuration is enabled. Default true.

🔗
Table 2. Configuration Properties for LdapConfiguration$ContextConfiguration
Property Type Description

micronaut.security.ldap.*.context.server

java.lang.String

Sets the server URL.

micronaut.security.ldap.*.context.manager-dn

java.lang.String

Sets the manager DN.

micronaut.security.ldap.*.context.manager-password

java.lang.String

Sets the manager password.

micronaut.security.ldap.*.context.factory

java.lang.String

Sets the context factory class. Default "com.sun.jndi.ldap.LdapCtxFactory"

micronaut.security.ldap.*.context.properties

java.util.Map

Any additional properties that should be passed to {@link javax.naming.directory.InitialDirContext#InitialDirContext(java.util.Hashtable)}.

🔗
Table 3. Configuration Properties for LdapConfiguration$SearchConfiguration
Property Type Description

micronaut.security.ldap.*.search.subtree

boolean

Sets if the subtree should be searched. Default true

micronaut.security.ldap.*.search.base

java.lang.String

Sets the base DN to search.

micronaut.security.ldap.*.search.filter

java.lang.String

Sets the search filter. Default "(uid={0})"

micronaut.security.ldap.*.search.attributes

java.lang.String[]

Sets the attributes to return. Default all

🔗
Table 4. Configuration Properties for LdapConfiguration$GroupConfiguration
Property Type Description

micronaut.security.ldap.*.groups.enabled

boolean

Sets if group search is enabled. Default false

micronaut.security.ldap.*.groups.subtree

boolean

Sets if the subtree should be searched. Default true

micronaut.security.ldap.*.groups.base

java.lang.String

Sets the base DN to search from.

micronaut.security.ldap.*.groups.filter

java.lang.String

Sets the group search filter. Default "uniquemember={0}"

micronaut.security.ldap.*.groups.attribute

java.lang.String

Sets the group attribute name. Default "cn"

To connect to an LDAP server with SSL, set the standard Java system properties to the values appropriate for your server.
-Djavax.net.ssl.trustStore="<path to truststore file>"
-Djavax.net.ssl.trustStorePassword="<passphrase for truststore>"

8.4.2 Extending Default Behavior

This section will outline some common requirements that will require custom code to implement and describe what to do in those cases.

Authentication Data

The authentication object returned from a successful authentication request is by default an instance of UserDetails, which only contains the username and any roles associated with the user. To store additional data in the authentication, extend UserDetails with your own implementation that has fields for the additional data you wish to store.

To use this new implementation, you must override the DefaultContextAuthenticationMapper and provide your own implementation.

MyContextAuthenticationMapper.java
@Singleton
@Replaces(DefaultContextAuthenticationMapper.class) (1)
public class MyContextAuthenticationMapper implements ContextAuthenticationMapper {

    @Override
    public AuthenticationResponse map(ConvertibleValues<Object> attributes, String username, Set<String> groups) {
        // return an extended UserDetails or an AuthenticationFailed object
    }
}
1 The usage of @Replaces will allow your bean to replace the default implementation in the context

Groups

By default the groups found in LDAP, if enabled, will be returned as is without any processing. No additional groups from any other sources will be added to the list. It is a common requirement to retrieve additional groups from other sources, or to normalize the names of the groups in a specific format.

To extend this behavior, it is necessary to create your own implementation of LdapGroupProcessor. Likely it will be desired to extend the default implementation because it has the logic for querying the groups from LDAP and executes the other methods to process the groups and query for additional groups.

MyLdapGroupProcessor.java
@Singleton
@Replaces(DefaultLdapGroupProcessor.class) (1)
public class MyLdapGroupProcessor extends DefaultLdapGroupProcessor {

    Set<String> getAdditionalGroups(LdapSearchResult result) { (2)
        //Use the result to query another source for additional groups (database, etc)
    }

    Optional<String> processGroup(String group) { (3)
        //convert "Admin" to "ROLE_ADMIN" for example
        //return an empty optional to exclude the group
    }
}
1 The usage of @Replaces will allow your bean to replace the default implementation in the context
2 The getAdditionalGroups method allows you to add groups from other sources
3 The processGroup method allows you to transform the name of the group, or exclude it

Search Logic

To customize how LDAP searches are done, replace the default implementation with your own. See LdapSearchService.

MyLdapSearchService.java
@Singleton
@Replaces(DefaultLdapSearchService.class)
public class MyLdapSearchService implements LdapSearchService {

}

Context Building

To customize how the LDAP context is built, replace the default implementation with your own. See ContextBuilder.

MyContextBuilder.java
@Singleton
@Replaces(DefaultContextBuilder.class)
public class MyContextBuilder implements ContextBuilder {

}

8.5 Custom Authorization Strategies

All authorization strategies implement the AuthenticationFetcher interface. The contract is designed to return an Authentication object from the request. To implement custom logic to retrieve the currently logged in user, simply create a bean that implements the contract and it will be picked up automatically.

For example to retrieve information from an X509 certificate:

X509 Authentication Fetcher
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.filters.AuthenticationFetcher;
import io.reactivex.Maybe;
import org.reactivestreams.Publisher;

import edu.umd.cs.findbugs.annotations.NonNull;
import javax.inject.Singleton;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
@Singleton
public class X509AuthenticationFetcher implements AuthenticationFetcher {

    @Override
    public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {
        return Maybe.<Authentication>create(emitter -> {
            Optional<Certificate> optionalCertificate = request.getCertificate();
            if (optionalCertificate.isPresent()) {
                Certificate certificate = optionalCertificate.get();
                if (certificate instanceof X509Certificate) {
                    emitter.onSuccess(new Authentication() {
                        X509Certificate x509Certificate = ((X509Certificate) certificate);
                        @Override
                        public String getName() {
                            return x509Certificate.getIssuerX500Principal().getName();
                        }

                        @NonNull
                        @Override
                        public Map<String, Object> getAttributes() {
                            return Collections.emptyMap();
                        }
                    });
                    return;
                }
            }
            emitter.onComplete();
        }).toFlowable();
    }
}

9 Rejection Handling

Micronaut allows the customization of the response that is sent when a request is not authorized to access a resource, or is not authenticated and the resource requires authentication.

When a request is rejected, the security filter emits an AuthorizationException. The default implementation (DefaultAuthorizationExceptionHandler) redirects based on the redirect configuration only if the request accepts text/html:

🔗
Table 1. Configuration Properties for RedirectConfigurationProperties
Property Type Description

micronaut.security.redirect.login-success

java.lang.String

Where the user is redirected to after a successful login. Default value ("/").

micronaut.security.redirect.login-failure

java.lang.String

Where the user is redirected to after a failed login. Default value ("/").

micronaut.security.redirect.logout

java.lang.String

URL where the user is redirected after logout. Default value ("/").

micronaut.security.redirect.prior-to-login

boolean

If true, the user should be redirected back to the unauthorized request that initiated the login flow. Supersedes the <code>login-success</code> configuration for those cases. Default value false.

For an unauthorized request, a 401 http response will be sent if unauthorized.enabled is false, or the request does not accept text/html.

For a rejected request, a 403 http response will be sent if forbidden.enabled is false, or the request does not accept text/html.

To fully customize the behavior, replace the relevant bean with your own implementation.

For example:

import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.security.authentication.AuthorizationException;
import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler;

import javax.inject.Singleton;

@Singleton
@Replaces(DefaultAuthorizationExceptionHandler.class)
public class MyRejectionHandler extends DefaultAuthorizationExceptionHandler {

    @Override
    public MutableHttpResponse<?> handle(HttpRequest request, AuthorizationException exception) {
        //Let the DefaultAuthorizationExceptionHandler create the initial response
        //then add a header
        return super.handle(request, exception).header("X-Reason", "Example Header");
    }
}

10 Token Propagation

Imagine you have a Gateway microservice which consumes three other microservices:

tokenpropagation

If the incoming request localhost:8080/api/books contains a valid JWT token, you may want to propagate that token to other requests in your network.

You can configure token propagation to achieve that.

micronaut:
    application:
        name: gateway
    security:
        token:
            jwt:
                signatures:
                    secret:
                        generator:
                            secret: "pleaseChangeThisSecretForANewOne"
                            jws-algorithm: HS256
            propagation:
                header:
                    enabled: true
                    headerName: "Authorization"
                    prefix: "Bearer "
                enabled: true
                service-id-regex: "http://localhost:(8083|8081|8082)"

The previous configuration, configures a HttpHeaderTokenPropagator and a and a propagation filter, TokenPropagationHttpClientFilter, which will propagate the security token seamlessly.

If you use Service Discovery features, you can use the service id in your service id regular expression:

micronaut.security.token.propagation.service-id-regex="catalogue|recommendations|inventory"

Several configuration options are available:

🔗
Table 1. Configuration Properties for TokenPropagationConfigurationProperties
Property Type Description

micronaut.security.token.propagation.enabled

boolean

Enables TokenPropagationHttpClientFilter. Default value false

micronaut.security.token.propagation.service-id-regex

java.lang.String

a regular expression to match the service id.

micronaut.security.token.propagation.uri-regex

java.lang.String

a regular expression to match the uri.

micronaut.security.token.propagation.path

java.lang.String

Configures TokenPropagationHttpClientFilter path. Default value "/**"

For propagation via an HTTP Header, you can configure:

🔗
Table 2. Configuration Properties for HttpHeaderTokenPropagatorConfigurationProperties
Property Type Description

micronaut.security.token.propagation.header.prefix

java.lang.String

Value prefix for Http Header. Default value (HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER).

micronaut.security.token.propagation.header.header-name

java.lang.String

Http Header to be used to propagate the token. Default value (HttpHeaders.AUTHORIZATION)

micronaut.security.token.propagation.header.enabled

boolean

Enable HttpHeaderTokenPropagator. Default value (true).

Read the Token Propagation tutorial to learn more.

11 Built-In Security Controllers

11.1 Login Controller

To enable the LoginController you need to have a bean of type LoginHandler. A custom implementation can, of course, be provided. However, several Login Handlers implementations are available out of the box.

You can configure the LoginController with:

🔗
Table 1. Configuration Properties for LoginControllerConfigurationProperties
Property Type Description

micronaut.security.endpoints.login.enabled

boolean

Enables LoginController. Default value true

micronaut.security.endpoints.login.path

java.lang.String

Path to the LoginController. Default value "/login"

Login Endpoint invocation example

Login Endpoint invocation example
curl -X "POST" "http://localhost:8080/login" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "username": "euler",
  "password": "password"
}'

11.2 Logout Controller

To enable the logout controller you need a bean of type LogoutHandler. The behaviour of the controller is delegated to it. A custom implementation can, of course, be provided. However, several Logout Handlers implementations are available out of the box.

You can configure the logout endpoint with:

🔗
Table 1. Configuration Properties for LogoutControllerConfigurationProperties
Property Type Description

micronaut.security.endpoints.logout.enabled

boolean

Enables LogoutController. Default value true.

micronaut.security.endpoints.logout.path

java.lang.String

Path to the LogoutController. Default value "/logout".

micronaut.security.endpoints.logout.get-allowed

boolean

Enables HTTP GET invocations of LogoutController. Default value (false).

If you are using JWT authentication not stored in a cookie, you may not need to invoke the /logout endpoint. Since logging out normally means simply deleting the JWT token in the client.

Logout Endpoint invocation example

Logout endpoint invocation example
curl -X "POST" "http://localhost:8080/logout"

11.3 Refresh Controller

The refresh token functionality has changed dramatically starting in Micronaut Security 2.0. Please read this section if you are upgrading as it now behaves differently.

Refresh tokens can be used to obtain a new access token. By default, refresh tokens are not generated.

The RefreshTokenGenerator API is responsible for generating the token that gets included in the response. The RefreshTokenValidator is responsible for validating the refresh token. Note that this validation step is not related to the persistence of the token, but instead is intended to verify the token is not a random/guessed value.

An implementation of both RefreshTokenGenerator and RefreshTokenValidator has been provided by default. The SignedRefreshTokenGenerator creates and verifies a JWS (JSON web signature) encoded object whose payload is a UUID with a hash-based message authentication code (HMAC). See the following configuration options:

🔗
Table 1. Configuration Properties for RefreshTokenConfigurationProperties
Property Type Description

micronaut.security.token.jwt.generator.refresh-token.enabled

boolean

Sets whether SignedRefreshTokenGenerator is enabled. Default value (true).

micronaut.security.token.jwt.generator.refresh-token.jws-algorithm

com.nimbusds.jose.JWSAlgorithm

{@link com.nimbusds.jose.JWSAlgorithm}. Defaults to HS256

micronaut.security.token.jwt.generator.refresh-token.secret

java.lang.String

shared secret. For HS256 must be at least 256 bits.

micronaut.security.token.jwt.generator.refresh-token.base64

boolean

Indicates whether the supplied secret is base64 encoded. Default value false.

To enable it, you must provide a secret:

micronaut:
    security:
        token:
            jwt:
                generator:
                    refresh-token:
                        secret: 'pleaseChangeThisSecretForANewOne'

After a token is generated, this library has no knowledge of it. The value is not cached or stored anywhere. It is up to each application to decide how to store the token, support revocation, and retrieve user details when given a token.

In addition to the above requirements, each application must provide an implementation of RefreshTokenPersistence.

The RefreshTokenPersistence implementation will receive an event when a refresh token is generated and then is responsible for persisting the token along with a link to the user that it was generated for. The user information and the token are both available in the RefreshTokenGeneratedEvent.

Refreshing the Token

Micronaut security comes with a controller to allow for the refresh of access tokens. The context loads the OauthController if your context contains beans of type: AccessRefreshTokenGenerator, RefreshTokenPersistence, RefreshTokenValidator

Moreover, the controller can be configured with:

🔗
Table 2. Configuration Properties for OauthControllerConfigurationProperties
Property Type Description

micronaut.security.endpoints.oauth.enabled

boolean

Sets whether the OauthController is enabled. Default value (true).

micronaut.security.endpoints.oauth.path

java.lang.String

Sets the path to map the OauthController to. Default value ("/oauth/access_token").

micronaut.security.endpoints.oauth.get-allowed

boolean

Enables HTTP GET invocations of refresh token requests. Only applies to requests sending a cookie (JWT_REFRESH_TOKEN). Default value (true).

The controller exposes an endpoint as defined by section 6 of the OAuth 2.0 spec - Refreshing an Access Token.

The refresh token endpoint uses the RefreshTokenValidator API to verify the token matches the format that is expected. SignedRefreshTokenGenerator attempts to verify the signature and returns the payload. Any validator implementations should not be concerned with revocation status, existence, or any other persistence related validation.

If the validator successfully validates the token, it is then passed to a RefreshTokenPersistence implementation, which each application must provide. A new access token is then created from the user details returned by RefreshTokenPersistence::getUserDetails and then sent in the response.

Here is an example of a refresh token request. Send a POST request to the /oauth/access_token endpoint:

Sample HTTP request to obtain an access token
POST /oauth/access_token HTTP/1.1
Host: micronaut.example
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ....

As you can see, is a form request with 2 parameters:

grant_type: must be refresh_token always.

refresh_token: the refresh token obtained previously.

Refresh tokens must be securely stored in your client application. See section 10.4 of the OAuth 2.0 spec for more information.

11.4 Keys Controller

This controller can only be enabled if you are using JWT authentication.

A JSON Web Key (JWK) is a JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value.

Meanwhile, a JWK Set is a JSON object that represents a set of JWKs. The JSON object MUST have a "keys" member, which is an array of JWKs.

To enable the KeysController you have to provide at least a bean of type: JwkProvider.

Moreover, you can configure it with:

🔗
Table 1. Configuration Properties for KeysControllerConfigurationProperties
Property Type Description

micronaut.security.endpoints.keys.enabled

boolean

Enables KeysController. Default value true.

micronaut.security.endpoints.keys.path

java.lang.String

Path to the KeysController. Default value "/keys".

12 Retrieve the authenticated user

Often you may want to retrieve the authenticated user.

You can bind java.security.Principal as a method’s parameter in a controller.

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.security.Principal;

@Controller("/user")
public class UserController {

    @Secured("isAnonymous()")
    @Get("/myinfo")
    public Map myinfo(@Nullable Principal principal) {
        if (principal == null) {
            return Collections.singletonMap("isLoggedIn", false);
        }
        return CollectionUtils.mapOf("isLoggedIn", true, "username", principal.getName());
    }
}

If you need a greater level of detail, you can bind Authentication as a method’s parameter in a controller.

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import edu.umd.cs.findbugs.annotations.Nullable


@Controller("/user")
public class UserController {

    @Secured("isAnonymous()")
    @Get("/myinfo")
    public Map myinfo(@Nullable Authentication authentication) {
        if (authentication == null) {
            return Collections.singletonMap("isLoggedIn", false);
        }
        return CollectionUtils.mapOf("isLoggedIn", true,
                "username", authentication.getName(),
                "roles", authentication.getAttributes().get("roles")
        );
    }
}

12.1 User outside of a controller

If you need to access the currently authenticated user outside of a controller, you can inject SecurityService bean, which provides a set of convenient methods related to authentication and authorization.

13 Security Events

Micronaut security classes generate several ApplicationEvents which you can subscribe to.

Event Name

Description

LoginFailedEvent

Triggered when an unsuccessful login takes place.

LoginSuccessfulEvent

Triggered when a successful login takes place.

LogoutEvent

Triggered when the user logs out.

TokenValidatedEvent

Triggered when a token is validated.

AccessTokenGeneratedEvent

Triggered when a JWT access token is generated.

RefreshTokenGeneratedEvent

Triggered when a JWT refresh token is generated.

To learn how to listen for events, see the Context Events section of the documentation.

14 OAuth 2.0

Micronaut supports authentication with OAuth 2.0 servers, including support for the OpenID standard.

The easiest way to get started is by configuring a provider that supports OpenID. Platforms such as Okta, Auth0, AWS Cognito, Keycloak, and Google are common examples.

Once you create an application client with a provider, you will get a client id and a client secret. The client id and secret combined with the issuer URL is all that is needed to enable the authentication code grant flow with an OpenID provider.

For example, to configure Google as a provider:

micronaut:
  security:
    oauth2:
      clients:
        google:
          client-secret: <<your client secret>>
          client-id: <<your client id>>
          openid:
            issuer: https://accounts.google.com

For normal OAuth 2.0, different steps are required to allow for the authorization code grant flow.

For the full authorization code flow to work there are also some additional requirements. A LoginHandler must be in the context to determine how to respond after a failed or successful login. A custom implementation can, of course, be provided. However, several Login Handlers implementations are available out of the box.

See the following guide for a full walk-through with Okta.

14.1 Installation

implementation("io.micronaut:micronaut-security-oauth2")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-security-oauth2</artifactId>
</dependency>

To use the BUILD-SNAPSHOT version of this library, check the documentation to use snapshots.

Code can be found at the micronaut-security repository.

14.2 OpenID Connect

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.

If you are new to OpenID Connect, we recommend watching OAuth 2.0 and OpenID Connect to get a better understanding.

To use OpenID client flows, the security-jwt dependency must be in your build because OpenID relies on JWT tokens.

implementation("io.micronaut:micronaut-security-jwt")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-security-jwt</artifactId>
</dependency>

Even though OpenID providers return JWT tokens, that token is not used directly to authorize requests with Micronaut. Instead a new token is created if the application is using JWT. This allows for authorization to be standardized across custom authentication providers, normal OAuth 2.0 flows, and OpenID flows. It also allows for purely session based authorization as a result of OpenID authentication.

14.3 Flows

14.3.1 Authorization Code

The authorization code grant flow is the most typical authentication flow with OAuth 2.0 and OpenID providers. The same main steps apply to the flow whether or not the provider supports OpenID, and is described in RFC6749 - Authorization Code Grant.

The OAuth 2.0 authorization code flow requires a callback endpoint. In addition, a login endpoint is available to trigger the flow. The URIs are configurable.

🔗
Table 1. Configuration Properties for OauthConfigurationProperties
Property Type Description

micronaut.security.oauth2.enabled

boolean

Sets whether the OAuth 2.0 support is enabled. Default value (true).

micronaut.security.oauth2.callback-uri

java.lang.String

The URI template that OAuth 2.0 providers can use to submit an authorization callback request. Default value ("/oauth/callback{/provider}").

micronaut.security.oauth2.login-uri

java.lang.String

The URI template that is used to initiate an OAuth 2.0 authorization code grant flow. Default value ("/oauth/login{/provider}").

micronaut.security.oauth2.default-provider

java.lang.String

The default authentication provider for an OAuth 2.0 authorization code grant flow.

The URI templates for login and callback have a template variable in them {provider}. That variable is used by the route builder to build routes for each provider that is configured. The name provider is special in this context and cannot be changed. The URI may be manipulated however in any way as long as the provider variable is part of the path of the URI.

For example /oauth/login{?provider} is not a valid configuration because Micronaut does not consider the query segment of a URL when routing requests. The provider variable must be part of the path.

It is possible to designate a default provider. The value of the configuration must match one of the client names. The default provider will have the same uri template, but with null passed as the provider parameter. By default that will result in /oauth/login being redirected to the default provider authentication page.

OAuth Login and CSRF

In order to prevent forced login attacks, you must implement CSRF protection on the oauth login endpoints. Because this library is not in control of the forms where login may originate from, we cannot ensure that CSRF is applied. In addition, there is no sensible defaults with regards to how the token is stored or how it should be retrieved. Implementing CSRF is relatively simple.

  1. When rendering the login form where users can choose to login via OAuth, the server should store a value either in state on the server specific to that user (session), or state on the client (http-only cookies, etc), or some other mechanism.

  2. That value gets sent with the response and subsequently included in the request to login with OAuth. The value could be sent as a query parameter, or sent as a cookie, etc.

  3. A server filter is written to compare the value in the user specific state and the value sent in the request. If they do not match, the request is rejected.

Here is an example:

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.filter.OncePerRequestHttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;


@Filter(value = {"/oauth/login", "/oauth/login/*"})
public class OAuthCsrfFilter extends OncePerRequestHttpServerFilter {

    @Override
    protected Publisher<MutableHttpResponse<?>> doFilterOnce(HttpRequest<?> request, ServerFilterChain chain) {
        String requestParameter = request.getParameters().get("_csrf");
        String cookieValue = request.getCookies().findCookie("_csrf").map(Cookie::getValue).orElse(null);

        if (cookieValue == null || !cookieValue.equals(requestParameter)) {
            return Publishers.just(HttpResponse.status(HttpStatus.FORBIDDEN));
        }

        return chain.proceed(request);
    }
}
import io.micronaut.context.annotation.Requires
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.OncePerRequestHttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher


@Filter(value = ["/oauth/login", "/oauth/login/*"])
class OAuthCsrfFilter extends OncePerRequestHttpServerFilter {

    @Override
    protected Publisher<MutableHttpResponse<?>> doFilterOnce(HttpRequest<?> request, ServerFilterChain chain) {
        String requestParameter = request.parameters.get("_csrf")
        String cookieValue = request.cookies.findCookie("_csrf").map({c -> c.getValue()}).orElse(null)

        if (cookieValue == null || cookieValue != requestParameter) {
            return Publishers.just(HttpResponse.status(HttpStatus.FORBIDDEN))
        }

        return chain.proceed(request)
    }
}
import io.micronaut.context.annotation.Requires
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.cookie.Cookie
import io.micronaut.http.filter.OncePerRequestHttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher


@Filter(value = ["/oauth/login", "/oauth/login/*"])
class OAuthCsrfFilter : OncePerRequestHttpServerFilter() {

    override fun doFilterOnce(request: HttpRequest<*>, chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
        val requestParameter = request.parameters["_csrf"]
        val cookieValue = request.cookies.findCookie("_csrf").map { obj: Cookie -> obj.value }.orElse(null)

        return if (cookieValue == null || cookieValue != requestParameter) {
            Publishers.just(HttpResponse.status<Any>(HttpStatus.FORBIDDEN))
        } else {
            chain.proceed(request)
        }
    }
}

14.3.1.1 OAuth 2.0

It’s possible to configure authorization with an OpenID provider simply with this library because OpenID standardizes how to retrieve user information from the provider. Because a user information endpoint is not part of the OAuth 2.0 specification, it is up to you to provide an implementation to retrieve that information.

Here is a high level diagram of how the authorization code grant flow works with an OAuth 2.0 provider.

standard oauth

14.3.1.1.1 Configuration

The minimum requirements to allow authorization with an OAuth 2.0 provider are:

  • Configuration of the authorization endpoint

  • Configuration of the token endpoint

  • Configuration of the client id and secret

  • An implementation of OauthUserDetailsMapper

Configuration is quite simple. For example to configure authorization with Github:

micronaut:
  security:
    oauth2:
      clients:
        github: (1)
          client-id: <<my client id>> (2)
          client-secret: <<my client secret>> (3)
          scopes: (4)
            - user:email
            - read:user
          authorization:
            url: https://github.com/login/oauth/authorize (5)
          token:
            url: https://github.com/login/oauth/access_token (6)
            auth-method: client-secret-post (7)
1 Configure a client. The name here is arbitrary
2 The client id
3 The client secret
4 The desired scopes (OPTIONAL)
5 The authorization endpoint URL
6 The token endpoint URL
7 The token endpoint authentication method. One of AuthenticationMethod. Choose the one your provider requires.

Authentication methods are not clearly defined in RFC 6749, however most OAuth 2.0 providers either accept client-secret-basic (basic authentication with id and secret), or client-secret-post (client id and secret are sent in the request body).

To disable a specific client for any given environment, set enabled: false within the client configuration.

14.3.1.1.2 User Details Mapper

Beyond configuration, an implementation of OauthUserDetailsMapper is required by the user to be implemented. The implementation must be qualified by name that matches the name present in the client configuration.

The purpose of the user details mapper is to transform the TokenResponse into a UserDetails. That will entail calling some endpoint the provider exposes to retrieve the user’s information. Once that information is received, the user details can be populated per your requirements.

Common requirements of a user details mapper may be to combine data from the OAuth 2.0 provider with data from a remote database and/or create new user records. The UserDetails object stores three basic properties: username, roles, and arbitrary attributes. All data stored in the user details will be retrievable in controllers that accept an Authentication.

For example, here is how it might be implemented for Github.

Create a class to store the response data:

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.micronaut.core.annotation.Introspected;

@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GithubUser {

    private String login;
    private String name;
    private String email;

    // getters and setters ...
}
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.micronaut.core.annotation.Introspected

@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class GithubUser {

    String login
    String name
    String email
}
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.micronaut.core.annotation.Introspected

@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
class GithubUser {

    var login: String? = null
    var name: String? = null
    var email: String? = null
}

Create an HTTP client to make the request:

import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;

@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
public interface GithubApiClient {

    @Get("/user")
    Flowable<GithubUser> getUser(@Header("Authorization") String authorization);
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Header
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable

@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
interface GithubApiClient {

    @Get("/user")
    Flowable<GithubUser> getUser(@Header("Authorization") String authorization)
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Header
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable

@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
interface GithubApiClient {

    @Get("/user")
    fun getUser(@Header("Authorization") authorization: String): Flowable<GithubUser>
}

Create the user details mapper that pulls it together:

import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper;
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse;
import org.reactivestreams.Publisher;

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

@Named("github") (1)
@Singleton
class GithubUserDetailsMapper implements OauthUserDetailsMapper {

    private final GithubApiClient apiClient;

    GithubUserDetailsMapper(GithubApiClient apiClient) {
        this.apiClient = apiClient;
    } (2)

    @Override
    public Publisher<UserDetails> createUserDetails(TokenResponse tokenResponse) {
        return Publishers.just(new UnsupportedOperationException());
    }

    @Override
    public Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse, @Nullable State state) { (3)
        return apiClient.getUser("token " + tokenResponse.getAccessToken())
                .map(user -> {
                    List<String> roles = Collections.singletonList("ROLE_GITHUB");
                    return new UserDetails(user.getLogin(), roles); (4)
                });
    }
}
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse
import org.reactivestreams.Publisher

import javax.inject.Named
import javax.inject.Singleton

@Named("github") (1)
@Singleton
class GithubUserDetailsMapper implements OauthUserDetailsMapper {

    private final GithubApiClient apiClient

    GithubUserDetailsMapper(GithubApiClient apiClient) { (2)
        this.apiClient = apiClient
    }

    @Override
    Publisher<UserDetails> createUserDetails(TokenResponse tokenResponse) {
        Publishers.just(new UnsupportedOperationException())
    }

    @Override
    Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse, @Nullable State state) { (3)
        apiClient.getUser("token ${tokenResponse.accessToken}")
            .map({ user ->
                new UserDetails(user.login, ["ROLE_GITHUB"]) (4)
            })
    }
}
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse
import org.reactivestreams.Publisher
import java.lang.UnsupportedOperationException

import javax.inject.Named
import javax.inject.Singleton

@Named("github") (1)
@Singleton
internal class GithubUserDetailsMapper(private val apiClient: GithubApiClient) (2)
    : OauthUserDetailsMapper {

    override fun createUserDetails(tokenResponse: TokenResponse?): Publisher<UserDetails> {
        return Publishers.just(UnsupportedOperationException())
    }

    override fun createAuthenticationResponse(tokenResponse: TokenResponse, state: State?): Publisher<AuthenticationResponse> { (3)
        return apiClient.getUser("token " + tokenResponse.accessToken)
                .map { user ->
                    UserDetails(user.login, listOf("ROLE_GITHUB")) (4)
                }
    }
}
1 The bean must have a named qualifier that matches the name in configuration.
2 How the request is made to retrieve the user information is totally up to you, however in this example we’re using a declarative client.
3 The token endpoint response is passed to the method.
4 The user information is converted to a UserDetails.

14.3.1.2 OpenID Connect

Adding support for authorization code flow with an OpenID provider is extremely easy with Micronaut.

Here is a high level diagram of how the authorization code grant flow works with an OpenID provider.

openid oauth

14.3.1.2.1 Configuration

The requirements to allow authorization with an OpenID provider are:

  • Configuration of the client id and secret

  • Configuration of the issuer

src/main/resources/application.yml
micronaut:
    security:
        oauth2:
            clients:
                okta: (1)
                    client-id: <<my client id>> (2)
                    client-secret: <<my client secret>>  (3)
                    openid:
                        issuer: <<my openid issuer>> (4)
1 Configure a client. The name here is arbitrary
2 The client id
3 The client secret
4 The OpenID provider issuer

The issuer URL will be used to discover the endpoints exposed by the provider.

To disable a specific client for any given environment, set enabled: false within the client configuration.

See the following tables for the configuration options:

🔗
Table 1. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.issuer

java.net.URL

URL using the https scheme with no query or fragment component that the Open ID provider asserts as its issuer identifier.

micronaut.security.oauth2.clients.*.openid.configuration-path

java.lang.String

The configuration path to discover openid configuration. Default ("/.well-known/openid-configuration").

micronaut.security.oauth2.clients.*.openid.jwks-uri

java.lang.String

The JWKS signature URI.

🔗
Table 2. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$AuthorizationEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.authorization.response-type

ResponseType

Determines the authorization processing flow to be used. Default value (code).

micronaut.security.oauth2.clients.*.openid.authorization.response-mode

java.lang.String

Mechanism to be used for returning authorization response parameters from the authorization endpoint.

micronaut.security.oauth2.clients.*.openid.authorization.display

Display

Controls how the authentication interface is displayed.

micronaut.security.oauth2.clients.*.openid.authorization.prompt

Prompt

Controls how the authentication server prompts the user.

micronaut.security.oauth2.clients.*.openid.authorization.max-age

java.lang.Integer

Maximum authentication age.

micronaut.security.oauth2.clients.*.openid.authorization.ui-locales

java.util.List

Preferred locales for authentication.

micronaut.security.oauth2.clients.*.openid.authorization.acr-values

java.util.List

Authentication class reference values.

micronaut.security.oauth2.clients.*.openid.authorization.url

java.lang.String

The endpoint URL

🔗
Table 3. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$TokenEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.token.content-type

MediaType

The content type of token endpoint requests. Default value (application/x-www-form-urlencoded).

micronaut.security.oauth2.clients.*.openid.token.url

java.lang.String

The endpoint URL

micronaut.security.oauth2.clients.*.openid.token.auth-method

AuthenticationMethod

Authentication Method

🔗
Table 4. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$EndSessionConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.end-session.enabled

boolean

The end session enabled flag. Default value (true).

micronaut.security.oauth2.clients.*.openid.end-session.url

java.lang.String

The endpoint URL

🔗
Table 5. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$RegistrationEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.registration.url

java.lang.String

The endpoint URL

🔗
Table 6. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$UserInfoEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.user-info.url

java.lang.String

The endpoint URL

14.3.1.2.2 User Details Mapper

Because the OpenID standard returns a JWT token in the token response, it is possible to retrieve information about the user without having to make an additional call. In addition, the data stored in the JWT is standardized so you can use the same code to retrieve that information across providers.

A default implementation of OpenIdUserDetailsMapper has been provided for you to map the JWT token to a UserDetails. The default implementation will carry over any of the specific OpenID JWT claims, as well as potentially include other claims based on configuration. The original provider name will always be included in the JWT with the claim key "oauth2Provider". The following table explains the additional claims.

🔗
Table 1. Configuration Properties for OauthConfigurationProperties$OpenIdConfigurationProperties$AdditionalClaimsConfigurationProperties
Property Type Description

micronaut.security.oauth2.openid.additional-claims.jwt

boolean

Set to true if the original JWT from the provider should be included in the Micronaut JWT. Default value (false).

micronaut.security.oauth2.openid.additional-claims.access-token

boolean

Set to true if the original access token from the provider should be included in the Micronaut JWT. Default value (false).

micronaut.security.oauth2.openid.additional-claims.refresh-token

boolean

Set to true if the original refresh token from the provider should be included in the Micronaut JWT. Default value (false).

Enabling all of the above with cookie JWT storage has been known to cause issues with Keycloak due to their tokens being very large and causing the resulting cookie to be larger than what browsers allow.

If the default implementation is not sufficient, it is possible to override the global default or provide an implementation specific to a provider.

To override the global default mapper, register a bean that replaces DefaultOpenIdUserDetailsMapper.

import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper;

import edu.umd.cs.findbugs.annotations.NonNull;
import javax.inject.Singleton;
import java.util.Collections;

@Singleton
@Replaces(DefaultOpenIdUserDetailsMapper.class)
public class GlobalOpenIdUserDetailsMapper implements OpenIdUserDetailsMapper {

    //This method is deprecated and will only be called if the createAuthenticationResponse is not implemented
    @NonNull
    @Override
    public UserDetails createUserDetails(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims) {
        throw new UnsupportedOperationException();
    }

    @Override
    @NonNull
    public AuthenticationResponse createAuthenticationResponse(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims, @Nullable State state) {
        return new UserDetails("name", Collections.emptyList());
    }
}
import io.micronaut.context.annotation.Replaces
import io.micronaut.context.annotation.Requires
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper

import edu.umd.cs.findbugs.annotations.NonNull
import javax.inject.Singleton

@Singleton
@Replaces(DefaultOpenIdUserDetailsMapper.class)
class GlobalOpenIdUserDetailsMapper implements OpenIdUserDetailsMapper {

    //This method is deprecated and will only be called if the createAuthenticationResponse is not implemented
    @Override
    UserDetails createUserDetails(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims) {
        throw new UnsupportedOperationException()
    }

    @Override
    @NonNull
    AuthenticationResponse createAuthenticationResponse(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims, @Nullable State state) {
        new UserDetails("name", [])
    }


}
import io.micronaut.context.annotation.Replaces
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper
import java.lang.UnsupportedOperationException

import javax.inject.Singleton

@Singleton
@Replaces(DefaultOpenIdUserDetailsMapper::class)
class GlobalOpenIdUserDetailsMapper : OpenIdUserDetailsMapper {

    //This method is deprecated and will only be called if the createAuthenticationResponse is not implemented
    override fun createUserDetails(providerName: String?, tokenResponse: OpenIdTokenResponse?, openIdClaims: OpenIdClaims?): UserDetails {
        throw UnsupportedOperationException()
    }

    override fun createAuthenticationResponse(providerName: String, tokenResponse: OpenIdTokenResponse, openIdClaims: OpenIdClaims, state: State?): AuthenticationResponse {
        return UserDetails("name", emptyList())
    }
}

To override the user detail mapping behavior for a specific provider, register a bean with a named qualifier with a value equal to the name specified in the client configuration.

import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper;

import edu.umd.cs.findbugs.annotations.NonNull;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;

@Singleton
@Named("okta") (1)
public class OktaUserDetailsMapper implements OpenIdUserDetailsMapper {

    //This method is deprecated and will only be called if the createAuthenticationResponse is not implemented
    @NonNull
    @Override
    public UserDetails createUserDetails(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims) {
        throw new UnsupportedOperationException();
    }

    @Override
    @NonNull
    public AuthenticationResponse createAuthenticationResponse(String providerName, (2)
                                                               OpenIdTokenResponse tokenResponse, (3)
                                                               OpenIdClaims openIdClaims, (4)
                                                               @Nullable State state) { (5)
        return new UserDetails("name", Collections.emptyList()); (6)
    }


}
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper

import edu.umd.cs.findbugs.annotations.NonNull
import javax.inject.Named
import javax.inject.Singleton

@Singleton
@Named("okta") (1)
class OktaUserDetailsMapper implements OpenIdUserDetailsMapper {

    //This method is deprecated and will only be called if the createAuthenticationResponse is not implemented
    @NonNull
    UserDetails createUserDetails(String providerName,
                                  OpenIdTokenResponse tokenResponse,
                                  OpenIdClaims openIdClaims) {
        throw new UnsupportedOperationException()
    }

    @Override
    @NonNull
    AuthenticationResponse createAuthenticationResponse(String providerName, (2)
                                                        OpenIdTokenResponse tokenResponse, (3)
                                                        OpenIdClaims openIdClaims, (4)
                                                        @Nullable State state) { (5)
        new UserDetails("name", []) (6)
    }
}
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper
import java.lang.UnsupportedOperationException

import javax.inject.Named
import javax.inject.Singleton

@Singleton
@Named("okta") (1)
class OktaUserDetailsMapper : OpenIdUserDetailsMapper {

    //This method is deprecated and will only be called if the createAuthenticationResponse is not implemented
    override fun createUserDetails(providerName: String?, tokenResponse: OpenIdTokenResponse?, openIdClaims: OpenIdClaims?): UserDetails {
        throw UnsupportedOperationException()
    }

    override fun createAuthenticationResponse(providerName: String, (2)
                                              tokenResponse: OpenIdTokenResponse, (3)
                                              openIdClaims: OpenIdClaims, (4)
                                              state: State?) (5)
            : AuthenticationResponse {
        return UserDetails("name", emptyList()) (6)
    }
}
1 The named qualifier is added that matches the name in configuration
2 The provider name is passed to the method. Only useful for the global version
3 The full token response is available
4 The JWT claims are available
5 The state object used during OAuth authentication
6 An instance of Authentication is returned

14.3.1.2.3 Parameters

The OpenID specification for authorization requests allows for additional parameters beyond what is included in the OAuth 2.0 specification. Some of those parameters make sense to be provided by a bean and are described in this section. Other parameters are able to be controlled through configuration.

Here are the configuration option for authorization request parameters:

🔗
Table 1. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$AuthorizationEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.authorization.response-type

ResponseType

Determines the authorization processing flow to be used. Default value (code).

micronaut.security.oauth2.clients.*.openid.authorization.response-mode

java.lang.String

Mechanism to be used for returning authorization response parameters from the authorization endpoint.

micronaut.security.oauth2.clients.*.openid.authorization.display

Display

Controls how the authentication interface is displayed.

micronaut.security.oauth2.clients.*.openid.authorization.prompt

Prompt

Controls how the authentication server prompts the user.

micronaut.security.oauth2.clients.*.openid.authorization.max-age

java.lang.Integer

Maximum authentication age.

micronaut.security.oauth2.clients.*.openid.authorization.ui-locales

java.util.List

Preferred locales for authentication.

micronaut.security.oauth2.clients.*.openid.authorization.acr-values

java.util.List

Authentication class reference values.

micronaut.security.oauth2.clients.*.openid.authorization.url

java.lang.String

The endpoint URL

14.3.1.2.3.1 Nonce

By default, this library will include a nonce parameter as described in the OpenID Connect specification in authentication requests.

Because the validation of the nonce requires the nonce to be stored somewhere temporarily, a NoncePersistence bean must be present to retrieve the nonce for validation.

Micronaut ships with two implementations of NoncePersistence. One implementation to store it in a HTTP cookie (CookieNoncePersistence) and another one to persist it with a HTTP Session (SessionNoncePersistence).

You can configure which implementation to use:

🔗
Table 1. Configuration Properties for DefaultNonceConfiguration
Property Type Description

micronaut.security.oauth2.openid.nonce.persistence

java.lang.String

Sets the mechanism to persist the nonce for later retrieval for validation. Supported values ("session", "cookie"). Default value ("cookie").

micronaut.security.oauth2.openid.nonce.enabled

boolean

Sets whether a nonce parameter will be sent. Default (true).

If you use the default implementation, which stores the nonce in a HTTP cookie, you can configure how the cookie is built. See the following configuration options:

🔗
Table 2. Configuration Properties for CookieNoncePersistenceConfiguration
Property Type Description

micronaut.security.oauth2.openid.nonce.cookie.cookie-domain

java.lang.String

Sets the domain name of this Cookie. Default value (null).

micronaut.security.oauth2.openid.nonce.cookie.cookie-path

java.lang.String

Sets the path of the cookie. Default value ("/").

micronaut.security.oauth2.openid.nonce.cookie.cookie-http-only

java.lang.Boolean

Whether the Cookie can only be accessed via HTTP. Default value (true).

micronaut.security.oauth2.openid.nonce.cookie.cookie-secure

java.lang.Boolean

Sets whether the cookie is secured. Default value (true).

micronaut.security.oauth2.openid.nonce.cookie.cookie-max-age

java.time.Duration

Sets the maximum age of the cookie. Default value (5 minutes).

micronaut.security.oauth2.openid.nonce.cookie.cookie-name

java.lang.String

Cookie Name. Default value ("OPENID_NONCE").

To use (SessionNoncePersistence`). which stores the nonce in a HTTP session:

  1. Add a dependency to micronaut-session

    implementation("io.micronaut:micronaut-session")
    <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-session</artifactId>
    </dependency>

  2. Set the nonce persistence to session

    application.yml
    micronaut.security.oauth2.nonce.persistence: session
    You can provide your own implementation of NoncePersistence
If nonce validation fails, the user will not be authenticated.

Customization

There are several interfaces that implementations can be provided for to override how the nonce parameter is handled.

Interface

Responsibility

Default Implementation

NonceFactory

Builds a java.lang.String nonce value

DefaultNonceFactory

OpenIdTokenResponseValidator

Validates an OpenID token response (including the nonce).

DefaultOpenIdTokenResponseValidator

Validates the nonce claim in the token response

NonceClaimValidator

NoncePersistence

Stores the nonce to be retrieved later to allow validation

CookieNoncePersistence

To override the behavior of any of those beans, provide an implementation and replace the default one.

14.3.1.2.3.2 Login Hint

This library does not include an login hint by default with authorization requests. A LoginHintResolver bean can be registered that will be called when the authorization request is being built.

14.3.1.2.3.3 ID Token Hint

This library does not include an ID Token hint by default with authorization requests. A IdTokenHintResolver bean can be registered that will be called when the authorization request is being built.

14.3.1.2.4 Token Validation

A OpenIdTokenResponseValidator bean is responsible for validating the JWT token. The default implementation follows the guidelines described in the OpenID Connect specification regarding token validation where possible.

By default all implementations of GenericJwtClaimsValidator and OpenIdClaimsValidator are used to validate the token.

To allow for additional or custom validations, register an OpenIdClaimsValidator bean.

14.3.1.3 State Parameter

By default, this library will include a state parameter as described in RFC 6749 to authentication requests. A JSON serialized object is stored that contains a nonce value used for validation.

Because the validation of the state requires the state to be stored somewhere temporarily, a StatePersistence bean must be present to retrieve the state for validation. The default implementation stores the state in an HTTP cookie. To configure how the cookie is built, see the following configuration options:

🔗
Table 1. Configuration Properties for CookieStatePersistenceConfiguration
Property Type Description

micronaut.security.oauth2.state.cookie.cookie-domain

java.lang.String

Sets the domain name of this Cookie. Default value (null).

micronaut.security.oauth2.state.cookie.cookie-path

java.lang.String

Sets the path of the cookie. Default value ("/").

micronaut.security.oauth2.state.cookie.cookie-http-only

java.lang.Boolean

Whether the Cookie can only be accessed via HTTP. Default value (true).

micronaut.security.oauth2.state.cookie.cookie-secure

java.lang.Boolean

Sets whether the cookie is secured. Default value (true).

micronaut.security.oauth2.state.cookie.cookie-max-age

java.time.Duration

Sets the maximum age of the cookie. Default value (5 minutes).

micronaut.security.oauth2.state.cookie.cookie-name

java.lang.String

Cookie Name. Default value ("OAUTH2_STATE").

You can provide your own implementation, however an implementation of state persistence that stores the state in an http session has also been provided.

To enable state persistence with an http session:

  1. Add a dependency to micronaut-session

    implementation("io.micronaut:micronaut-session")
    <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-session</artifactId>
    </dependency>

  2. Set the state persistence to session

    application.yml
    micronaut.security.oauth2.state.persistence: session

    If state validation fails, the user will not be authenticated.

Customization

There are several interfaces that implementations can be provided for to override how the state parameter is handled.

Interface

Responsibility

Default Implementation

StateFactory

Builds a State

DefaultStateFactory

StateSerDes

Serializes and de-serializes the state object for use in the authorization request

JacksonStateSerDes

StateValidator

Validates the state received in the authorization response

DefaultStateValidator

StatePersistence

Stores the state to be retrieved later to allow validation

CookieStatePersistence

To override the behavior of any of those beans, provide an implementation and replace the default one.

14.3.2 Password

The resource owner password credentials grant is described in RFC 6749. In short, credentials are passed directly to the token endpoint and if authentication succeeds, the token endpoint responds with the appropriate token.

The process that handles the token response onward is the same for both authorization code and password grants. See the following high level flow diagrams:

OAuth 2.0 Provider

oauth password

OpenID Provider

openid password

In Micronaut, the password grant is supported by setting the grant-type configuration option in the client configuration. For example:

micronaut:
  security:
    oauth2:
      clients:
        github:
          grant-type: password
This example above is not intended to be a complete configuration reference

When a client is configured for the password grant type, the authorization code endpoints will not be available and instead an AuthenticationProvider will be created that will participate in the normal login flow.

14.4 Endpoints

14.4.1 OpenID End Session

Part of the OpenID Connect specification includes a draft document titled Session Management. Because the specification is a draft, some providers have implemented it differently or not at all. See the following diagram for how end session works with default configuration:

openid end session

If any configured OpenID provider supports end session behavior, a route will be registered that responds to /oauth/logout and redirects to the provider to log the user out. A parameter is also sent to the provider that indicates what URL the provider should redirect the user to after logging out. The default URL is /logout which will cause the local authentication to also be cleared and a final redirect issued according to the LogoutHandler.

All of the above is configurable through micronaut.security.oauth2.openid:

🔗
Table 1. Configuration Properties for OauthConfigurationProperties$OpenIdConfigurationProperties
Property Type Description

micronaut.security.oauth2.openid.logout-uri

java.lang.String

The URI used to log out of an OpenID provider. Default value ("/oauth/logout").

🔗
Table 2. Configuration Properties for OauthConfigurationProperties$OpenIdConfigurationProperties$EndSessionConfigurationProperties
Property Type Description

micronaut.security.oauth2.openid.end-session.redirect-uri

java.lang.String

The URI the OpenID provider should redirect to after logging out. Default value ("/logout").

To enable the usage of the /logout endpoint, see the section on the Logout Endpoint.

The get-allowed configuration option must be set to true because the OpenID provider will issue a redirect which is a GET request.

This library supports end session for Auth0, AWS Cognito, and Okta out of the box. The EndSessionEndpointResolver is responsible for determining which EndSessionEndpoint will be used for a given provider, if any.

Before choosing any of the default providers, the endpoint resolver will first look for an EndSessionEndpoint bean with a named qualifier that matches the name of the client in configuration. If no bean is found then the default endpoints will be matched against the issuer URL.

If for example you are using one of the providers that is supported out of the box and you don’t want the end session support, it is possible to disable it per client.

🔗
Table 3. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$EndSessionConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.end-session.enabled

boolean

The end session enabled flag. Default value (true).

micronaut.security.oauth2.clients.*.openid.end-session.url

java.lang.String

The endpoint URL

14.4.2 Introspection

It is possible to configure the introspection endpoint URL for OAuth 2.0 and OpenID providers, however Micronaut currently does not use this configuration in any way.

See the following configuration table:

🔗
Table 1. Configuration Properties for OauthClientConfigurationProperties$IntrospectionEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.introspection.url

java.lang.String

The endpoint URL

micronaut.security.oauth2.clients.*.introspection.auth-method

AuthenticationMethod

Authentication Method

14.4.3 Revocation

It is possible to configure the revocation endpoint URL for OAuth 2.0 and OpenID providers, however Micronaut currently does not use this configuration in any way.

See the following configuration table:

🔗
Table 1. Configuration Properties for OauthClientConfigurationProperties$RevocationEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.revocation.url

java.lang.String

The endpoint URL

micronaut.security.oauth2.clients.*.revocation.auth-method

AuthenticationMethod

Authentication Method

14.4.4 OpenID User Info

It is possible to configure the user info endpoint URL for OpenID providers, however Micronaut currently does not use this configuration in any way.

See the following configuration table:

🔗
Table 1. Configuration Properties for OauthClientConfigurationProperties$OpenIdClientConfigurationProperties$UserInfoEndpointConfigurationProperties
Property Type Description

micronaut.security.oauth2.clients.*.openid.user-info.url

java.lang.String

The endpoint URL

14.5 Custom Clients

The center of the OAuth 2.0 authorization code grant support is the OauthClient for standard OAuth 2.0 and OpenIdClient for OpenID. The current implementation builds clients based on configuration. It is possible however to register a custom client and that client will automatically have the routes associated with it to enable the authorization code grant flow.

The OauthClient interface is simple and only requires three methods.

public interface OauthClient {

    /**
     * @return The provider name
     */
    String getName();

    /**
     * Responsible for redirecting to the authorization endpoint.
     *
     * @param originating The originating request
     * @return A response publisher
     */
    Publisher<MutableHttpResponse<?>> authorizationRedirect(HttpRequest<?> originating);

    /**
     * Responsible for receiving the authorization callback request and returning
     * an authentication response.
     *
     * @param request The callback request
     * @return The authentication response
     */
    Publisher<AuthenticationResponse> onCallback(HttpRequest<Map<String, Object>> request);

}
  • The getName method is used to build the URLs used to trigger the client methods

  • The authorizationRedirect method returns a response that redirects to the provider

  • The onCallback method receives a callback authorization response and returns a response including either an authentication failure or the user details.

Because of how generic this interface is, it is possible to implement authentication for any provider that follows the redirect / redirect back pattern. For example one could implement support for OAuth 1.0a providers (looking at you, twitter).

15 Repository

You can find the source code of this project in this repository: