import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpRequest;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.management.endpoint.EndpointSensitivityProcessor;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRuleResult;
import io.micronaut.security.rules.SensitiveEndpointRule;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Singleton
@Replaces(SensitiveEndpointRule.class)
class SensitiveEndpointRuleReplacement extends SensitiveEndpointRule {
SensitiveEndpointRuleReplacement(EndpointSensitivityProcessor endpointSensitivityProcessor) {
super(endpointSensitivityProcessor);
}
@Override
@NonNull
protected Publisher<SecurityRuleResult> checkSensitiveAuthenticated(@NonNull HttpRequest<?> request,
@NonNull Authentication authentication,
@NonNull ExecutableMethod<?, ?> method) {
return Mono.just(SecurityRuleResult.ALLOWED);
}
}
Table of Contents
Micronaut Security
Official Security Solution for Micronaut
Version: 4.11.1
1 Introduction
Micronaut Security is a fully featured and customizable security solution for your applications.
Micronaut Security 3.x requires Micronaut 3.x. |
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 What's New
Micronaut Security includes the following new features and improvements.
What’s New in Micronaut Security 3
-
Improvements to many APIs
-
Consolidated authentication state into a single interface
-
The SecurityRule API is now reactive
-
A
getRoles
method was added to Authentication -
New static methods on Authentication and AuthenticationResponse have been added to help create authentication related objects
4 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).
Micronaut Security 4.0 breaking changes
-
SecurityRule
check
method no longer contains the argumentRouteMatch
. You can easily retrieve aRouteMatch
with theHttpRequest
method argument withrequest.getAttribute(HttpAttributes.ROUTE_MATCH).orElse(null)
. -
For applications with
micronaut.security.authentication
set tocookie
andmicronaut.security.redirect.enabled
set tofalse
, the server responds with 401 HTTP Status code instead of 200 for login failed attempts. -
micronaut.security.intercept-url-map-prepend-pattern-with-context-path
defaults totrue
. Thus intercept url patterns are be prepended with the server context path if it is set. -
Micronaut Security APIs have been decoupled from HTTP. Many APIs now have generics instead of HTTP-related types such as
HttpRequest
. For example, instead ofAuthenticationProvider
, you should useAuthenticationProvider<HttpRequest<?>>
.
Configuration Changes
Some configuration keys have changed.
Old | New |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Classes relocated
Some classes have been renamed and moved from io.micronaut.security:micronaut-security-jwt
to io.micronaut.security:micronaut.security
.
Old Pkg | New Pkg |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Annotation Mappers Removed
The mappers for javax.annotation.security
annotations – DenyAll
, PermitAll
, RolesAllowed
– have been removed. Use the Jakarta versions for jakarta.annotation.security
annotations instead.
Removed | Retained |
---|---|
io.micronaut.security.annotation.DenyAllAnnotationMapper |
io.micronaut.security.annotation.JakartaDenyAllAnnotationMapper |
io.micronaut.security.annotation.PermitAllAnnotationMapper |
io.micronaut.security.annotation.JakartaPermitAllAnnotationMapper |
io.micronaut.security.annotation.RolesAllowedAnnotationMapper |
io.micronaut.security.annotation.JakartaRolesAllowedAnnotationMapper |
Reactive OpenIdAuthenticationMapper
The return type of the OpenIdAuthenticationMapper.createAuthenticationResponse
has changed to return a Publisher
to be consistent with the OauthAuthenticationMapper
interface. Because the method now returns a Publisher, blocking operations can be offloaded to another thread pool using the reactive streams implementation of your choice.
Micronaut Security 3.4 breaking changes
Sensitive endpoints will now respond with an error unless a replacement for the SensitiveEndpointRule is bound.
The following code snippet illustrates how to restore the previous functionality:
The previous functionality allows any authenticated user, no matter their role to view sensitive endpoints. |
import io.micronaut.context.annotation.Replaces
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.NonNull
import io.micronaut.http.HttpRequest
import io.micronaut.inject.ExecutableMethod
import io.micronaut.management.endpoint.EndpointSensitivityProcessor
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.rules.SecurityRuleResult
import io.micronaut.security.rules.SensitiveEndpointRule
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
@Singleton
@Replaces(SensitiveEndpointRule.class)
class SensitiveEndpointRuleReplacement extends SensitiveEndpointRule {
SensitiveEndpointRuleReplacement(EndpointSensitivityProcessor endpointSensitivityProcessor) {
super(endpointSensitivityProcessor);
}
@Override
@NonNull
protected Publisher<SecurityRuleResult> checkSensitiveAuthenticated(@NonNull HttpRequest<?> request,
@NonNull Authentication authentication,
@NonNull ExecutableMethod<?, ?> method) {
return Mono.just(SecurityRuleResult.ALLOWED);
}
}
import io.micronaut.context.annotation.Replaces
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.inject.ExecutableMethod
import io.micronaut.management.endpoint.EndpointSensitivityProcessor
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.rules.SecurityRuleResult
import io.micronaut.security.rules.SensitiveEndpointRule
import io.micronaut.security.token.RolesFinder
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
@Replaces(SensitiveEndpointRule::class)
@Singleton
class SensitiveEndpointRuleReplacement(endpointSensitivityProcessor: EndpointSensitivityProcessor) : SensitiveEndpointRule(endpointSensitivityProcessor) {
override fun checkSensitiveAuthenticated(
request: HttpRequest<*>,
authentication: Authentication,
method: ExecutableMethod<*, *>
): Publisher<SecurityRuleResult> = Mono.just(SecurityRuleResult.ALLOWED)
}
Micronaut Security 3.1 breaking changes
Micronaut security no longer exposes micronaut-management
dependency.
Micronaut Security 3.0 breaking changes
User Details Removal
The UserDetails
class has been removed and all usages should be replaced with Authentication.
Affected APIs
Classes Renamed
Old | New |
---|---|
io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper |
io.micronaut.security.oauth2.endpoint.token.response.OauthAuthenticationMapper |
io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper |
io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper |
io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper |
io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper |
Other Changes
-
The LoginSuccessfulEvent that gets emitted when a user logs in will now be created with an instance of Authentication.
-
The
AuthenticationUserDetailsAdapter
class has been deleted.
SecurityRule Changes
The SecurityRule API has changed. The last argument to the method was a map that represented the user attributes. Instead that argument was replaced with a reference to the Authentication. This has the benefit of rules now having access to the username of the logged in user as well as access to the convenience method getRoles()
.
In addition, the return type of the method has changed to return a Publisher
. This was necessary because the security rules execute as part of the security filter which may be on a non blocking thread. Because the method now returns a Publisher
, blocking operations can be offloaded to another thread pool using the reactive streams implementation of your choice.
Micronaut 2 API:
SecurityRuleResult check(HttpRequest<?> request, @Nullable RouteMatch<?> routeMatch, @Nullable Map<String, Object> claims);
Micronaut 3 API:
Publisher<SecurityRuleResult> check(HttpRequest<?> request, @Nullable RouteMatch<?> routeMatch, @Nullable Authentication authentication);
LDAP Package Change
All classes in the io.micronaut.configuration.security.ldap
have been moved to the io.micronaut.security.ldap
package.
SecurityFilter
The security filter no longer extends deprecated OncePerRequestHttpServerFilter
because it has been deprecated in Micronaut 3.
Cookie Secure Configuration
The following properties' default value has been removed in Micronaut Security 3.0.0:
-
micronaut.security.oauth2.openid.nonce.cookie.cookie-secure
-
micronaut.security.oauth2.state.cookie.cookie-secure
-
micronaut.security.token.jwt.cookie.cookie-secure
-
micronaut.security.token.refresh.cookie.cookie-secure`
If the cookie-secure setting is not set, cookies will be secure if the request is determined to be HTTPS.
|
Deprecations Removal
Most if not all deprecated classes constructors, and methods have been removed.
Other Changes
-
The constructor of DefaultJwtAuthenticationFactory has changed
-
The constructor of IdTokenLoginHandler has changed
-
The constructor of SessionLoginHandler has changed
-
The constructor of BasicAuthAuthenticationFetcher has changed
-
The RolesFinder method
findInClaims
has been deprecated and usages should be replaced withresolveRoles(@Nullable Map<String, Object> attributes)
.
5 Installation
Using the CLI
If you are creating your project using the Micronaut CLI, supply either $ mn create-app my-app --features security |
To use the Micronaut’s security capabilities you must have the security
dependency on your classpath:
annotationProcessor("io.micronaut.security:micronaut-security-annotations")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-annotations</artifactId>
</path>
</annotationProcessorPaths>
implementation("io.micronaut.security:micronaut-security")
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security</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.
6 How It Works - Security Filter
When you add the micronaut-security
dependency, it contributes a Micronaut HTTP Server Filter - the SecurityFilter.
For each request, the filter evaluates the request against every bean of type AuthenticationFetcher. That evaluation may result in an instance of type Authentication being resolved.
Then, the optional authentication is evaluated against every bean of type SecurityRule to determine if the request is authorized. If not authorized, an AuthorizationException is thrown.
6.1 Built-In Authentication Fetchers
You can write your own bean of type AuthenticationFetcher. However, Micronaut Security ships with several built-in authentication fetchers:
Bean |
Description |
It parses the credentials from an HTTP Request, which uses Basic Authentication. Then, it invokes every Authentication Provider using the parsed credentials. |
|
It attempts to retrieve a token from the HTTP Request, and then it tries to validate the token. |
|
It attempts to retrieve an Authentication from an HTTP session. |
|
Creates an Authentication if an X.509 client certificate is present and a name (CN) can be extracted. |
6.1.1 Basic-Auth Authentication Fetcher
BasicAuthAuthenticationFetcher parses the credentials from an HTTP Request using Basic Authentication. Then, it invokes every Authentication Provider using the parsed credentials.
6.1.2 Token Authentication Fetcher
TokenAuthenticationFetcher attempts to retrieve a token from the HTTP Request via the TokenResolver API, and then it tries to validate the token via the TokenValidator API.
6.1.2.1 Token Readers
You can write your own bean of type TokenReader. However, Micronaut Security ships with several built-in beans of type TokenReader.
6.1.2.1.1 Bearer Token Reader
BearerTokenReader attempts to read a RFC 6750 Bearer Token.
The following configuration properties are available to customize how the Bearer Token will be read:
Property | Type | Description |
---|---|---|
|
boolean |
Set whether to enable bearer token authentication. Default value true. |
|
java.lang.String |
Sets the prefix to use for the auth token. Default value Bearer. |
|
java.lang.String |
Sets the header name to use. Default value Authorization. |
6.1.2.1.2 Cookie Token Reader
CookieTokenReader attempts to read token from a request cookie.
The following configuration properties are available to customize how the token will be read from a cookie:
Property | Type | Description |
---|---|---|
|
java.lang.String |
|
|
java.lang.Boolean |
|
|
java.lang.Boolean |
|
|
java.time.Duration |
|
|
Sets the same-site setting of the cookie. Default value null. Value is case sensitive. Allowed values: |
|
|
boolean |
Whether JWT cookie configuration is enabled. Default value (true). |
|
java.lang.String |
Cookie Name. Default value ("JWT"). |
|
java.lang.String |
The path of the cookie. Default value ("/"). |
6.2 Security Rules
The decision to allow access to a particular endpoint to anonymous or authenticated users is determined by a collection of Security Rules which are executed from the SecurityFilter. Micronaut ships with several built-in security rules. If they don’t fulfil your needs, you can implement your own SecurityRule.
Security rules return a publisher that should emit a single SecurityRuleResult. See the following table for a description of each result.
Result | Description |
---|---|
Access to the resource should be granted and no further rules will be considered. |
|
Access to the resource should be rejected and no further rules will be considered. |
|
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!
|
SecurityFilter evaluates security rules in order. The remaining rules are not evaluated once a rule returns ALLOWED or REJECTED .
|
Security rules implement the ordered interface and so all of the existing rules have a static variable ORDER
that stores the order of that rule. The rules they are executed in order from lower to higher values. You can use those variables to place your custom rule before or after any of the existing rules.
In the following table you can find the order and a short description of the behavior of built-in security rules. You can find more details about theese rules in their own guide sections.
Rule | Order | ACCEPT condition | REJECT condition | UNKNOWN confition |
---|---|---|---|---|
-300 |
Never |
None of the IP patterns matched the hostaddress |
The address matches at least one of the patterns or no address could be resolved |
|
-200 |
At least one required role is granted to the authenticated user |
None of the required roles is granted to the authenticated user |
No secured annotation is specified on the requested method |
|
-100 |
At least one required role is granted to the authenticated user |
None of the required roles is granted to the authenticated user |
No path pattern is matched |
|
0 |
User is authenticated |
User is not authenticated |
Path is not a sensitive one |
Do not execute any blocking operations in the rule implementation without offloading those operations to another thread pool. |
Since version 2.5, the Micronaut Framework executes the filters and then it reads the HTTP Request’s body. SecurityFilter evaluates the beans of type SecurityRule. Because of that, SecurityRule cannot rely on HTTP Request’s body because the Micronaut Framework has not read the body yet. |
6.2.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[0]=127.0.0.1
micronaut.security.ip-patterns[1]=192.168.1.*
micronaut:
security:
ip-patterns:
- 127.0.0.1
- 192.168.1.*
[micronaut]
[micronaut.security]
ip-patterns=[
"127.0.0.1",
"192.168.1.*"
]
micronaut {
security {
ipPatterns = ["127.0.0.1", "192.168.1.*"]
}
}
{
micronaut {
security {
ip-patterns = ["127.0.0.1", "192.168.1.*"]
}
}
}
{
"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
.
6.2.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. |
6.2.2.1 Jakarta Annotations
Alternatively, you can use Jakarta Annotations:
-
jakarta.annotation.security.PermitAll
-
jakarta.annotation.security.RolesAllowed
-
jakarta.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.
|
When the @Secured annotation has a set of roles, the SecuredAnnotationRule grants access to a user if they have any of the roles. |
Don’t use both annotations, @Secured and @RolesAllowed , in the same method. If you use it, the first occurrence (either @RolesAllowed or @Secured ) is used. The other annotation is ignored.
|
6.2.2.2 Secured with expressions
In combination with @Secured, you can use expressions, introduced in Micronaut Framework 4.0, to access the authenticated user:
@Controller("/authenticated")
public class ExampleController {
@Secured("#{ user?.attributes?.get('email') == 'sherlock@micronaut.example' }")
@Produces(MediaType.TEXT_PLAIN)
@Get("/email")
public String authenticationByEmail(Principal principal) {
return principal.getName() + " is authenticated";
}
}
@Controller("/authenticated")
class ExampleController {
@Secured("#{ user?.attributes?.get('email') == 'sherlock@micronaut.example' }")
@Produces(MediaType.TEXT_PLAIN)
@Get("/email")
String authenticationByEmail(Principal principal) {
"${principal.name} is authenticated"
}
}
@Controller("/authenticated")
class ExampleController {
@Secured("#{ user?.attributes?.get('email') == 'sherlock@micronaut.example' }")
@Produces(MediaType.TEXT_PLAIN)
@Get("/email")
fun authenticationByEmail(principal: Principal) = "${principal.name} is authenticated"
}
user
is of type Authentication
6.2.3 Intercept URL Map
Moreover, you can configure endpoint authentication and authorization access with an Intercept URL Map:
micronaut.security.intercept-url-map[0].pattern=/images/*
micronaut.security.intercept-url-map[0].http-method=GET
micronaut.security.intercept-url-map[0].access[0]=isAnonymous()
micronaut.security.intercept-url-map[1].pattern=/books
micronaut.security.intercept-url-map[1].access[0]=isAuthenticated()
micronaut.security.intercept-url-map[2].pattern=/books/grails
micronaut.security.intercept-url-map[2].http-method=POST
micronaut.security.intercept-url-map[2].access[0]=ROLE_GRAILS
micronaut.security.intercept-url-map[2].access[1]=ROLE_GROOVY
micronaut.security.intercept-url-map[3].pattern=/books/grails
micronaut.security.intercept-url-map[3].http-method=PUT
micronaut.security.intercept-url-map[3].access[0]=ROLE_ADMIN
micronaut:
security:
intercept-url-map:
-
pattern: /images/*
http-method: GET
access:
- isAnonymous()
-
pattern: /books
access:
- isAuthenticated()
-
pattern: /books/grails
http-method: POST
access:
- ROLE_GRAILS
- ROLE_GROOVY
-
pattern: /books/grails
http-method: PUT
access:
- ROLE_ADMIN
[micronaut]
[micronaut.security]
[[micronaut.security.intercept-url-map]]
pattern="/images/*"
http-method="GET"
access=[
"isAnonymous()"
]
[[micronaut.security.intercept-url-map]]
pattern="/books"
access=[
"isAuthenticated()"
]
[[micronaut.security.intercept-url-map]]
pattern="/books/grails"
http-method="POST"
access=[
"ROLE_GRAILS",
"ROLE_GROOVY"
]
[[micronaut.security.intercept-url-map]]
pattern="/books/grails"
http-method="PUT"
access=[
"ROLE_ADMIN"
]
micronaut {
security {
interceptUrlMap = [{
pattern = "/images/*"
httpMethod = "GET"
access = ["isAnonymous()"]
}, {
pattern = "/books"
access = ["isAuthenticated()"]
}, {
pattern = "/books/grails"
httpMethod = "POST"
access = ["ROLE_GRAILS", "ROLE_GROOVY"]
}, {
pattern = "/books/grails"
httpMethod = "PUT"
access = ["ROLE_ADMIN"]
}]
}
}
{
micronaut {
security {
intercept-url-map = [{
pattern = "/images/*"
http-method = "GET"
access = ["isAnonymous()"]
}, {
pattern = "/books"
access = ["isAuthenticated()"]
}, {
pattern = "/books/grails"
http-method = "POST"
access = ["ROLE_GRAILS", "ROLE_GROOVY"]
}, {
pattern = "/books/grails"
http-method = "PUT"
access = ["ROLE_ADMIN"]
}]
}
}
}
{
"micronaut": {
"security": {
"intercept-url-map": [{
"pattern": "/images/*",
"http-method": "GET",
"access": ["isAnonymous()"]
}, {
"pattern": "/books",
"access": ["isAuthenticated()"]
}, {
"pattern": "/books/grails",
"http-method": "POST",
"access": ["ROLE_GRAILS", "ROLE_GROOVY"]
}, {
"pattern": "/books/grails",
"http-method": "PUT",
"access": ["ROLE_ADMIN"]
}]
}
}
}
-
pattern
/images/*
enables access to authenticated and not authenticated users -
pattern
/books
enables access for everyone authenticated -
pattern
/books/grails
enables 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[0].pattern=/v1/myResource/**
micronaut.security.intercept-url-map[0].httpMethod=GET
micronaut.security.intercept-url-map[0].access[0]=isAnonymous()
micronaut.security.intercept-url-map[1].pattern=/v1/myResource/**
micronaut.security.intercept-url-map[1].access[0]=isAuthenticated()
micronaut:
security:
intercept-url-map:
- pattern: /v1/myResource/**
httpMethod: GET
access:
- isAnonymous()
- pattern: /v1/myResource/**
access:
- isAuthenticated()
[micronaut]
[micronaut.security]
[[micronaut.security.intercept-url-map]]
pattern="/v1/myResource/**"
httpMethod="GET"
access=[
"isAnonymous()"
]
[[micronaut.security.intercept-url-map]]
pattern="/v1/myResource/**"
access=[
"isAuthenticated()"
]
micronaut {
security {
interceptUrlMap = [{
pattern = "/v1/myResource/**"
httpMethod = "GET"
access = ["isAnonymous()"]
}, {
pattern = "/v1/myResource/**"
access = ["isAuthenticated()"]
}]
}
}
{
micronaut {
security {
intercept-url-map = [{
pattern = "/v1/myResource/**"
httpMethod = "GET"
access = ["isAnonymous()"]
}, {
pattern = "/v1/myResource/**"
access = ["isAuthenticated()"]
}]
}
}
}
{
"micronaut": {
"security": {
"intercept-url-map": [{
"pattern": "/v1/myResource/**",
"httpMethod": "GET",
"access": ["isAnonymous()"]
}, {
"pattern": "/v1/myResource/**",
"access": ["isAuthenticated()"]
}]
}
}
}
-
accessing
/v1/myResource/**
with a GET request does not require authentication -
accessing
/v1/myResource/**
with a request that isn’t GET requires authentication
When the @Secured annotation has a set of roles, the SecuredAnnotationRule grants access to a user if they have any of the roles. |
6.2.4 Built-In Endpoints Security
When you turn on security, Built-in endpoints are secured depending on their sensitive value.
endpoints.beans.enabled=true
endpoints.beans.sensitive=true
endpoints.info.enabled=true
endpoints.info.sensitive=false
endpoints:
beans:
enabled: true
sensitive: true
info:
enabled: true
sensitive: false
[endpoints]
[endpoints.beans]
enabled=true
sensitive=true
[endpoints.info]
enabled=true
sensitive=false
endpoints {
beans {
enabled = true
sensitive = true
}
info {
enabled = true
sensitive = false
}
}
{
endpoints {
beans {
enabled = true
sensitive = true
}
info {
enabled = true
sensitive = false
}
}
}
{
"endpoints": {
"beans": {
"enabled": true,
"sensitive": true
},
"info": {
"enabled": true,
"sensitive": false
}
}
}
-
the
/beans
endpoint is secured -
the
/info
endpoint is open for unauthenticated access
You need to replace the default implementation SensitiveEndpointRule and implement SensitiveEndpointRule::checkSensitiveAuthenticated
to allow authenticated users access to sensitive endpoints. For example, you may want to restrict access to users with a specific role:
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpRequest;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.management.endpoint.EndpointSensitivityProcessor;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRuleResult;
import io.micronaut.security.rules.SensitiveEndpointRule;
import io.micronaut.security.token.RolesFinder;
import jakarta.inject.Singleton;
import java.util.Collections;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Replaces(SensitiveEndpointRule.class)
@Singleton
public class SensitiveEndpointRuleReplacement extends SensitiveEndpointRule {
private final RolesFinder rolesFinder;
public SensitiveEndpointRuleReplacement(EndpointSensitivityProcessor endpointSensitivityProcessor,
RolesFinder rolesFinder) {
super(endpointSensitivityProcessor);
this.rolesFinder = rolesFinder;
}
@Override
@NonNull
protected Publisher<SecurityRuleResult> checkSensitiveAuthenticated(@NonNull HttpRequest<?> request,
@NonNull Authentication authentication,
@NonNull ExecutableMethod<?, ?> method) {
return Mono.just(rolesFinder.hasAnyRequiredRoles(Collections.singletonList("ROLE_SYSTEM"), authentication.getRoles())
? SecurityRuleResult.ALLOWED : SecurityRuleResult.REJECTED);
}
}
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
@Replaces(SensitiveEndpointRule.class)
@Singleton
class SensitiveEndpointRuleReplacement extends SensitiveEndpointRule {
private final RolesFinder rolesFinder;
SensitiveEndpointRuleReplacement(EndpointSensitivityProcessor endpointSensitivityProcessor,
RolesFinder rolesFinder) {
super(endpointSensitivityProcessor)
this.rolesFinder = rolesFinder
}
@Override
@NonNull
protected Publisher<SecurityRuleResult> checkSensitiveAuthenticated(@NonNull HttpRequest<?> request,
@NonNull Authentication authentication,
@NonNull ExecutableMethod<?, ?> method) {
Mono.just(rolesFinder.hasAnyRequiredRoles(["ROLE_SYSTEM"], authentication.roles)
? SecurityRuleResult.ALLOWED : SecurityRuleResult.REJECTED)
}
}
import io.micronaut.context.annotation.Replaces
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.inject.ExecutableMethod
import io.micronaut.management.endpoint.EndpointSensitivityProcessor
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.rules.SecurityRuleResult
import io.micronaut.security.rules.SensitiveEndpointRule
import io.micronaut.security.token.RolesFinder
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
@Replaces(SensitiveEndpointRule::class)
@Singleton
class SensitiveEndpointRuleReplacement(endpointSensitivityProcessor: EndpointSensitivityProcessor,
private val rolesFinder: RolesFinder) : SensitiveEndpointRule(endpointSensitivityProcessor) {
override fun checkSensitiveAuthenticated(
request: HttpRequest<*>,
authentication: Authentication,
method: ExecutableMethod<*, *>
): Publisher<SecurityRuleResult> {
return Mono.just(
if (rolesFinder.hasAnyRequiredRoles(listOf("ROLE_SYSTEM"), authentication.roles)) SecurityRuleResult.ALLOWED
else SecurityRuleResult.REJECTED
)
}
}
7 Exception Handlers
Micronaut Security ships with several built-in Exception Handlers:
Exception |
Handler |
You may need to replace some of those beans to customize Micronaut Security exception handling to your needs.
8 Endpoints
The Security Filter section describes how for each request an Authentication maybe resolved. It maybe a token placed in an HTTP Header, in a cookie, or an HTTP session. How do you obtain such a token or how is the user saved into the HTTP Session in the first place?. In a Micronaut application, you can expose the LoginController.
8.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:
Property | Type | Description |
---|---|---|
|
java.util.Set |
Supported content types for POST endpoints. Default Value application/json and application/x-www-form-urlencoded |
|
boolean |
Whether the controller is enabled. |
|
java.lang.String |
Path to the controller. |
8.1.1 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.
8.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:
Property | Type | Description |
---|---|---|
|
java.util.Set |
Supported content types for POST endpoints. Default Value application/json and application/x-www-form-urlencoded |
|
boolean |
Whether the controller is enabled. |
|
java.lang.String |
Path to the LogoutController. Default value "/logout". |
|
boolean |
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.
|
8.2.1 Logout Handler
The LogoutHandler API defines how to respond to a logout attempt. For example, with the Logout Controller.
8.3 Built-in Login and Logout Handlers
You can provide your own implementations of LoginHandler and LogoutHandler.
However, Micronaut security modules ship with several implementations which you can enable by setting the configuration micronaut.security.authentication
micronaut.security.authentication Value |
Required Module | Login Handler | Logout Handler |
---|---|---|---|
|
|
||
|
|
||
|
|
||
|
|
These handlers allow you to set the following scenarios:
8.3.1 Authentication Mode Bearer
When you set micronaut.security.authentication=bearer
, AccessRefreshTokenLoginHandler a bean of type LoginHandler is enabled.
8.3.2 Authentication Mode Session
When you set micronaut.security.authentication=session
, SessionLoginHandler a bean of type LoginHandler and SessionLogoutHandler a bean of type LogoutHandler are enabled.
8.3.3 Authentication Mode Cookie
When you set micronaut.security.authentication=cookie
, TokenCookieLoginHandler a bean of type LoginHandler and JwtCookieClearerLogoutHandler a bean of type LogoutHandler are enabled.
8.3.4 Authentication Mode ID Token
When you set micronaut.security.authentication=idtoken
, IdTokenLoginHandler a bean of type LoginHandler and JwtCookieClearerLogoutHandler a bean of type LogoutHandler are enabled.
9 Security Configuration
The following global configuration options are available:
Property | Type | Description |
---|---|---|
|
Defines which authentication to use. Defaults to null. Possible values bearer, session, cookie, idtoken. Should only be supplied if the service handles login and logout requests. |
|
|
boolean |
If Security is enabled. Default value true |
|
java.util.List |
Map that defines the interception patterns. |
|
java.util.List |
Allowed IP patterns. Default value (["0.0.0.0"]) |
|
boolean |
Whether the intercept URL patterns should be prepended with context path if defined. Defaults to true. |
|
Determines how authentication providers should be processed. Default value ANY. Possible values: ANY or ALL. |
|
|
boolean |
Whether the server should respond with 401 for requests that do not match any routes on the server, if you set it to false, it will return 404 for requests that do not match any routes on the server. Default value (true). |
9.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.
9.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.
9.3 Handlers without redirection
Some built-in handlers respond with a 303 (See other) response to the urls defined in the Redirection Configuration
If you disable redirection configuration with by setting micronaut.security.redirect.enabled
to false
, these handlers respond with 200 responses instead.
9.4 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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Where the user is redirected to after a successful login. Default value ("/"). |
|
java.lang.String |
Where the user is redirected to after a failed login. Default value ("/"). |
|
java.lang.String |
URL where the user is redirected after logout. Default value ("/"). |
|
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. |
|
boolean |
Sets whether Redirection configuration enabled. Default value (true). |
Property | Type | Description |
---|---|---|
|
java.lang.String |
Where the user is redirected to after trying to access a secured route which he is forbidden to access. Default value ("/"). |
|
boolean |
Whether it should redirect on forbidden rejections. Default value (true). |
Property | Type | Description |
---|---|---|
|
java.lang.String |
Where the user is redirected to after trying to access a secured route. Default value ("/"). |
|
boolean |
Whether it should redirect on unauthorized rejections. Default value (true). |
Use the API RedirectService, which prepends the context path, if defined, to the redirect URLs. |
10 Reactive Authentication Providers
To authenticate users you must provide implementations of ReactiveAuthenticationProvider or HttpRequestReactiveAuthenticationProvider.
The following code snippet illustrates a naive implementation:
@Singleton
class CustomAuthenticationProvider<B> implements HttpRequestReactiveAuthenticationProvider<B> {
@Override
@SingleResult
public Publisher<AuthenticationResponse> authenticate(HttpRequest<B> requestContext, AuthenticationRequest<String, String> authRequest) {
AuthenticationResponse rsp = authRequest.getIdentity().equals("user") && authRequest.getSecret().equals("password")
? AuthenticationResponse.success("user")
: AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH);
return Mono.create(emitter -> {
emitter.success(rsp);
});
}
}
@Singleton
class CustomAuthenticationProvider<B> implements HttpRequestReactiveAuthenticationProvider<B> {
@Override
@SingleResult
Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<B> httpRequest,
@NonNull AuthenticationRequest<String, String> authRequest) {
AuthenticationResponse rsp = (authRequest.identity == "user" && authRequest.secret == "password")
? AuthenticationResponse.success("user")
: AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH)
return Mono.create(emitter -> {
emitter.success(rsp)
})
}
}
@Singleton
class CustomAuthenticationProvider<Any> :
HttpRequestReactiveAuthenticationProvider<Any> {
override fun authenticate(
requestContext: HttpRequest<Any>?,
authenticationRequest: AuthenticationRequest<String, String>
): Publisher<AuthenticationResponse> {
val rsp = if (authenticationRequest.identity == "user" && authenticationRequest.secret == "password")
AuthenticationResponse.success("user")
else AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH)
return Mono.create { emitter -> emitter.success(rsp) }
}
}
10.1 Authentication Provider
The ReactiveAuthenticationProvider interface is a reactive API. If you prefer an imperative style, you can instead implement the AuthenticationProvider or HttpRequestAuthenticationProvider interface:
@Singleton
class CustomAuthenticationProvider<B> implements HttpRequestAuthenticationProvider<B> {
@Override
public AuthenticationResponse authenticate(HttpRequest<B> requestContext, AuthenticationRequest<String, String> authRequest) {
return (authRequest.getIdentity().equals("user") && authRequest.getSecret().equals("password"))
? AuthenticationResponse.success("user")
: AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH);
}
}
@Singleton
class CustomAuthenticationProviderCustomAuthenticationProvider<B> implements HttpRequestAuthenticationProvider<B> {
@Override
AuthenticationResponse authenticate(HttpRequest<B> requestContext, AuthenticationRequest<String, String> authRequest) {
authRequest.identity == "user" && authRequest.secret == "password"
? AuthenticationResponse.success("user")
: AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH)
}
}
@Singleton
class CustomAuthenticationProvider :
HttpRequestAuthenticationProvider<Any> {
override fun authenticate(
requestContext: HttpRequest<Any>?,
authRequest: AuthenticationRequest<String, String>
): AuthenticationResponse {
return if (authRequest.identity == "user" && authRequest.secret == "password")
AuthenticationResponse.success("user")
else AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH)
}
}
If your implementation is blocking (e.g., you fetch the user credentials from a database in a blocking way to check against the supplied authentication request), implement the ExecutorAuthenticationProvider or HttpRequestExecutorAuthenticationProvider interface. Those APIs specify the executor name, by default TaskExecutors.BLOCKING , where the code gets executed to avoid blocking the main reactive flow.
|
10.2 Authentication Providers Use Cases
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.
10.3 Built-In Authentication Providers
Micronaut comes with authentication providers for LDAP (LdapAuthenticationProvider) and the OAuth 2.0 password grant authentication flow (OauthPasswordAuthenticationProvider and OpenIdPasswordAuthenticationProvider). For any custom authentication, an authentication provider must be created.
11 Authorization Strategies
11.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:
Below is a sample of a 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:
HttpRequest request = HttpRequest.GET("/home").basicAuth('sherlock', 'password')
Read the Basic Authentication Micronaut Guide to learn more. |
11.2 Session Authorization
Micronaut supports Session based authentication.
Using the CLI
If you are creating your project using the Micronaut CLI, supply either the $ 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:
annotationProcessor("io.micronaut.security:micronaut-security-annotations")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-annotations</artifactId>
</path>
</annotationProcessorPaths>
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:
Check the Redirection configuration to customize session based authentication behaviour.
Example of Session-Based Authentication configuration
micronaut.security.authentication=session
micronaut.security.redirect.login-failure=/login/authFailed
micronaut:
security:
authentication: session
redirect:
login-failure: /login/authFailed
[micronaut]
[micronaut.security]
authentication="session"
[micronaut.security.redirect]
login-failure="/login/authFailed"
micronaut {
security {
authentication = "session"
redirect {
loginFailure = "/login/authFailed"
}
}
}
{
micronaut {
security {
authentication = "session"
redirect {
login-failure = "/login/authFailed"
}
}
}
}
{
"micronaut": {
"security": {
"authentication": "session",
"redirect": {
"login-failure": "/login/authFailed"
}
}
}
}
Read the following guides to learn more abut session based authentication:
Session-based authentication without redirection
When you set micronaut.security.authentication
to session
, you enable SessionLoginHandler and SessionLogoutHandler.
These handlers return 303 responses to the urls defined in the Redirection Configuration. Disable redirection configuration with micronaut.security.redirection.enabled=false
to respond with 200 responses instead.
11.3 JSON Web Token
The following configuration properties are available to customize token based authentication:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether the configuration is enabled. Default value true. |
|
java.lang.String |
|
|
java.lang.String |
|
|
java.lang.String |
If the entry used for the roles in the Authentication attributes map is a String, you can use the separator to split its value into multiple roles. Default value DEFAULT_ROLES_SEPARATOR. |
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 $ 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:
annotationProcessor("io.micronaut.security:micronaut-security-annotations")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-annotations</artifactId>
</path>
</annotationProcessorPaths>
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:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether JWT security is enabled. Default value (true). |
What does a JWT look like?
Header
A base64-encoded JSON like:
{
"alg": "HS256",
"typ": "JWT"
}
Claims
A base64-encoded JSON like:
{
"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.
11.3.1 Reading JWT Token
11.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:
The following configuration properties are available to customize how the Bearer Token will be read:
Property | Type | Description |
---|---|---|
|
boolean |
Set whether to enable bearer token authentication. Default value true. |
|
java.lang.String |
Sets the prefix to use for the auth token. Default value Bearer. |
|
java.lang.String |
Sets the header name to use. Default value Authorization. |
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:
String accessToken = rsp.body().accessToken
List<Book> books = gatewayClient.toBlocking().retrieve(HttpRequest.GET("/api/gateway")
.bearerAuth(accessToken), Argument.listOf(Book))
GET /protectedResource HTTP/1.1
Host: micronaut.example`
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjI5OTU5MjIsInN1YiI6ImppbWkiLCJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sImlhdCI6MTQyMjk5MjMyMn0.rA7A2Gwt14LaYMpxNRtrCdO24RGrfHtZXY9fIjV8x8o
Check out the Micronaut JWT authentication for a tutorial on Micronaut’s JWT support. |
11.3.1.2 Cookie Token Reader
You can send/read a JWT token from a Cookie too.
The following sequence illustrates the authentication flow:
Reading tokens from Cookies is disabled by default. Note that using JWT tokens from cookies requires JWT Authentication to be enabled.
Property | Type | Description |
---|---|---|
|
java.lang.String |
|
|
java.lang.Boolean |
|
|
java.lang.Boolean |
|
|
java.time.Duration |
|
|
Sets the same-site setting of the cookie. Default value null. Value is case sensitive. Allowed values: |
|
|
boolean |
Whether JWT cookie configuration is enabled. Default value (true). |
|
java.lang.String |
Cookie Name. Default value ("JWT"). |
|
java.lang.String |
The path of the cookie. Default value ("/"). |
Read the Micronaut JWT Authentication with Cookies to learn more. |
11.3.2 JWT Parser
You can inject a bean of type The JsonWebTokenParser to parse a JWT token. The parser decrypts the token if encrypted.
11.3.3 JWT Signature Validation
Micronaut security capabilities use signed JWT’s as specified by the JSON Web Signature specification.
Micronaut’s JWT validation supports multiple signature configurations. Thus, you can validate JSON Web tokens signed by different issuers in the same application.
To verify the signature of JWT tokens, you need beans of type SignatureConfiguration or ReactiveSignatureConfiguration
The easiest way is to create a bean of type SignatureConfiguration is to have in your app a bean of type RSASignatureConfiguration,
ECSignatureConfiguration, or
SecretSignatureConfiguration which must be qualified with @Named
since the configuration beans are used by factories (html,
ECSignatureConfiguration) or other beans (SecretSignature) which use
@EachBean to drive configuration.
The APIs JsonWebTokenSignatureValidator, and ReactiveJsonWebTokenSignatureValidator allows you to validate the signature of a JWT token.
ReactiveJsonWebTokenSignatureValidator
validates the signature using both SignatureConfiguration or ReactiveSignatureConfiguration.
JsonWebTokenSignatureValidator
validates the signature using beans of type SignatureConfiguration.
11.3.3.1 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
micronaut:
security:
token:
jwt:
signatures:
jwks:
awscognito:
url: 'https://cognito-idp.eu-west-1.amazonaws.com/eu-west-XXXX/.well-known/jwks.json'
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.jwks]
[micronaut.security.token.jwt.signatures.jwks.awscognito]
url="https://cognito-idp.eu-west-1.amazonaws.com/eu-west-XXXX/.well-known/jwks.json"
micronaut {
security {
token {
jwt {
signatures {
jwks {
awscognito {
url = "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
{
micronaut {
security {
token {
jwt {
signatures {
jwks {
awscognito {
url = "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
}
{
"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 ReactiveJwksSignature bean with a awscognito
name qualifier.
If you have the Micronaut HTTP Client on the classpath, then it will be used to retrieve the remote JWK Set. This allows for the configuration settings of the HTTP Client to be applied when fetching the resource. If a named service-specific client exists and the name of the service matches the name of the configured JWKS provider name, then that specific client instance will be used, otherwise a default Http Client instance (with any global configuration settings from micronaut.http.client.*
applied) will be used. If the Micronaut HTTP Client is not on the classpath, then the implementation will fall back to the internal resource fetching mechanism of the external JWT library dependency.
For example, to use an HTTP proxy for the fetching of the JWK Set from the above example:
micronaut.http.services.awscognito.url=https://cognito-idp.eu-west-1.amazonaws.com
micronaut.http.services.awscognito.proxy-type=http
micronaut.http.services.awscognito.proxy-address=proxy.company.net:8080
micronaut.security.token.jwt.signatures.jwks.awscognito.url=/eu-west-XXXX/.well-known/jwks.json
micronaut:
http:
services:
awscognito:
url: 'https://cognito-idp.eu-west-1.amazonaws.com'
proxy-type: 'http'
proxy-address: 'proxy.company.net:8080'
security:
token:
jwt:
signatures:
jwks:
awscognito:
url: '/eu-west-XXXX/.well-known/jwks.json'
[micronaut]
[micronaut.http]
[micronaut.http.services]
[micronaut.http.services.awscognito]
url="https://cognito-idp.eu-west-1.amazonaws.com"
proxy-type="http"
proxy-address="proxy.company.net:8080"
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.jwks]
[micronaut.security.token.jwt.signatures.jwks.awscognito]
url="/eu-west-XXXX/.well-known/jwks.json"
micronaut {
http {
services {
awscognito {
url = "https://cognito-idp.eu-west-1.amazonaws.com"
proxyType = "http"
proxyAddress = "proxy.company.net:8080"
}
}
}
security {
token {
jwt {
signatures {
jwks {
awscognito {
url = "/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
{
micronaut {
http {
services {
awscognito {
url = "https://cognito-idp.eu-west-1.amazonaws.com"
proxy-type = "http"
proxy-address = "proxy.company.net:8080"
}
}
}
security {
token {
jwt {
signatures {
jwks {
awscognito {
url = "/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
}
{
"micronaut": {
"http": {
"services": {
"awscognito": {
"url": "https://cognito-idp.eu-west-1.amazonaws.com",
"proxy-type": "http",
"proxy-address": "proxy.company.net:8080"
}
}
},
"security": {
"token": {
"jwt": {
"signatures": {
"jwks": {
"awscognito": {
"url": "/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
}
Note that the same approach will be applied when using a jwks_uri
supplied via Open ID Connect metadata.
The Micronaut HTTP Client based implementation can be explicitly disabled in favor of the JWT library’s internal resource fetching implementation by explicitly setting micronaut.security.token.jwt.signatures.jwks-client.http-client.enabled=false
. For example:
micronaut.security.token.jwt.signatures.jwks-client.http-client.enabled=false
micronaut.security.token.jwt.signatures.jwks.awscognito.url=/eu-west-XXXX/.well-known/jwks.json
micronaut:
security:
token:
jwt:
signatures:
jwks-client:
http-client:
enabled: false
jwks:
awscognito:
url: '/eu-west-XXXX/.well-known/jwks.json'
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.jwks-client]
[micronaut.security.token.jwt.signatures.jwks-client.http-client]
enabled=false
[micronaut.security.token.jwt.signatures.jwks]
[micronaut.security.token.jwt.signatures.jwks.awscognito]
url="/eu-west-XXXX/.well-known/jwks.json"
micronaut {
security {
token {
jwt {
signatures {
jwksClient {
httpClient {
enabled = false
}
}
jwks {
awscognito {
url = "/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
{
micronaut {
security {
token {
jwt {
signatures {
jwks-client {
http-client {
enabled = false
}
}
jwks {
awscognito {
url = "/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"token": {
"jwt": {
"signatures": {
"jwks-client": {
"http-client": {
"enabled": false
}
},
"jwks": {
"awscognito": {
"url": "/eu-west-XXXX/.well-known/jwks.json"
}
}
}
}
}
}
}
}
Exposing your Application’s Json Web Key Set (JKWS)
If you want to expose your own JWK Set, read the Keys Controller section.
11.3.3.1.1 JWKS Caching
Json Web Key Set (JWKS) fetched from a remote web server are cached. By default, they are cached via the internal bean ReactorCacheJwkSetFetcher using
Project Reactor’s Mono::cacheInvalidateIf
. You can configure the cache expiration with micronaut.security.token.jwt.signatures.jwks.*.cache-expiration
. It defaults to 60 seconds.
However, we recommend you enable caching via Micronaut Cache. This will be the only option in the future. |
Caching via Micronaut Cache
To cache JWKS with Micronaut Cache, you need to add an implementation of Micronaut Cache (E.g. Caffeine, Redis, Ehcache, Hazelcast, Coherence, Infinispan, EclipseStore, or MicroStream) and configure the expiration.
For example, you could add Caffeine implementation:
implementation("io.micronaut.cache:micronaut-cache-caffeine")
<dependency>
<groupId>io.micronaut.cache</groupId>
<artifactId>micronaut-cache-caffeine</artifactId>
</dependency>
The cache name is jwks
. You can configure the cache expiration with:
micronaut.caches.jwks.expire-after-write=24h
micronaut:
caches:
jwks:
expire-after-write: 24h
[micronaut]
[micronaut.caches]
[micronaut.caches.jwks]
expire-after-write="24h"
micronaut {
caches {
jwks {
expireAfterWrite = "24h"
}
}
}
{
micronaut {
caches {
jwks {
expire-after-write = "24h"
}
}
}
}
{
"micronaut": {
"caches": {
"jwks": {
"expire-after-write": "24h"
}
}
}
}
11.3.3.1.2 Local JWKS
You can specify a path starting with classpath:
or file:
to serve a JSON JWKS from anywhere on disk or in the classpath. For example to serve static resources from src/main/resources/jwks/certs.json
, you would use classpath:jwks/certs.json
as the path.
micronaut.security.token.jwt.signatures.jwks-static.google.path=classpath:jwks/certs.json
micronaut:
security:
token:
jwt:
signatures:
jwks-static:
google:
path: 'classpath:jwks/certs.json'
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.jwks-static]
[micronaut.security.token.jwt.signatures.jwks-static.google]
path="classpath:jwks/certs.json"
micronaut {
security {
token {
jwt {
signatures {
jwksStatic {
google {
path = "classpath:jwks/certs.json"
}
}
}
}
}
}
}
{
micronaut {
security {
token {
jwt {
signatures {
jwks-static {
google {
path = "classpath:jwks/certs.json"
}
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"token": {
"jwt": {
"signatures": {
"jwks-static": {
"google": {
"path": "classpath:jwks/certs.json"
}
}
}
}
}
}
}
}
11.3.4 JWT Validation
The APIs JsonWebTokenValidator, and ReactiveJsonWebTokenValidator allows you to validate a JWT token.
The validation performs the following steps:
-
- Parses the Token (if encrypted, it decrypts).
-
- Validates the Signature
-
- Validates the Claims with beans of type GenericJwtClaimsValidator.
JsonWebTokenValidator
will not use remote signature configuration. E.g. it will not validate the signature with remote JWKs.
Only, ReactiveJsonWebTokenValidator implements
TokenValidator.
11.3.5 JWT Token Generation
Micronaut relies on Nimbus JOSE + JWT library to provide JWT token signature and encryption.
The following configuration options are available:
Property | Type | Description |
---|---|---|
|
java.lang.Integer |
Access token expiration. Default value (3600). |
11.3.5.1 Signed JWT Generation
To generate a signed JWT you need to have in your app a bean of type
RSASignatureGeneratorConfiguration,
ECSignatureGeneratorConfiguration,
, or
SecretSignatureConfiguration which must be qualified with @Named
generator
since the configuration beans are used by factories (
RSASignatureGeneratorFactory,
ECSignatureGeneratorFactory) or other beans (SecretSignature) which use
@EachBean to drive configuration.
Remember to qualify with @Named generator your signature configuration beans which you wish to use to sign your JSON web tokens.
|
11.3.5.2 Example of JWT Signed with Secret
You can setup a SecretSignatureConfiguration qualified with @Named
generator
easily via configuration:
micronaut.security.token.jwt.signatures.secret.generator.secret=pleaseChangeThisSecretForANewOne
micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm=HS256
micronaut:
security:
token:
jwt:
signatures:
secret:
generator:
secret: pleaseChangeThisSecretForANewOne
jws-algorithm: HS256
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.secret]
[micronaut.security.token.jwt.signatures.secret.generator]
secret="pleaseChangeThisSecretForANewOne"
jws-algorithm="HS256"
micronaut {
security {
token {
jwt {
signatures {
secret {
generator {
secret = "pleaseChangeThisSecretForANewOne"
jwsAlgorithm = "HS256"
}
}
}
}
}
}
}
{
micronaut {
security {
token {
jwt {
signatures {
secret {
generator {
secret = "pleaseChangeThisSecretForANewOne"
jws-algorithm = "HS256"
}
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"token": {
"jwt": {
"signatures": {
"secret": {
"generator": {
"secret": "pleaseChangeThisSecretForANewOne",
"jws-algorithm": "HS256"
}
}
}
}
}
}
}
}
-
Change the
secret
property to your own secret and keep it safe. -
jws-algorithm
specifies the 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=
micronaut.security.token.jwt.signatures.secret.generator.base64=true
micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm=HS256
micronaut:
security:
token:
jwt:
signatures:
secret:
generator:
secret: 'cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU='
base64: true
jws-algorithm: HS256
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.secret]
[micronaut.security.token.jwt.signatures.secret.generator]
secret="cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU="
base64=true
jws-algorithm="HS256"
micronaut {
security {
token {
jwt {
signatures {
secret {
generator {
secret = "cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU="
base64 = true
jwsAlgorithm = "HS256"
}
}
}
}
}
}
}
{
micronaut {
security {
token {
jwt {
signatures {
secret {
generator {
secret = "cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU="
base64 = true
jws-algorithm = "HS256"
}
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"token": {
"jwt": {
"signatures": {
"secret": {
"generator": {
"secret": "cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU=",
"base64": true,
"jws-algorithm": "HS256"
}
}
}
}
}
}
}
}
-
This example of
secret
is Base64 encoded -
Set
base64
to signal that the secret is Base64 encoded
11.3.5.3 Example of JWT Signed with RSA
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 |
11.3.6 JWT Encryption
Signed claims prevent an attacker from tampering with 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 contain sensitive information, you can use a JSON Web Encryption algorithm to prevent them from being decoded.
Micronaut’s JWT validation supports multiple encryption configurations.
Beans of type RSAEncryptionConfiguration, ECEncryptionConfiguration, SecretEncryptionConfiguration participate as encryption configurations in the JWT validation.
Those beans need to be qualified with @Named
since the configuration beans are used by factories (RSAEncryptionFactory,
ECEncryptionFactory) or other beans (SecretEncryptionFactory) which use
@EachBean to drive configuration.
Use generator
as the @Named
qualifier if you want to use encryption configuration in the tokens your app generates.
The API JsonWebTokenEncryption allows you to decrypt an encrypted token.
11.3.6.1 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
micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm=HS256
pem.path=/home/user/rsa-2048bit-key-pair.pem
micronaut:
security:
token:
jwt:
signatures:
secret:
generator:
secret: pleaseChangeThisSecretForANewOne
jws-algorithm: HS256
pem:
path: /home/user/rsa-2048bit-key-pair.pem #(2)
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.secret]
[micronaut.security.token.jwt.signatures.secret.generator]
secret="pleaseChangeThisSecretForANewOne"
jws-algorithm="HS256"
[pem]
path="/home/user/rsa-2048bit-key-pair.pem"
micronaut {
security {
token {
jwt {
signatures {
secret {
generator {
secret = "pleaseChangeThisSecretForANewOne"
jwsAlgorithm = "HS256"
}
}
}
}
}
}
}
pem {
path = "/home/user/rsa-2048bit-key-pair.pem"
}
{
micronaut {
security {
token {
jwt {
signatures {
secret {
generator {
secret = "pleaseChangeThisSecretForANewOne"
jws-algorithm = "HS256"
}
}
}
}
}
}
}
pem {
path = "/home/user/rsa-2048bit-key-pair.pem"
}
}
{
"micronaut": {
"security": {
"token": {
"jwt": {
"signatures": {
"secret": {
"generator": {
"secret": "pleaseChangeThisSecretForANewOne",
"jws-algorithm": "HS256"
}
}
}
}
}
}
},
"pem": {
"path": "/home/user/rsa-2048bit-key-pair.pem"
}
}
-
Name the Signature configuration
generator
to make it participate in JWT token generation. -
pem.path
specifies the 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
}
}
-
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()
}
}
11.3.7 Claims Generation
If the built-in JWTClaimsSetGenerator, does not fulfil your needs you can provide your own replacement of ClaimsGenerator.
11.3.8 Claims Validation
The claims of a JSON Web Token are validated using every bean of type GenericJwtClaimsValidator.
Micronaut Security includes some validators by default:
Bean |
Description |
Enabled |
JWT |
Enabled when the |
|
JWT is not expired. It uses the |
Enabled by default. You can disable it by setting |
|
JWT |
Enabled when the |
|
If the JWT |
Disabled by default. Enabled when the |
|
JWT |
Enabled by default. You can disable it by setting |
If you are using micronaut.security.authentication: idtoken
, IdTokenClaimsValidator, a bean of type GenericJwtClaimsValidator, is registered in the bean context as well. IdTokenClaimsValidator
validates points 2-5 of the ID Token Validation section of the OpenID Connect Spec. You can disable it by setting micronaut.security.token.jwt.claims-validators.openid-idtoken
to false.
11.3.9 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.
11.3.10 Guides
Read the following guides to learn more about JSON Web Token and JSON Web Keys:
11.4 LDAP Authentication
Micronaut supports authentication with LDAP out of the box. To get started, add the security-ldap
dependency to your application.
compile "io.micronaut.security: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
.
11.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.
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether this configuration is enabled. Default true. |
Property | Type | Description |
---|---|---|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.util.Map |
Property | Type | Description |
---|---|---|
|
boolean |
|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.lang.String |
Property | Type | Description |
---|---|---|
|
boolean |
|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.lang.String |
|
|
boolean |
Sets if group search is enabled. Default false |
|
java.lang.String |
The argument to pass to the search filter. |
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>"
11.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 Authentication
, which only contains the username and any roles associated with the user. You can use Authentication attributes
to store additional data. Replace DefaultContextAuthenticationMapper and provide your own implementation.
@Singleton
@Replaces(DefaultContextAuthenticationMapper.class) (1)
public class MyContextAuthenticationMapper implements ContextAuthenticationMapper {
@Override
public AuthenticationResponse map(ConvertibleValues<Object> attributes, String username, Set<String> groups) {
// return an Authentication instance (Authentication::build methods) 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.
@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.
@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.
@Singleton
@Replaces(DefaultContextBuilder.class)
public class MyContextBuilder implements ContextBuilder {
}
Read LDAP and database authentication providers to learn more about LDAP Authentication: |
11.5 X.509 Certificate Authentication
Micronaut Security supports using X.509 client certificates with HTTPS to enable mutual authentication.
Once you have configured HTTPS in your application, users can install X.509 browser certificates to provide authentication information and access restricted URLs.
When X.509 is enabled, in addition to the client (e.g. browser or API client) verifying that the server certificate is valid (i.e. that it was issued and signed by a trusted certificate authority (CA)), the server can also verify the client with the certificate from the client SSL handshake. If the client certificate is valid and contains a username/principal that corresponds to an application user, access will be granted.
Configuration
There are two configuration options as seen in the following table:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Set the Subject DN regex. Default value "CN=(.*?)(?:, |
$)". |
|
boolean |
Use micronaut.security.x509.enabled
to enable X.509 support, or enable per-environment.
Use the micronaut.security.x509.subject-dn-regex
property to override the default regular expression used to extract the principal (username) from the certificate. Typically, the principal is stored in the CN
(Common Name) property, prefixed with "CN="
. The default regular expression "CN=(.*?)(?:,|$)"
will extract the text after "CN="
and up to the next (optional) delimiter, but if your certificates are configured differently, override the regex as needed.
In addition to X.509-specific configuration, you must also configure HTTPS for your server, and configure requesting client certificates during the SSL handshake. Set the value of the property micronaut.server.ssl.client-authentication
to want
or need
(depending on whether client certificates are optional or required).
Here’s a sample configuration enabling and configuring HTTPS and X.509:
micronaut.application.name=your_application_name
micronaut.security.x509.enabled=true
micronaut.ssl.enabled=true
micronaut.server.ssl.client-authentication=want
micronaut.server.ssl.key-store.path=classpath:ssl/keystore.p12
micronaut.server.ssl.key-store.password=your_keystore_password
micronaut.server.ssl.key-store.type=PKCS12
micronaut.server.ssl.trust-store.path=classpath:ssl/truststore.jks
micronaut.server.ssl.trust-store.password=your_truststore_password
micronaut.server.ssl.trust-store.type=JKS
micronaut:
application:
name: your_application_name
security:
x509:
enabled: true
ssl:
enabled: true
server:
ssl:
client-authentication: want # or 'need'
key-store:
path: classpath:ssl/keystore.p12
password: your_keystore_password
type: PKCS12
trust-store:
path: classpath:ssl/truststore.jks
password: your_truststore_password
type: JKS
[micronaut]
[micronaut.application]
name="your_application_name"
[micronaut.security]
[micronaut.security.x509]
enabled=true
[micronaut.ssl]
enabled=true
[micronaut.server]
[micronaut.server.ssl]
client-authentication="want"
[micronaut.server.ssl.key-store]
path="classpath:ssl/keystore.p12"
password="your_keystore_password"
type="PKCS12"
[micronaut.server.ssl.trust-store]
path="classpath:ssl/truststore.jks"
password="your_truststore_password"
type="JKS"
micronaut {
application {
name = "your_application_name"
}
security {
x509 {
enabled = true
}
}
ssl {
enabled = true
}
server {
ssl {
clientAuthentication = "want"
keyStore {
path = "classpath:ssl/keystore.p12"
password = "your_keystore_password"
type = "PKCS12"
}
trustStore {
path = "classpath:ssl/truststore.jks"
password = "your_truststore_password"
type = "JKS"
}
}
}
}
{
micronaut {
application {
name = "your_application_name"
}
security {
x509 {
enabled = true
}
}
ssl {
enabled = true
}
server {
ssl {
client-authentication = "want"
key-store {
path = "classpath:ssl/keystore.p12"
password = "your_keystore_password"
type = "PKCS12"
}
trust-store {
path = "classpath:ssl/truststore.jks"
password = "your_truststore_password"
type = "JKS"
}
}
}
}
}
{
"micronaut": {
"application": {
"name": "your_application_name"
},
"security": {
"x509": {
"enabled": true
}
},
"ssl": {
"enabled": true
},
"server": {
"ssl": {
"client-authentication": "want",
"key-store": {
"path": "classpath:ssl/keystore.p12",
"password": "your_keystore_password",
"type": "PKCS12"
},
"trust-store": {
"path": "classpath:ssl/truststore.jks",
"password": "your_truststore_password",
"type": "JKS"
}
}
}
}
}
Read the X.509 Authentication Micronaut Guide to learn about configuring applications to use X.509 and how to create the various certificates for testing. |
11.6 Custom Authorization Strategies
All authorization strategies implement the AuthenticationFetcher interface. The contract is designed to return an Authentication 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, if you use a product like SiteMinder that handles authentication for you, you can trust that users access your application are authenticated, and you can access their username via the SM_USER
request header and build an Authentication
from that:
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.filters.AuthenticationFetcher;
import jakarta.inject.Singleton;
import java.util.Collection;
import java.util.Collections;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Singleton
public class SiteminderAuthenticationFetcher implements AuthenticationFetcher<HttpRequest<?>> {
public static final String SITEMINDER_USER_HEADER = "SM_USER";
@Override
public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {
return Mono.<Authentication>create(emitter -> {
String siteminderUser = request.getHeaders().get(SITEMINDER_USER_HEADER);
if (StringUtils.isEmpty(siteminderUser)) {
emitter.success();
return;
}
Collection<String> roles = Collections.singleton("ROLE_USER");
emitter.success(Authentication.build(siteminderUser, roles));
});
}
}
12 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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Where the user is redirected to after a successful login. Default value ("/"). |
|
java.lang.String |
Where the user is redirected to after a failed login. Default value ("/"). |
|
java.lang.String |
URL where the user is redirected after logout. Default value ("/"). |
|
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. |
|
boolean |
Sets whether Redirection configuration enabled. Default value (true). |
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.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor;
import io.micronaut.security.authentication.AuthorizationException;
import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler;
import io.micronaut.security.config.RedirectConfiguration;
import io.micronaut.security.config.RedirectService;
import io.micronaut.security.errors.PriorToLoginPersistence;
import jakarta.inject.Singleton;
@Singleton
@Replaces(DefaultAuthorizationExceptionHandler.class)
public class MyRejectionHandler extends DefaultAuthorizationExceptionHandler {
public MyRejectionHandler(ErrorResponseProcessor<?> errorResponseProcessor,
RedirectConfiguration redirectConfiguration,
RedirectService redirectService,
@Nullable PriorToLoginPersistence priorToLoginPersistence) {
super(errorResponseProcessor, redirectConfiguration, redirectService, priorToLoginPersistence);
}
@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");
}
}
13 Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated
13.1 CSRF Dependency
Add the Micronaut Security CSRF dependency to protect against CSRF:
implementation("io.micronaut.security:micronaut-security-csrf")
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-csrf</artifactId>
</dependency>
13.2 CSRF Filter
The core of Micronaut Security CSRF implementation is io.micronaut.security.csrf.filter.CsrfFilter
.
A Server Filter which attempts to resolve a CSRF Token with
every bean of type CsrfTokenResolver and validates it with beans of type CsrfTokenValidator.
The following configuration options are available for the CSRF Filter:
Property | Type | Description |
---|---|---|
|
java.util.Set |
Filter will only process requests whose method matches any of these methods. Default Value is POST, PUT, DELETE, PATCH. |
|
java.util.Set |
Filter will only process requests whose content type matches any of these content types. Default Value is application/x-www-form-urlencoded, multipart/form-data. |
|
boolean |
Whether the filter is enabled. Default value true. |
|
java.lang.String |
CSRF filter processes only request paths matching this regular expression. Default Value: "^.*$" |
13.3 CSRF Mitigations
Ensure your application does not perform state-changing actions via the GET request method. Your application should perform state-changing actions only via POST, PUT, PATCH, or DELETE methods. |
13.3.1 Syncronizer Token Pattern
In a synchronized token pattern, the server generates a CSRF token and shares it with the client before returning it, usually through a hidden form parameter for the associated action. On form submission, the server checks the CSRF token against one stored in the user’s session. If they match, the request is approved; otherwise, it’s rejected
13.3.1.1 CSRF and Session
If you use Session-Based Authentication and Micronaut Security CSRF, a CSRF token is automatically generated upon login and saved into the HTTP Session. Micronaut Session provides an implementation CsrfTokenRepository which fetches the CSRF token from the user’s HTTP session. Thus, when the application sends new request to the sever with a CSRF token (e.g. in a hidden form field or HTTP Header), the server validates the supplied token against the value stored in the HTTP Session.
You can disable the CSRF Session repository by setting micronaut.security.csrf.repositories.session.enabled
to false.
13.3.2 Double Submit Cookie Pattern
In a double-submit cookie pattern, the server generates a CSRF token, and it sends the CSRF token to the client in a cookie.
Then, the server only needs to verify that following requests cookie’s value matches the CSRF token sent in a request parameter (a hidden form field) or header. This process is stateless, as the server doesn’t need to store any information about the CSRF token.
POST /transfer HTTP/1.1
Host: vulnerable bank
Content-Type: application/x-www-form-urlencoded
Cookie: session=<token>; __Host-csrfToken=o24b65486f506e2cd4403caf0d640024
[...]
amount=100&toUser=intended&csrfToken=o24b65486f506e2cd4403caf0d640024
When you use Micronaut Security Authentication cookie
, or
idtoken
a CSRF Token is saved in a Cookie upon login.
You can configure different cookie attributes. For example, by default the cookie name uses a __Host-
Cookie prefix, can extend security protections against CSRF Attacks.
13.3.3 Signed Double Submit Cookie Pattern
Signed Double-Submit Cookie. Sign the CSRF Token with to prevent attackers from overriding the cookie value with their own (e.g. with taken-over subdomain attacks) . |
To do sign the CSRF Token, set the property micronaut.security.csrf.signature-key
.
micronaut.security.csrf.signature-key=pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
micronaut:
security:
csrf:
signature-key: pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
[micronaut]
[micronaut.security]
[micronaut.security.csrf]
signature-key="pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
micronaut {
security {
csrf {
signatureKey = "pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
}
}
{
micronaut {
security {
csrf {
signature-key = "pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
}
}
}
{
"micronaut": {
"security": {
"csrf": {
"signature-key": "pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
}
}
}
13.4 CSRF Configuration
The following configuration options are available for CSRF:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. Default Value |
|
java.lang.String |
Key to look for the CSRF token in an HTTP Session. Default Value: "csrfToken". |
|
int |
Random value’s size in bytes. The random value used is used to build a CSRF Token. Default Value: 16. |
|
java.lang.String |
HTTP Header name to look for the CSRF token. Default Value: "X-CSRF-TOKEN". |
|
java.lang.String |
Field name in a form url encoded submission to look for the CSRF token. Default Value: "csrfToken". |
|
boolean |
Whether the CSRF integration is enabled. Default value true. |
|
java.lang.String |
Sets the domain name of this Cookie. Default value (null). |
|
java.lang.Boolean |
Sets whether the cookie is secured. Defaults to the secure status of the request. |
|
java.lang.String |
Cookie Name. |
|
java.lang.String |
Sets the path of the cookie. Default value ("/"). |
|
java.lang.Boolean |
Whether the Cookie can only be accessed via HTTP. Default value (true). |
|
java.time.Duration |
Sets the maximum age of the cookie. Default value ({@value AccessTokenConfigurationProperties#DEFAULT_EXPIRATION} seconds). |
|
Cookie Same Site Configuration. It defaults to Strict. |
13.5 CSRF Token Resolvers
Micronaut Security CSRF resolves a CSRF Token with beans of type CsrfTokenResolver
13.5.1 HTTP Header CSRF Token Resolution
Micronaut Security CSRF ships a io.micronaut.security.csrf.resolver.HttpHeaderCsrfTokenResolver
an implementation of CsrfTokenResolver which looks for a CSRF Token in a Request’s HTTP Header.
POST /transfer HTTP/1.1
Host: vulnerable bank
Content-Type: application/x-www-form-urlencoded
Cookie: session=<token>
X-CSRF-TOKEN: o24b65486f506e2cd4403caf0d640024
[...]
amount=100&toUser=intended
You can disable it by setting micronaut.security.csrf.token-resolvers.http-header.enabled=false
The HTTP Header name used by HttpHeaderCsrfTokenResolver
can be configured.
It is recommended to use a custom HTTP Header Name. By using a custom HTTP Header name, it will not be possible to send them cross-origin without a permissive CORS implementation.
Moreover, If possible, we recommend you to send the CSRF token via an HTTP Header instead of a form field as it is harder to attack.
For example, Turbo sends the CSRF token via a custom HTTP Header upon form submission. You can find information about Micronaut Turbo integration.
13.5.2 Field CSRF Token Resolution
Micronaut Security CSRF ships a io.micronaut.security.csrf.resolver.FieldCsrfTokenResolver
an implementation of CsrfTokenResolver which looks for a CSRF Token in a Request’s form-url-encoded field.
POST /transfer HTTP/1.1
Host: vulnerable bank
Content-Type: application/x-www-form-urlencoded
Cookie: session=<token>
[...]
amount=100&toUser=intended&csrfToken=o24b65486f506e2cd4403caf0d640024
You can disable it by setting micronaut.security.csrf.token-resolvers.field.enabled=false
13.6 CSRF APIs
The main APIs for CSRF protection are:
14 Token Propagation
Imagine you have a Gateway microservice which consumes three other microservices:
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
micronaut.security.token.jwt.signatures.secret.generator.secret=pleaseChangeThisSecretForANewOne
micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm=HS256
micronaut.security.token.propagation.header.enabled=true
micronaut.security.token.propagation.header.headerName=Authorization
micronaut.security.token.propagation.header.prefix=Bearer
micronaut.security.token.propagation.enabled=true
micronaut.security.token.propagation.service-id-regex=http://localhost:(8083|8081|8082)
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)"
[micronaut]
[micronaut.application]
name="gateway"
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.signatures]
[micronaut.security.token.jwt.signatures.secret]
[micronaut.security.token.jwt.signatures.secret.generator]
secret="pleaseChangeThisSecretForANewOne"
jws-algorithm="HS256"
[micronaut.security.token.propagation]
enabled=true
service-id-regex="http://localhost:(8083|8081|8082)"
[micronaut.security.token.propagation.header]
enabled=true
headerName="Authorization"
prefix="Bearer "
micronaut {
application {
name = "gateway"
}
security {
token {
jwt {
signatures {
secret {
generator {
secret = "pleaseChangeThisSecretForANewOne"
jwsAlgorithm = "HS256"
}
}
}
}
propagation {
header {
enabled = true
headerName = "Authorization"
prefix = "Bearer "
}
enabled = true
serviceIdRegex = "http://localhost:(8083|8081|8082)"
}
}
}
}
{
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)"
}
}
}
}
}
{
"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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.util.regex.Pattern |
|
|
java.util.regex.Pattern |
|
|
boolean |
Enables TokenPropagationHttpClientFilter. Default value false |
|
java.lang.String |
For propagation via an HTTP Header, you can configure:
Property | Type | Description |
---|---|---|
|
boolean |
Enable HttpHeaderTokenPropagator. Default value (true). |
|
java.lang.String |
|
|
java.lang.String |
Read the Token Propagation tutorial to learn more. |
15 Built-In Security Token Controllers
15.1 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:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether SignedRefreshTokenGenerator is enabled. Default value (true). |
|
com.nimbusds.jose.JWSAlgorithm |
{@link com.nimbusds.jose.JWSAlgorithm}. Defaults to HS256 |
|
java.lang.String |
shared secret. For HS256 must be at least 256 bits. |
|
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
micronaut:
security:
token:
jwt:
generator:
refresh-token:
secret: 'pleaseChangeThisSecretForANewOne'
[micronaut]
[micronaut.security]
[micronaut.security.token]
[micronaut.security.token.jwt]
[micronaut.security.token.jwt.generator]
[micronaut.security.token.jwt.generator.refresh-token]
secret="pleaseChangeThisSecretForANewOne"
micronaut {
security {
token {
jwt {
generator {
refreshToken {
secret = "pleaseChangeThisSecretForANewOne"
}
}
}
}
}
}
{
micronaut {
security {
token {
jwt {
generator {
refresh-token {
secret = "pleaseChangeThisSecretForANewOne"
}
}
}
}
}
}
}
{
"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:
Property | Type | Description |
---|---|---|
|
java.util.Set |
Supported content types for POST endpoints. Default Value application/json and application/x-www-form-urlencoded |
|
boolean |
Whether the controller is enabled. |
|
java.lang.String |
Sets the path to map the {@link OauthController} to. Default value ("/oauth/access_token"). |
|
boolean |
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::getAuthentication
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:
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. |
15.2 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:
Property | Type | Description |
---|---|---|
|
boolean |
|
|
java.lang.String |
Path to the KeysController. Default value "/keys". |
15.3 Introspection Endpoint
The Introspection endpoint exposes an endpoint to inquire the current state of a token.
POST /token_info
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic dXNlcjpwYXNzd29yZA==
token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6IjM1NjhjM2EzLWFlMmMtNDFiMy1hYzU5LTU0ZTkxODVkM2ViOCIsImlhdCI6MTYwMTA0OTU5OCwiZXhwIjoxNjAxMDUzMTk4fQ.Sc5Xh7jI6e_F3FAUo3n3AUCHNSxWH8t-WlM6JxeHZGI&token_type_hint=access_token
Moreover, you can access a secured GET endpoint which responds the introspection response for the authenticated user:
GET /token_info
Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6IjM1NjhjM2EzLWFlMmMtNDFiMy1hYzU5LTU0ZTkxODVkM2ViOCIsImlhdCI6MTYwMTA0OTU5OCwiZXhwIjoxNjAxMDUzMTk4fQ.Sc5Xh7jI6e_F3FAUo3n3AUCHNSxWH8t-WlM6JxeHZGI
responds:
{
"active": false
"username": "1234567890",
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"jti": "3568c3a3-ae2c-41b3-ac59-54e9185d3eb8",
"iat": 1601049598,
"exp": 1601053198
}
Property | Type | Description |
---|---|---|
|
boolean |
|
|
java.lang.String |
Path to the IntrospectionController. Default value "/token_info" |
16 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.core.annotation.Nullable
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 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.core.annotation.Nullable
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
@Controller("/user")
class UserController {
@Secured("isAnonymous()")
@Get("/myinfo")
Map myinfo(@Nullable Authentication authentication) {
if (authentication == null) {
return Collections.singletonMap("isLoggedIn", false);
}
return CollectionUtils.mapOf("isLoggedIn", true,
"username", authentication.getName(),
"roles", authentication.getRoles()
);
}
}
16.1 Custom Binding
You can create a custom argument binder to bind the authenticated user to a custom class tailored to your application needs.
If, in your application, the authenticated user has an email address, you can create a class such as:
package io.micronaut.security.docs.customauthentication;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record AuthenticationWithEmail(String username,
String email) {
public static AuthenticationWithEmail of(Authentication authentication) {
Object obj = authentication.getAttributes().get("email");
String email = obj == null ? null : obj.toString();
return new AuthenticationWithEmail(authentication.getName(), email);
}
}
package io.micronaut.security.docs.customauthentication
import groovy.transform.Canonical
import io.micronaut.security.authentication.Authentication
import io.micronaut.serde.annotation.Serdeable
@Canonical
@Serdeable
class AuthenticationWithEmail {
String username
String email
static AuthenticationWithEmail of(Authentication authentication) {
new AuthenticationWithEmail(authentication.getName(), authentication.getAttributes().get("email")?.toString())
}
}
package io.micronaut.security.docs.customauthentication
import io.micronaut.security.authentication.Authentication
import io.micronaut.serde.annotation.Serdeable
@Serdeable
data class AuthenticationWithEmail(val username: String, val email: String?) {
companion object {
fun of(authentication: Authentication): AuthenticationWithEmail {
val obj = authentication.getAttributes()["email"]
val email = obj?.toString()
return AuthenticationWithEmail(authentication.name, email)
}
}
}
and then a TypedRequestArgumentBinder
:
package io.micronaut.security.docs.customauthentication;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.filters.SecurityFilter;
import jakarta.inject.Singleton;
import java.util.Optional;
@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Singleton
public class AuthenticationWithEmailArgumentBinder implements TypedRequestArgumentBinder<AuthenticationWithEmail> {
private final Argument<AuthenticationWithEmail> argumentType;
public AuthenticationWithEmailArgumentBinder() {
argumentType = Argument.of(AuthenticationWithEmail.class);
}
@Override
public Argument<AuthenticationWithEmail> argumentType() {
return argumentType;
}
@Override
public BindingResult<AuthenticationWithEmail> bind(ArgumentConversionContext<AuthenticationWithEmail> context, HttpRequest<?> source) {
if (!source.getAttributes().contains(SecurityFilter.KEY)) {
return BindingResult.UNSATISFIED;
}
final Optional<Authentication> existing = source.getUserPrincipal(Authentication.class);
return existing.isPresent() ? (() -> existing.map(AuthenticationWithEmail::of)) : BindingResult.EMPTY;
}
}
package io.micronaut.security.docs.customauthentication
import io.micronaut.context.annotation.Requires
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.filters.SecurityFilter
import jakarta.inject.Singleton
@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Singleton
class AuthenticationWithEmailArgumentBinder implements TypedRequestArgumentBinder<AuthenticationWithEmail> {
private final Argument<AuthenticationWithEmail> argumentType;
AuthenticationWithEmailArgumentBinder() {
argumentType = Argument.of(AuthenticationWithEmail.class);
}
@Override
Argument<AuthenticationWithEmail> argumentType() {
return argumentType;
}
@Override
BindingResult<AuthenticationWithEmail> bind(ArgumentConversionContext<AuthenticationWithEmail> context, HttpRequest<?> source) {
if (!source.getAttributes().contains(SecurityFilter.KEY)) {
return BindingResult.UNSATISFIED
}
final Optional<Authentication> existing = source.getUserPrincipal(Authentication.class)
existing.isPresent() ? (() -> existing.map(AuthenticationWithEmail::of)) : BindingResult.EMPTY
}
}
package io.micronaut.security.docs.customauthentication
import io.micronaut.context.annotation.Requires
import io.micronaut.core.bind.ArgumentBinder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.filters.SecurityFilter
import jakarta.inject.Singleton
@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Singleton
class AuthenticationWithEmailArgumentBinder : TypedRequestArgumentBinder<AuthenticationWithEmail> {
override fun bind(context: ArgumentConversionContext<AuthenticationWithEmail>, source: HttpRequest<*>): ArgumentBinder.BindingResult<AuthenticationWithEmail> {
if (!source.attributes.contains(SecurityFilter.KEY)) {
return ArgumentBinder.BindingResult.unsatisfied()
}
val existing = source.getUserPrincipal(Authentication::class.java)
return if (existing.isPresent) ArgumentBinder.BindingResult {
existing.map(AuthenticationWithEmail::of)
} else ArgumentBinder.BindingResult.empty()
}
override fun argumentType(): Argument<AuthenticationWithEmail> {
return Argument.of(AuthenticationWithEmail::class.java)
}
}
Then you can bind it in a controller method parameter:
@Secured(SecurityRule.IS_AUTHENTICATED)
@Produces(MediaType.TEXT_PLAIN)
@Get("/custom-authentication")
String index(AuthenticationWithEmail authentication) {
return authentication.email();
}
@Secured(SecurityRule.IS_AUTHENTICATED)
@Produces(MediaType.TEXT_PLAIN)
@Get("/custom-authentication")
String index(AuthenticationWithEmail authentication) {
authentication.email
}
@Secured(SecurityRule.IS_AUTHENTICATED)
@Produces(MediaType.TEXT_PLAIN)
@Get("/custom-authentication")
fun index(authentication: AuthenticationWithEmail) = authentication.email
16.2 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.
SecurityService with Project Reactor
If you use Micronaut Reactor and access SecurityService within a reactive chain, add the following dependencies to handle the Reactor Context Propagation.
implementation("io.micronaut.reactor:micronaut-security-reactor")
<dependency>
<groupId>io.micronaut.reactor</groupId>
<artifactId>micronaut-security-reactor</artifactId>
</dependency>
implementation("io.micrometer:context-propagation")
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>context-propagation</artifactId>
</dependency>
17 Data Change Auditing
Micronaut Security provides integration with Micronaut Data for automatically capturing the identity of the user that created or updated a particular entity.
17.1 Auditing Annotations
The annotations @CreatedBy and @UpdatedBy annotations are provided for application to your Micronaut Data entities. The annotated fields will be automatically populated with the currently authenticated user’s identity. For example:
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.security.annotation.CreatedBy;
import io.micronaut.security.annotation.UpdatedBy;
import jakarta.validation.constraints.NotBlank;
@MappedEntity //1
public record Book(
@Id
@GeneratedValue
@Nullable
Long id,
@NotBlank
String title,
@NotBlank
String author,
@Nullable
@CreatedBy //2
String creator,
@Nullable
@UpdatedBy //3
String editor) {
}
import io.micronaut.core.annotation.Nullable
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.security.annotation.CreatedBy
import io.micronaut.security.annotation.UpdatedBy
import jakarta.validation.constraints.NotBlank
@MappedEntity //1
class Book {
@Id
@GeneratedValue
@Nullable
Long id
@NotBlank
String title
@NotBlank
String author
@CreatedBy //2
String creator
@UpdatedBy //3
String editor
}
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.security.annotation.CreatedBy
import io.micronaut.security.annotation.UpdatedBy
@MappedEntity //1
data class Book(
@field:Id
@field:GeneratedValue(GeneratedValue.Type.AUTO)
var id: Long? = null,
var title: String,
var author: String,
@field:CreatedBy //2
var creator: String? = null,
@UpdatedBy //3
var editor: String? = null
)
1 | The class is mapped and persisted by Micronaut Data |
2 | The creator field will be populated on save() |
3 | The editor field will be populated on both save() and update() |
17.2 Custom Authentication Mapping
A PrincipalToStringConverter is provided to map the current Authentication object to the annotated String fields. The default implementation maps the value of Principal.getName()
to the fields. To customize this mapping, you can provide your own TypeConverter
implementation that replaces PrincipalToStringConverter
. For example:
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.TypeConverter;
import io.micronaut.security.authentication.Authentication;
import jakarta.inject.Singleton;
import java.util.Optional;
@Singleton
public class AuthenticationToStringConverter implements TypeConverter<Authentication, String> { // (1)
@Override
public Optional<String> convert(Authentication authentication, Class<String> targetType, ConversionContext context) {
return Optional.ofNullable(authentication.getAttributes().get("CUSTOM_ID_ATTR")).map(Object::toString); // (2)
}
}
import io.micronaut.context.annotation.Requires
import io.micronaut.core.convert.ConversionContext
import io.micronaut.core.convert.TypeConverter
import io.micronaut.security.authentication.Authentication
import jakarta.inject.Singleton
@Singleton
class AuthenticationToStringConverter implements TypeConverter<Authentication, String> { // (1)
@Override
Optional<String> convert(Authentication authentication, Class<String> targetType, ConversionContext context) {
Optional.ofNullable(authentication.attributes.get("CUSTOM_ID_ATTR")).map(Object::toString) // (2)
}
}
import io.micronaut.context.annotation.Requires
import io.micronaut.core.convert.ConversionContext
import io.micronaut.core.convert.TypeConverter
import io.micronaut.security.authentication.Authentication
import jakarta.inject.Singleton
import java.util.*
@Singleton
class AuthenticationToStringConverter : TypeConverter<Authentication, String> { // (1)
override fun convert(
authentication: Authentication,
targetType: Class<String>,
context: ConversionContext
): Optional<String> {
return Optional.ofNullable(authentication.getAttributes()["CUSTOM_ID_ATTR"]).map { obj -> obj.toString() } // (3)
}
}
1 | Conversion between Authentication and String is implemented |
2 | The implementation maps a custom attribute to the auto-populated identity |
The type conversion mechanism could also be used to map Authentication
to more complex field types other than String, such as a custom domain-specific User
object.
18 Security Events
Micronaut security classes generate several ApplicationEvents which you can subscribe to.
Event Name |
Description |
Triggered when an unsuccessful login takes place. |
|
Triggered when a successful login takes place. |
|
Triggered when the user logs out. |
|
Triggered when a token is validated. |
|
Triggered when a JWT access token is generated. |
|
Triggered when a JWT refresh token is generated. |
To learn how to listen for events, see the Context Events section of the documentation.
19 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>>
micronaut.security.oauth2.clients.google.client-id=<<your client id>>
micronaut.security.oauth2.clients.google.openid.issuer=https://accounts.google.com
micronaut:
security:
oauth2:
clients:
google:
client-secret: <<your client secret>>
client-id: <<your client id>>
openid:
issuer: https://accounts.google.com
[micronaut]
[micronaut.security]
[micronaut.security.oauth2]
[micronaut.security.oauth2.clients]
[micronaut.security.oauth2.clients.google]
client-secret="<<your client secret>>"
client-id="<<your client id>>"
[micronaut.security.oauth2.clients.google.openid]
issuer="https://accounts.google.com"
micronaut {
security {
oauth2 {
clients {
google {
clientSecret = "<<your client secret>>"
clientId = "<<your client id>>"
openid {
issuer = "https://accounts.google.com"
}
}
}
}
}
}
{
micronaut {
security {
oauth2 {
clients {
google {
client-secret = "<<your client secret>>"
client-id = "<<your client id>>"
openid {
issuer = "https://accounts.google.com"
}
}
}
}
}
}
}
{
"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.
19.1 Installation
implementation("io.micronaut.security:micronaut-security-oauth2")
<dependency>
<groupId>io.micronaut.security</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.
19.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.security:micronaut-security-jwt")
<dependency>
<groupId>io.micronaut.security</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 by default. 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. To use the provider token directly, set the authentication mode to idtoken .
|
19.3 Flows
19.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.
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether the OAuth 2.0 support is enabled. Default value (true). |
|
java.lang.String |
The URI template that is used to initiate an OAuth 2.0 authorization code grant flow. Default value ("/oauth/login{/provider}"). |
|
java.lang.String |
The URI template that OAuth 2.0 providers can use to submit an authorization callback request. Default value ("/oauth/callback{/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.
-
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.
-
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.
-
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.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
@Filter(value = {"/oauth/login", "/oauth/login/*"})
public class OAuthCsrfFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(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.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher
@Filter(value = ["/oauth/login", "/oauth/login/*"])
class OAuthCsrfFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(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.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher
@Filter(value = ["/oauth/login", "/oauth/login/*"])
class OAuthCsrfFilter : HttpServerFilter {
override fun doFilter(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)
}
}
}
19.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.
19.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 OauthAuthenticationMapper
Configuration is quite simple. For example to configure authorization with Github:
micronaut.security.oauth2.clients.github.client-id=<<my client id>>
micronaut.security.oauth2.clients.github.client-secret=<<my client secret>>
micronaut.security.oauth2.clients.github.scopes[0]=user:email
micronaut.security.oauth2.clients.github.scopes[1]=read:user
micronaut.security.oauth2.clients.github.authorization.url=https://github.com/login/oauth/authorize
micronaut.security.oauth2.clients.github.token.url=https://github.com/login/oauth/access_token
micronaut.security.oauth2.clients.github.token.auth-method=client-secret-post
micronaut:
security:
oauth2:
clients:
github:
client-id: <<my client id>>
client-secret: <<my client secret>>
scopes:
- user:email
- read:user
authorization:
url: https://github.com/login/oauth/authorize
token:
url: https://github.com/login/oauth/access_token
auth-method: client-secret-post
[micronaut]
[micronaut.security]
[micronaut.security.oauth2]
[micronaut.security.oauth2.clients]
[micronaut.security.oauth2.clients.github]
client-id="<<my client id>>"
client-secret="<<my client secret>>"
scopes=[
"user:email",
"read:user"
]
[micronaut.security.oauth2.clients.github.authorization]
url="https://github.com/login/oauth/authorize"
[micronaut.security.oauth2.clients.github.token]
url="https://github.com/login/oauth/access_token"
auth-method="client-secret-post"
micronaut {
security {
oauth2 {
clients {
github {
clientId = "<<my client id>>"
clientSecret = "<<my client secret>>"
scopes = ["user:email", "read:user"]
authorization {
url = "https://github.com/login/oauth/authorize"
}
token {
url = "https://github.com/login/oauth/access_token"
authMethod = "client-secret-post"
}
}
}
}
}
}
{
micronaut {
security {
oauth2 {
clients {
github {
client-id = "<<my client id>>"
client-secret = "<<my client secret>>"
scopes = ["user:email", "read:user"]
authorization {
url = "https://github.com/login/oauth/authorize"
}
token {
url = "https://github.com/login/oauth/access_token"
auth-method = "client-secret-post"
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"oauth2": {
"clients": {
"github": {
"client-id": "<<my client id>>",
"client-secret": "<<my client secret>>",
"scopes": ["user:email", "read:user"],
"authorization": {
"url": "https://github.com/login/oauth/authorize"
},
"token": {
"url": "https://github.com/login/oauth/access_token",
"auth-method": "client-secret-post"
}
}
}
}
}
}
}
-
Configure a client. The name
github
is arbitrary -
Provide values for
client-id
andclient-secret
-
Optionally specify desired
scopes
-
Provide an
authorization
endpoint URL -
Additionally, the
token
endpoint URL and authentication method -
auth-method
is 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.
|
19.3.1.1.2 User Details Mapper
Beyond configuration, an implementation of OauthAuthenticationMapper 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 Authentication. 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 Authentication 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.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.micronaut.core.annotation.Introspected;
@Introspected
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class GithubUser {
private String login;
private String name;
private String email;
// getters and setters ...
}
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.micronaut.core.annotation.Introspected
@Introspected
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
class GithubUser {
String login
String name
String email
}
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.micronaut.core.annotation.Introspected
@Introspected
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
class GithubUser {
lateinit var login: String
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 org.reactivestreams.Publisher;
@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
public interface GithubApiClient {
@Get("/user")
Publisher<GithubUser> getUser(@Header("Authorization") String authorization);
}
import io.micronaut.http.annotation.Header
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
interface GithubApiClient {
@Get("/user")
Publisher<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 org.reactivestreams.Publisher
@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
interface GithubApiClient {
@Get("/user")
fun getUser(@Header("Authorization") authorization: String): Publisher<GithubUser>
}
Create the user details mapper that pulls it together:
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OauthAuthenticationMapper;
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import java.util.Collections;
import java.util.List;
@Named("github") // (1)
@Singleton
class GithubAuthenticationMapper implements OauthAuthenticationMapper {
private final GithubApiClient apiClient;
GithubAuthenticationMapper(GithubApiClient apiClient) { // (2)
this.apiClient = apiClient;
}
@Override
public Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse, @Nullable State state) { // (3)
return Flux.from(apiClient.getUser("token " + tokenResponse.getAccessToken()))
.map(user -> {
List<String> roles = Collections.singletonList("ROLE_GITHUB");
return AuthenticationResponse.success(user.getLogin(), roles); // (4)
});
}
}
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OauthAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@Named("github") // (1)
@Singleton
class GithubAuthenticationMapper implements OauthAuthenticationMapper {
private final GithubApiClient apiClient
GithubAuthenticationMapper(GithubApiClient apiClient) { // (2)
this.apiClient = apiClient
}
@Override
Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse, @Nullable State state) { // (3)
Flux.from(apiClient.getUser("token ${tokenResponse.accessToken}"))
.map({ user ->
AuthenticationResponse.success(user.login, ["ROLE_GITHUB"]) // (4)
})
}
}
import io.micronaut.context.annotation.Requires
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OauthAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@Named("github") // (1)
@Singleton
internal class GithubAuthenticationMapper(private val apiClient: GithubApiClient) // (2)
: OauthAuthenticationMapper {
override fun createAuthenticationResponse(tokenResponse: TokenResponse, state: State?): Publisher<AuthenticationResponse> { // (3)
return Flux.from(apiClient.getUser("token " + tokenResponse.accessToken))
.map { user ->
AuthenticationResponse.success(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 Authentication. |
19.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.
19.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
micronaut.security.oauth2.clients.okta.client-id=<<my client id>>
micronaut.security.oauth2.clients.okta.client-secret=<<my client secret>>
micronaut.security.oauth2.clients.okta.openid.issuer=<<my openid issuer>>
micronaut:
security:
oauth2:
clients:
okta:
client-id: <<my client id>>
client-secret: <<my client secret>>
openid:
issuer: <<my openid issuer>>
[micronaut]
[micronaut.security]
[micronaut.security.oauth2]
[micronaut.security.oauth2.clients]
[micronaut.security.oauth2.clients.okta]
client-id="<<my client id>>"
client-secret="<<my client secret>>"
[micronaut.security.oauth2.clients.okta.openid]
issuer="<<my openid issuer>>"
micronaut {
security {
oauth2 {
clients {
okta {
clientId = "<<my client id>>"
clientSecret = "<<my client secret>>"
openid {
issuer = "<<my openid issuer>>"
}
}
}
}
}
}
{
micronaut {
security {
oauth2 {
clients {
okta {
client-id = "<<my client id>>"
client-secret = "<<my client secret>>"
openid {
issuer = "<<my openid issuer>>"
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"oauth2": {
"clients": {
"okta": {
"client-id": "<<my client id>>",
"client-secret": "<<my client secret>>",
"openid": {
"issuer": "<<my openid issuer>>"
}
}
}
}
}
}
}
-
Configure a client. The name
okta
is arbitrary -
Provide values for
client-id
andclient-secret
-
Specify an OpenID provider issuer url
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:
Property | Type | Description |
---|---|---|
|
java.net.URL |
URL using the https scheme with no query or fragment component that the Open ID provider asserts as its issuer identifier. |
|
java.lang.String |
The configuration path to discover openid configuration. Default ("/.well-known/openid-configuration"). |
|
java.lang.String |
The JWKS signature URI. |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
Determines the authorization processing flow to be used. Default value (code). |
|
|
java.lang.String |
Mechanism to be used for returning authorization response parameters from the authorization endpoint. |
|
Controls how the authentication interface is displayed. |
|
|
Controls how the authentication server prompts the user. |
|
|
java.lang.Integer |
Maximum authentication age. |
|
java.util.List |
Preferred locales for authentication. |
|
java.util.List |
Authentication class reference values. |
|
java.lang.String |
Code Challenge Method to use for PKCE. |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
||
|
java.lang.String |
Authentication Method |
|
The content type of token endpoint requests. Default value (application/x-www-form-urlencoded). |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
boolean |
The end session enabled flag. Default value (true). |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
19.3.1.2.2 OAuth 2.0 Authentication 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 OpenIdAuthenticationMapper has been provided for you to map the JWT token to a Authentication. 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.
Property | Type | Description |
---|---|---|
|
boolean |
Set to true if the original JWT from the provider should be included in the Micronaut JWT. Default value (false). |
|
boolean |
Set to true if the original access token from the provider should be included in the Micronaut JWT. Default value (false). |
|
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.
You cannot use micronaut.security.authentication = idtoken , if you wish to use a custom OpenIdAuthenticationMapper . You should use micronaut.security.authentication = cookie or provide your own implementation of Login Handler and Logout Handler.
|
To override the global default mapper, register a bean that replaces DefaultOpenIdAuthenticationMapper.
import io.micronaut.context.annotation.Replaces;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@Singleton
@Replaces(DefaultOpenIdAuthenticationMapper.class)
public class GlobalOpenIdAuthenticationMapper implements OpenIdAuthenticationMapper {
@Override
@NonNull
public Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims, @Nullable State state) {
return Flux.just(AuthenticationResponse.success("name"));
}
}
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@Singleton
@Replaces(DefaultOpenIdAuthenticationMapper.class)
class GlobalOpenIdAuthenticationMapper implements OpenIdAuthenticationMapper {
@Override
@NonNull
Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims, @Nullable State state) {
return Flux.just(AuthenticationResponse.success("name"));
}
}
import io.micronaut.context.annotation.Replaces
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@Singleton
@Replaces(DefaultOpenIdAuthenticationMapper::class)
class GlobalOpenIdAuthenticationMapper : OpenIdAuthenticationMapper {
override fun createAuthenticationResponse(providerName: String, tokenResponse: OpenIdTokenResponse, openIdClaims: OpenIdClaims, state: State?): Publisher<AuthenticationResponse> {
return Flux.just(AuthenticationResponse.success("name"));
}
}
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 io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@Singleton
@Named("okta") // (1)
public class OktaAuthenticationMapper implements OpenIdAuthenticationMapper {
@Override
@NonNull
public Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName, // (2)
OpenIdTokenResponse tokenResponse, // (3)
OpenIdClaims openIdClaims, // (4)
@Nullable State state) { // (5)
return Flux.just(AuthenticationResponse.success("name")); // (6)
}
}
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@Singleton
@Named("okta") // (1)
class OktaAuthenticationMapper implements OpenIdAuthenticationMapper {
@Override
@NonNull
Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName, // (2)
OpenIdTokenResponse tokenResponse, // (3)
OpenIdClaims openIdClaims, // (4)
@Nullable State state) { // (5)
Flux.just(AuthenticationResponse.success("name")); // (6)
}
}
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.oauth2.endpoint.authorization.state.State
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@Singleton
@Named("okta") // (1)
class OktaAuthenticationMapper : OpenIdAuthenticationMapper {
override fun createAuthenticationResponse(providerName: String, // (2)
tokenResponse: OpenIdTokenResponse, // (3)
openIdClaims: OpenIdClaims, // (4)
state: State?) // (5)
: Publisher<AuthenticationResponse> {
return Flux.just(AuthenticationResponse.success("name")) // (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 AuthenticationResponse is returned |
19.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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
Determines the authorization processing flow to be used. Default value (code). |
|
|
java.lang.String |
Mechanism to be used for returning authorization response parameters from the authorization endpoint. |
|
Controls how the authentication interface is displayed. |
|
|
Controls how the authentication server prompts the user. |
|
|
java.lang.Integer |
Maximum authentication age. |
|
java.util.List |
Preferred locales for authentication. |
|
java.util.List |
Authentication class reference values. |
|
java.lang.String |
Code Challenge Method to use for PKCE. |
19.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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Sets the mechanism to persist the nonce for later retrieval for validation. Supported values ("session", "cookie"). Default value ("cookie"). |
|
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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Sets the domain name of this Cookie. Default value (null). |
|
java.lang.Boolean |
Sets whether the cookie is secured. Defaults to the secure status of the request. |
|
java.lang.String |
Cookie Name. Default value |
|
java.lang.String |
Sets the path of the cookie. Default value ("/"). |
|
java.lang.Boolean |
Whether the Cookie can only be accessed via HTTP. Default value (true). |
|
java.time.Duration |
Sets the maximum age of the cookie. Default value (5 minutes). |
To use (SessionNoncePersistence`). which stores the nonce in a HTTP session:
-
Add a dependency to
micronaut-session
implementation("io.micronaut.security:micronaut-session")
<dependency> <groupId>io.micronaut.security</groupId> <artifactId>micronaut-session</artifactId> </dependency>
-
Set the nonce persistence to
session
micronaut.security.oauth2.nonce.persistence=session
micronaut: security: oauth2: nonce: persistence: session
[micronaut] [micronaut.security] [micronaut.security.oauth2] [micronaut.security.oauth2.nonce] persistence="session"
micronaut { security { oauth2 { nonce { persistence = "session" } } } }
{ micronaut { security { oauth2 { nonce { persistence = "session" } } } } }
{ "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 |
Builds a |
||
Validates an OpenID token response (including the nonce). |
||
Validates the nonce claim in the token response |
||
Stores the nonce to be retrieved later to allow validation |
To override the behavior of any of those beans, provide an implementation and replace the default one.
19.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.
19.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.
19.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.
19.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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Sets the domain name of this Cookie. Default value (null). |
|
java.lang.Boolean |
Sets whether the cookie is secured. Defaults to the secure status of the request. |
|
java.lang.String |
Cookie Name. Default value |
|
java.lang.String |
Sets the path of the cookie. Default value ("/"). |
|
java.lang.Boolean |
Whether the Cookie can only be accessed via HTTP. Default value (true). |
|
java.time.Duration |
Sets the maximum age of the cookie. Default value (5 minutes). |
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:
-
Add a dependency to
micronaut-session
implementation("io.micronaut.security:micronaut-session")
<dependency> <groupId>io.micronaut.security</groupId> <artifactId>micronaut-session</artifactId> </dependency>
-
Set the state persistence to
session
micronaut.security.oauth2.state.persistence=session
micronaut.security.oauth2.state.persistence: session
"micronaut.security.oauth2.state.persistence"="session"
micronaut.security.oauth2.state.persistence = "session"
{ "micronaut.security.oauth2.state.persistence" = "session" }
{ "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 |
Builds a State |
||
Serializes and de-serializes the state object for use in the authorization request |
||
Validates the state received in the authorization response |
||
Stores the state to be retrieved later to allow validation |
To override the behavior of any of those beans, provide an implementation and replace the default one.
19.3.1.4 PKCE
Using OpenID Connect Discovery by setting micronaut.security.oauth2.clients.*.openid.issuer
and the Authorization server specifies via
code_challenge_methods
either plain
, S256
, or both, Micronaut security automatically sends a code challenge in the authorization request as specified in Proof Key for Code Exchange (PKCE) Spec.
Using manual OAuth 2.0 Client configuration, you can specify the challenge method supported by setting micronaut.security.oauth2.clients.*.authorization.code-challenge-method
.
If the built-in implementation does not fulfill your needs, you can provide a replacement of bean CodeVerifierGenerator or PkceGenerator.
19.3.2 Client Credentials
You can obtain a bean of type ClientCredentialsClient to request an access token via a Client Credentials Grant for your OAuth 2.0 Clients.
For example:
micronaut.security.oauth2.clients.companyauthserver.client-id=XXX
micronaut.security.oauth2.clients.companyauthserver.client-secret=YYY
micronaut.security.oauth2.clients.companyauthserver.token.url=https://foo.bar/token
micronaut.security.oauth2.clients.companyauthserver.token.auth-method=client_secret_basic
micronaut.security.oauth2.clients.google.client-id=ZZZZ
micronaut.security.oauth2.clients.google.client-secret=PPPP
micronaut.security.oauth2.clients.google.openid.issuer=https://accounts.google.com
micronaut:
security:
oauth2:
clients:
companyauthserver:
client-id: 'XXX'
client-secret: 'YYY'
token:
url: "https://foo.bar/token"
auth-method: "client_secret_basic"
google:
client-id: 'ZZZZ'
client-secret: 'PPPP'
openid:
issuer: https://accounts.google.com
[micronaut]
[micronaut.security]
[micronaut.security.oauth2]
[micronaut.security.oauth2.clients]
[micronaut.security.oauth2.clients.companyauthserver]
client-id="XXX"
client-secret="YYY"
[micronaut.security.oauth2.clients.companyauthserver.token]
url="https://foo.bar/token"
auth-method="client_secret_basic"
[micronaut.security.oauth2.clients.google]
client-id="ZZZZ"
client-secret="PPPP"
[micronaut.security.oauth2.clients.google.openid]
issuer="https://accounts.google.com"
micronaut {
security {
oauth2 {
clients {
companyauthserver {
clientId = "XXX"
clientSecret = "YYY"
token {
url = "https://foo.bar/token"
authMethod = "client_secret_basic"
}
}
google {
clientId = "ZZZZ"
clientSecret = "PPPP"
openid {
issuer = "https://accounts.google.com"
}
}
}
}
}
}
{
micronaut {
security {
oauth2 {
clients {
companyauthserver {
client-id = "XXX"
client-secret = "YYY"
token {
url = "https://foo.bar/token"
auth-method = "client_secret_basic"
}
}
google {
client-id = "ZZZZ"
client-secret = "PPPP"
openid {
issuer = "https://accounts.google.com"
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"oauth2": {
"clients": {
"companyauthserver": {
"client-id": "XXX",
"client-secret": "YYY",
"token": {
"url": "https://foo.bar/token",
"auth-method": "client_secret_basic"
}
},
"google": {
"client-id": "ZZZZ",
"client-secret": "PPPP",
"openid": {
"issuer": "https://accounts.google.com"
}
}
}
}
}
}
}
You can obtain a bean of type ClientCredentialsClient for any of the OAuth 2.0 clients using a Name Qualifier.
public MyClass(@Named("google") ClientCredentialsClient googleClientCredentialclient) {
...
}
or
...
beanContext.getBean(ClientCredentialsClient.class, Qualifiers.byName("companyauthserver"))
ClientCredentialsClient caches the token response. If the cached access token is expired, they renew it automatically.
19.3.2.1 HTTP Client Filter for Client Credentials
Micronaut Security includes ClientCredentialsHttpClientFilter. This HTTP Client Filter allows you to automatically include an access token to an outgoing request HTTP Header. It obtains the access token via a Client Credentials request.
For example, the next configuration adds an access token to the requests' HTTP Headers done via the HTTP Client inventory
. It obtains the access token with a Client Credentials request with the OAuth 2.0 client companyauthserver
.
micronaut.security.oauth2.clients.companyauthserver.client-id=XXX
micronaut.security.oauth2.clients.companyauthserver.client-secret=YYY
micronaut.security.oauth2.clients.companyauthserver.client-credentials.service-id-regex=inventory
micronaut.security.oauth2.clients.companyauthserver.token.url=https://foo.bar/token
micronaut.security.oauth2.clients.companyauthserver.token.auth-method=client_secret_basic
micronaut:
security:
oauth2:
clients:
companyauthserver:
client-id: 'XXX'
client-secret: 'YYY'
client-credentials:
service-id-regex: 'inventory'
token:
url: "https://foo.bar/token"
auth-method: client_secret_basic
[micronaut]
[micronaut.security]
[micronaut.security.oauth2]
[micronaut.security.oauth2.clients]
[micronaut.security.oauth2.clients.companyauthserver]
client-id="XXX"
client-secret="YYY"
[micronaut.security.oauth2.clients.companyauthserver.client-credentials]
service-id-regex="inventory"
[micronaut.security.oauth2.clients.companyauthserver.token]
url="https://foo.bar/token"
auth-method="client_secret_basic"
micronaut {
security {
oauth2 {
clients {
companyauthserver {
clientId = "XXX"
clientSecret = "YYY"
clientCredentials {
serviceIdRegex = "inventory"
}
token {
url = "https://foo.bar/token"
authMethod = "client_secret_basic"
}
}
}
}
}
}
{
micronaut {
security {
oauth2 {
clients {
companyauthserver {
client-id = "XXX"
client-secret = "YYY"
client-credentials {
service-id-regex = "inventory"
}
token {
url = "https://foo.bar/token"
auth-method = "client_secret_basic"
}
}
}
}
}
}
}
{
"micronaut": {
"security": {
"oauth2": {
"clients": {
"companyauthserver": {
"client-id": "XXX",
"client-secret": "YYY",
"client-credentials": {
"service-id-regex": "inventory"
},
"token": {
"url": "https://foo.bar/token",
"auth-method": "client_secret_basic"
}
}
}
}
}
}
}
The following configuration options are available per OAuth 2.0 client:
Property | Type | Description |
---|---|---|
|
java.lang.String |
|
|
java.lang.String |
|
|
java.time.Duration |
Number of seconds for a token obtained via client credentials grant to be considered expired prior to its expiration date. Default value (30 seconds). |
|
java.lang.String |
Scope to be requested in the client credentials request. Defaults to none. |
|
boolean |
Enables ClientCredentialsClient. Default value true |
|
java.util.Map |
19.3.2.2 Guides
Read the following guides to learn more abut Client Credentials Flow:
19.3.3 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
OpenID Provider
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
micronaut:
security:
oauth2:
clients:
github:
grant-type: password
[micronaut]
[micronaut.security]
[micronaut.security.oauth2]
[micronaut.security.oauth2.clients]
[micronaut.security.oauth2.clients.github]
grant-type="password"
micronaut {
security {
oauth2 {
clients {
github {
grantType = "password"
}
}
}
}
}
{
micronaut {
security {
oauth2 {
clients {
github {
grant-type = "password"
}
}
}
}
}
}
{
"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.
19.4 Endpoints
19.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:
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
:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The URI used to log out of an OpenID provider. Default value ("/oauth/logout"). |
Property | Type | Description |
---|---|---|
|
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.
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
boolean |
The end session enabled flag. Default value (true). |
19.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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
||
|
java.lang.String |
Authentication Method |
19.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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
|
||
|
java.lang.String |
Authentication Method |
19.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:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
19.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).
19.6 Guides
Read the following guides to learn more abut OAuth:
20 Ahead-of-time optimizations
Micronaut AOT is a framework which implements ahead-of-time (AOT) optimizations for Micronaut application and libraries. Those optimizations consist of computing at build time things which would normally be done at runtime]
Micronaut Security offers several ahead-of-time optimizations.
20.1 Micronaut Security AOT Configuration
With Micronaut Gradle Plugin, you can use aotPlugins
configuration to declare additional AOT modules to be used:
dependencies {
...
..
//http://github.com/micronaut-projects/micronaut-security/releases
aotPlugins("io.micronaut.security:micronaut-security-aot:3.9.0")
}
for Micronaut Maven Plugin you will need to do:
<build>
<plugins>
<plugin>
<groupId>io.micronaut.build</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
<configuration>
<aotDependencies>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-aot</artifactId>
<version>3.9.0</version>
</dependency>
...
</aotDependencies>
</configuration>
</plugin>
</plugins>
</build>
20.2 OpenID Configuration Optimization
When you use OpenID Connect in a Micronaut application, the application constructs a URL by appending .well-known/openid-configuration
to the value you specify for each OAuth 2.0 client via
micronaut.security.oauth2.clients.*.openid.issuer
. It visits the URL to fetch OpenID Connect metadata related to the specified authorization server and configure itself.
However, this is a network request which may be slow. Moreover, the application will probably need to execute this network request to serve the first request.
You can use Micronaut Security ahead-of-time build optimizations to do the request at build-time.
To enable this optimization, add
micronaut.security.openid-configuration.enabled=true
to aot.properties
.
The application uses the configuration present at the authorization server at build time. The configuration may be out-of-date if the authorization server changes the configuration between the build time and the application startup. |
20.3 JWKS Optimization
Micronaut security supports JWKS (JSON Web Key Sets) either specifying them directly or via the jwks_uri
obtained when fetching the OpenID Connect metadata. Micronaut security uses JWKS to validate the signature of tokens issued by another application or an authorization server.
Micronaut applications need to make a network request to fetch JWKS. You can use Micronaut Security ahead-of-time build optimizations to make the request at build-time.
To enable this optimization, add
micronaut.security.jwks.enabled.enabled=true
to aot.properties
.
The application uses the JWKS exposed by the authorization server at build time. The JWKS may be out-of-date if the authorization server changes them between build time and the application startup. You can clear keys with JwksSignature::clear or JwkSetFetcher::clearCache .
|
21 Repository
You can find the source code of this project in this repository: