$ mn create-app my-app --features security-jwt
Table of Contents
Micronaut Security
Official Security Solution for Micronaut
Version:
1 Introduction
Micronaut Security is a fully featured and customizable security solution for your applications.
This module requires Micronaut 1.3.x. |
Getting Started
Using the CLI
If you are creating your project using the Micronaut CLI, supply either the |
To use the Micronaut’s security capabilities you must have the security
dependency on your classpath. For example in build.gradle
:
annotationProcessor "io.micronaut:micronaut-security"
compile "io.micronaut:micronaut-security"
Enable security capabilities with:
Property | Type | Description |
---|---|---|
|
boolean |
If Security is enabled. Default value false |
|
java.util.List |
Map that defines the interception patterns. |
|
java.util.List |
Allowed IP patterns. Default value (["0.0.0.0"]) |
Once you enable security, Micronaut returns HTTP Status Unauthorized (401) for any endpoint invocation.
2 What's New
Micronaut Security 1.3.1 includes the following changes:
Startup Time Improvements
For users of the OAuth module, you should see an improvement in startup time because the loading of the provider metadata is now done lazily.
3 Authentication Providers
To authenticate users you must provide implementations of AuthenticationProvider.
The following code snippet illustrates a naive implementation:
@Singleton
public class AuthenticationProviderUserPassword implements AuthenticationProvider {
@Override
public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) {
if (authenticationRequest.getIdentity().equals("user") && authenticationRequest.getSecret().equals("password")) {
return Flowable.just(new UserDetails("user", new ArrayList<>()));
}
return Flowable.just(new AuthenticationFailed());
}
}
The built-in Login Controller uses every available authentication provider. Authentication strategies, such as basic auth, where the credentials are present in the request use the available authentication providers too.
Micronaut ships with DelegatingAuthenticationProvider which can be typically used in environments such as the one described in the following diagram.
DelegatingAuthenticationProvider
is not enabled unless you provide implementations for UserFetcher,
PasswordEncoder and AuthoritiesFetcher
Read the LDAP and Database authentication providers to learn more. |
4 Security Rules
The decision to allow access to a particular endpoint to anonymous or authenticated users is determined by a collection of Security Rules. Micronaut ships with several built-in security rules. If they don’t fulfil your needs, you can implement your own SecurityRule.
4.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:
enabled: true
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.
4.2 Secured Annotation
As illustrated below, you can use Secured annotation to configure access at Controller or Controller’s Action level.
@Controller("/example")
@Secured(SecurityRule.IS_AUTHENTICATED) (1)
public class ExampleController {
@Produces(MediaType.TEXT_PLAIN)
@Get("/admin")
@Secured({"ROLE_ADMIN", "ROLE_X"}) (2)
public String withroles() {
return "You have ROLE_ADMIN or ROLE_X roles";
}
@Produces(MediaType.TEXT_PLAIN)
@Get("/anonymous")
@Secured(SecurityRule.IS_ANONYMOUS) (3)
public String anonymous() {
return "You are anonymous";
}
@Produces(MediaType.TEXT_PLAIN)
@Get("/authenticated") (1)
public String authenticated(Authentication authentication) {
return authentication.getName() + " is authenticated";
}
}
1 | Authenticated users are able to access authenticated Controller’s action. |
2 | Users granted role ROLE_ADMIN or ROLE_X roles can access withroles Controller’s action. |
3 | Anonymous users (authenticated and not authenticated users) can access anonymous Controller’s action. |
Alternatively, you could use JSR_250 annotations (javax.annotation.security.PermitAll
, javax.annotation.security.RolesAllowed
, javax.annotation.security.DenyAll
):
@Controller("/example")
public class ExampleController {
@Produces(MediaType.TEXT_PLAIN)
@Get("/admin")
@RolesAllowed({"ROLE_ADMIN", "ROLE_X"}) (1)
public String withroles() {
return "You have ROLE_ADMIN or ROLE_X roles";
}
@Produces(MediaType.TEXT_PLAIN)
@Get("/anonymous")
@PermitAll (2)
public String anonymous() {
return "You are anonymous";
}
}
1 | Users granted role ROLE_ADMIN or ROLE_X roles can access withroles Controller’s action. |
2 | Anonymous users (authenticated and not authenticated users) can access anonymous Controller’s action. |
4.3 Intercept URL Map
Moreover, you can configure endpoint authentication and authorization access with an Intercept URL Map:
micronaut:
security:
enabled: true
intercept-url-map:
-
pattern: /images/*
http-method: GET
access:
- isAnonymous() (1)
-
pattern: /books
access:
- isAuthenticated() (2)
-
pattern: /books/grails
http-method: GET
access:
- ROLE_GRAILS (3)
- ROLE_GROOVY
1 | Enable access to authenticated and not authenticated users |
2 | Enable access for everyone authenticated |
3 | Enable access for users who are granted any of the specified roles. |
As you see in the previous code listing, any endpoint is identified by a combination of pattern and an optional HTTP method.
If a given request URI matches more than one intercept url map, the one that specifies an http method that matches the request method will be used. If there are multiple mappings that do not specify a method and match the request URI, then the first mapping will be used. For example:
The example below defines that all HTTP requests to URIs matching the pattern /v1/myResource/**
and using HTTP method GET
will be accessible to everyone. Requests matching the same URI pattern but using a different HTTP method than GET
require fully authenticated access.
micronaut:
security:
enabled: true
intercept-url-map:
- pattern: /v1/myResource/**
httpMethod: GET (1)
access:
- isAnonymous()
- pattern: /v1/myResource/**
access:
- isAuthenticated() (2)
1 | Accessing /v1/myResource/** with a GET request does not require authentication. |
2 | Accessing /v1/myResource/** with a request that isn’t GET requires authentication. |
4.4 Built-In Endpoints Security
When you turn on security, Built-in endpoints are secured depending on their sensitive value.
endpoints:
beans:
enabled: true
sensitive: true (1)
info:
enabled: true
sensitive: false (2)
1 | /beans endpoint is secured |
2 | /info endpoint is open for unauthenticated access. |
5 Authentication Strategies
5.1 Basic Auth
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.
Once you enable Micronaut security, Basic Auth is enabled by default.
micronaut:
security:
enabled: true
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')
The following configuration properties are available to customize basic authentication behaviour:
Property | Type | Description |
---|---|---|
|
boolean |
Enables BasicAuthTokenReader. Default value true. |
|
java.lang.String |
Http Header name. Default value {@value io.micronaut.http.HttpHeaders#AUTHORIZATION}. |
|
java.lang.String |
Http Header value prefix. Default value {@value io.micronaut.http.HttpHeaderValues#AUTHORIZATION_PREFIX_BASIC}. |
Read the Basic Authentication Micronaut Guide to learn more. |
5.2 Session Authentication
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 in build.gradle
:
annotationProcessor "io.micronaut:micronaut-security"
compile "io.micronaut:micronaut-security-session"
The following sequence illustrates the authentication flow:
The following configuration properties are available to customize session based authentication behaviour:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Sets the login success target URL. Default value ("/"). |
|
java.lang.String |
Sets the login failure target URL. Default value ("/"). |
|
java.lang.String |
Sets the logout target URL. Default value ("/"). |
|
java.lang.String |
Sets the unauthorized target URL. |
|
java.lang.String |
Sets the forbidden target URL. |
|
boolean |
Sets whether the session config is enabled. Default value (false). |
|
boolean |
Decides whether the deprecated {@link SessionSecurityFilterOrderProvider} is loaded, instead of the new RedirectRejectionHandler. Defaults to (true). |
Example of Session-Based Authentication configuration
micronaut:
security:
enabled: true
endpoints:
login:
enabled: true
logout:
enabled: true
session:
enabled: true
login-failure-target-url: /login/authFailed
Read the Session-Based Authentication Micronaut Guide to learn more. |
5.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 |
Name of the roles property. Default value "roles". |
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 in build.gradle
:
annotationProcessor "io.micronaut:micronaut-security"
compile "io.micronaut:micronaut-security-jwt"
The following configuration properties are available to customize JWT based authentication behaviour:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether JWT security is enabled. Default value (false). |
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.
5.3.1 Reading JWT Token
5.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 header name to use. Default value {@value io.micronaut.http.HttpHeaders#AUTHORIZATION}. |
|
java.lang.String |
Sets the prefix to use for the auth token. Default value {@value io.micronaut.http.HttpHeaderValues#AUTHORIZATION_PREFIX_BEARER}. |
Sending tokens in the request
The code snippet below illustrates how to send a JWT token in the Authorization
request header, using the bearerAuth
method from MutableHttpRequest method:
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 guide for a tutorial on Micronaut’s JWT support. |
5.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 |
Sets the domain name of this Cookie. |
|
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.lang.Boolean |
Sets whether the cookie is secured. Default value (true. |
|
java.time.Duration |
Sets the maximum age of the cookie. |
|
boolean |
Sets whether JWT cookie based security is enabled. Default value (false). |
|
java.lang.String |
Sets the logout target URL. Default value ("/"). |
|
java.lang.String |
Cookie Name. Default value ("JWT"). |
|
java.lang.String |
Sets the login success target URL. Default value ("/"). |
|
java.lang.String |
Sets the login failure target URL. Default value ("/"). |
Read the Micronaut JWT Authentication with Cookies to learn more. |
5.3.2 JWT Token Generation
Micronaut relies on Nimbus JOSE + JWT library to provide JWT token signature and encryption.
The following configuration options are available:
Property | Type | Description |
---|---|---|
|
java.lang.Integer |
Refresh token expiration. By default refresh tokens, do not expire. |
|
java.lang.Integer |
Access token expiration. Default value (3600). |
5.3.2.1 JWT Signature
Micronaut security capabilities use signed JWT’s as specified by the JSON Web Signature specification.
To enable a JWT signature in token generation, you need to have in your app a bean of type SecretSignatureConfiguration
qualified with name generator
.
To verify signed JWT tokens, you need to have in your app a bean of type RSASignatureConfiguration, RSASignatureGeneratorConfiguration, ECSignatureGeneratorConfiguration, ECSignatureConfiguration, or SecretSignature.
You can setup a SecretSignatureConfiguration
named generator
easily via configuration properties:
micronaut:
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: pleaseChangeThisSecretForANewOne (1)
jws-algorithm: HS256 (2)
1 | Change this for your own secret and keep it safe. |
2 | Json Web Token Signature name. In this example, HMAC using SHA-256 hash algorithm. |
You can supply the secret with Base64 encoding.
micronaut:
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: 'cGxlYXNlQ2hhbmdlVGhpc1NlY3JldEZvckFOZXdPbmU=' (1)
base64: true (2)
jws-algorithm: HS256
1 | Secret Base64 encoded |
2 | Signal that the secret is Base64 encoded |
A programmatic setup of a RSA signature generation may look like
@Factory
class MySignatureGeneratorConfigurationFactory {
@Bean
@Named("generator") (1)
SignatureGeneratorConfiguration signatureGeneratorConfiguration(RSASignatureGeneratorConfiguration configuration) {(2)
return new RSASignatureGenerator(configuration)
}
}
1 | Name the SignatureGeneratorConfiguration generator to make it participate in JWT token generation. |
2 | Register an additional bean of type RSASignatureGeneratorConfiguration which is injected here |
5.3.2.2 Encrypted JWTs
Signed claims prevents an attacker to tamper its contents to introduce malicious data or try a privilege escalation by adding more roles. However, the claims can be decoded just by using Base 64.
If the claims contains sensitive information, you can use a JSON Web Encryption algorithm to prevent them to be decoded.
To enable a JWT encryption in token generation, you need to have in your app a bean of type RSAEncryptionConfiguration,
ECEncryptionConfiguration,
SecretEncryptionConfiguration qualified with name generator
.
Example of JWT Signed with Secret and Encrypted with RSA
Setup a SecretSignatureConfiguration through configuration properties
micronaut:
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: pleaseChangeThisSecretForANewOne (1)
jws-algorithm: HS256 (2)
pem:
path: /home/user/rsa-2048bit-key-pair.pem (2)
1 | Name the Signature configuration generator to make it participate in JWT token generation. |
2 | Location of PEM file |
Generate a 2048-bit RSA private + public key pair:
openssl genrsa -out rsa-2048bit-key-pair.pem 2048
@Named("generator") (1)
@Singleton
class RSAOAEPEncryptionConfiguration implements RSAEncryptionConfiguration {
private RSAPrivateKey rsaPrivateKey
private RSAPublicKey rsaPublicKey
JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256
EncryptionMethod encryptionMethod = EncryptionMethod.A128GCM
RSAOAEPEncryptionConfiguration(@Value('${pem.path}') String pemPath) {
Optional<KeyPair> keyPair = KeyPairProvider.keyPair(pemPath)
if (keyPair.isPresent()) {
this.rsaPublicKey = (RSAPublicKey) keyPair.get().getPublic()
this.rsaPrivateKey = (RSAPrivateKey) keyPair.get().getPrivate()
}
}
@Override
RSAPublicKey getPublicKey() {
return rsaPublicKey
}
@Override
RSAPrivateKey getPrivateKey() {
return rsaPrivateKey
}
@Override
JWEAlgorithm getJweAlgorithm() {
return jweAlgorithm
}
@Override
EncryptionMethod getEncryptionMethod() {
return encryptionMethod
}
}
1 | Name Bean generator to designate this bean as participant in the JWT Token Generation. |
To parse the PEM key, use a collaborator as described in OpenSSL key generation.
@Slf4j
class KeyPairProvider {
/**
*
* @param pemPath Full path to PEM file.
* @return returns KeyPair if successfully for PEM files.
*/
static Optional<KeyPair> keyPair(String pemPath) {
// Load BouncyCastle as JCA provider
Security.addProvider(new BouncyCastleProvider())
// Parse the EC key pair
PEMParser pemParser
try {
pemParser = new PEMParser(new InputStreamReader(Files.newInputStream(Paths.get(pemPath))))
PEMKeyPair pemKeyPair = (PEMKeyPair) pemParser.readObject()
// Convert to Java (JCA) format
JcaPEMKeyConverter converter = new JcaPEMKeyConverter()
KeyPair keyPair = converter.getKeyPair(pemKeyPair)
pemParser.close()
return Optional.of(keyPair)
} catch (FileNotFoundException e) {
log.warn("file not found: {}", pemPath)
} catch (PEMException e) {
log.warn("PEMException {}", e.getMessage())
} catch (IOException e) {
log.warn("IOException {}", e.getMessage())
}
return Optional.empty()
}
}
5.3.3 JWT Token Validation
Micronaut’s JWT validation support multiple Signature and Encryption configurations.
Any beans of type RSASignatureConfiguration, ECSignatureConfiguration, SecretSignatureConfiguration participate as signature configurations in the JWT validation.
Any beans of type RSAEncryptionConfiguration, ECEncryptionConfiguration, SecretEncryptionConfiguration participate as encryption configurations in the JWT validation.
5.3.3.1 Validation with remote JWKS
A JSON Web Key (JWK) is a JSON object that represents a cryptographic key. You can use a remote JWK Set, A JSON object that represents a set of JWKs, to validate JWT signatures.
You can configure a remote JWKS as a signature validator:
micronaut:
security:
enabled: true
token:
jwt:
enabled: true
signatures:
jwks:
awscognito:
url: 'https://cognito-idp.eu-west-1.amazonaws.com/eu-west-XXXX/.well-known/jwks.json'
The previous snippet creates a JwksSignature bean with a awscognito
name qualifier.
If you want to expose your own JWK Set, read the Keys Controller section.
5.3.4 Claims Generation
If the built-in JWTClaimsSetGenerator, does not fulfil your needs you can provide your own replacement of ClaimsGenerator.
For example, if you want to add the email address of the user to the JWT Claims you could create a class which extends UserDetails
:
public class EmailUserDetails extends UserDetails {
private String email;
public EmailUserDetails(String username, Collection<String> roles) {
super(username, roles);
}
public EmailUserDetails(String username, Collection<String> roles, String email) {
super(username, roles);
this.email = email;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Configure your AuthenticationProvider
to respond such a class:
@Singleton
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) {
return Flowable.just(new EmailUserDetails("sherlock", Collections.emptyList(), "sherlock@micronaut.example"));
}
}
And then replace JWTClaimsSetGenerator
with a bean that overrides the method populateWithUserDetails
:
@Singleton
@Replaces(bean = JWTClaimsSetGenerator.class)
public class CustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator {
public CustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration,
@Nullable JwtIdGenerator jwtIdGenerator,
@Nullable ClaimsAudienceProvider claimsAudienceProvider,
@Nullable ApplicationConfiguration applicationConfiguration) {
super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider, applicationConfiguration);
}
@Override
protected void populateWithUserDetails(JWTClaimsSet.Builder builder, UserDetails userDetails) {
super.populateWithUserDetails(builder, userDetails);
if (userDetails instanceof EmailUserDetails) {
builder.claim("email", ((EmailUserDetails)userDetails).getEmail());
}
}
}
5.3.5 Token Render
When you use JWT 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.
5.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.configuration:micronaut-security-ldap"
Read the LDAP and Database authentication providers to see an example. |
LDAP authentication can be globally disabled by setting micronaut.security.ldap.enabled
to false
, or on a provider
basis, eg micronaut.security.ldap.default.enabled: false
.
5.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 |
Sets the server URL. |
|
java.lang.String |
Sets the manager DN. |
|
java.lang.String |
Sets the manager password. |
|
java.lang.String |
Sets the context factory class. Default "com.sun.jndi.ldap.LdapCtxFactory" |
|
java.util.Map |
Any additional properties that should be passed to {@link javax.naming.directory.InitialDirContext#InitialDirContext(java.util.Hashtable)}. |
Property | Type | Description |
---|---|---|
|
boolean |
Sets if the subtree should be searched. Default true |
|
java.lang.String |
Sets the base DN to search. |
|
java.lang.String |
Sets the search filter. Default "(uid={0})" |
|
java.lang.String[] |
Sets the attributes to return. Default all |
Property | Type | Description |
---|---|---|
|
boolean |
Sets if group search is enabled. Default false |
|
boolean |
Sets if the subtree should be searched. Default true |
|
java.lang.String |
Sets the base DN to search from. |
|
java.lang.String |
Sets the group search filter. Default "uniquemember={0}" |
|
java.lang.String |
Sets the group attribute name. Default "cn" |
5.4.2 Extending Default Behavior
This section will outline some common requirements that will require custom code to implement and describe what to do in those cases.
Authentication Data
The authentication object returned from a successful authentication request is by default an instance of UserDetails, which only contains the username and any roles associated with the user. To store additional data in the authentication, extend UserDetails with your own implementation that has fields for the additional data you wish to store.
To use this new implementation, you must override the DefaultContextAuthenticationMapper and provide your own implementation.
@Singleton
@Replaces(DefaultContextAuthenticationMapper.class) (1)
public class MyContextAuthenticationMapper implements ContextAuthenticationMapper {
@Override
public AuthenticationResponse map(ConvertibleValues<Object> attributes, String username, Set<String> groups) {
// return an extended UserDetails or an AuthenticationFailed object
}
}
1 | The usage of @Replaces will allow your bean to replace the default implementation in the context |
Groups
By default the groups found in LDAP, if enabled, will be returned as is without any processing. No additional groups from any other sources will be added to the list. It is a common requirement to retrieve additional groups from other sources, or to normalize the names of the groups in a specific format.
To extend this behavior, it is necessary to create your own implementation of LdapGroupProcessor. Likely it will be desired to extend the default implementation because it has the logic for querying the groups from LDAP and executes the other methods to process the groups and query for additional groups.
@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 {
}
6 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.
To control this response, it is necessary to register a RejectionHandler and ensure your bean replaces the default implementation.
If both beans (UnauthorizedRejectionUriProvider, ForbiddenRejectionUriProvider) exists, then RedirectRejectionHandler,
a generic redirect handler, is registered as a RejectionHandler
.
You can create your own RejectionHandler
. You will need to replace the current RejectionHandler
.
For those using JWT based security, replace HttpStatusCodeRejectionHandler.
For example:
@Singleton
@Replaces(HttpStatusCodeRejectionHandler.class)
public class MyRejectionHandler extends HttpStatusCodeRejectionHandler {
@Override
public Publisher<MutableHttpResponse<?>> reject(HttpRequest<?> request, boolean forbidden) {
//Let the HttpStatusCodeRejectionHandler create the initial request
//then add a header
return Flowable.fromPublisher(super.reject(request, forbidden))
.map(response -> response.header("X-Reason", "Example Header"));
}
}
For those using session based security, replace SessionSecurityfilterRejectionHandler.
If you are using session based security to use the RedirectRejectionHandler you will need to disable SessionSecurityfilterRejectionHandler with micronaut.security.session.legacy-redirect-handler: false .
|
7 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
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: "pleaseChangeThisSecretForANewOne"
jws-algorithm: HS256
writer:
header:
enabled: true
headerName: "Authorization"
prefix: "Bearer "
propagation:
enabled: true
service-id-regex: "http://localhost:(8083|8081|8082)"
The previous configuration, configures a HttpHeaderTokenWriter 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"
Read the Token Propagation tutorial to learn more. |
8 Built-In Security Controllers
8.1 Login Controller
You can enable LoginController
with configuration property:
Property | Type | Description |
---|---|---|
|
boolean |
Enables LoginController. Default value false |
|
java.lang.String |
Path to the LoginController. Default value "/login" |
The response of the Login Endpoint is handled by a bean instance of LoginHandler.
Login Endpoint invocation example
curl -X "POST" "http://localhost:8080/login" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"username": "euler",
"password": "password"
}'
8.2 Logout Controller
You can enable the logout endpoint with configuration:
Property | Type | Description |
---|---|---|
|
boolean |
Enables LogoutController. Default value false. |
|
java.lang.String |
Path to the LogoutController. Default value "/logout". |
|
boolean |
Enables HTTP GET invocations of LogoutController. Default value (false). |
The behavior of the controller is delegated to a LogoutHandler implementation. There are default implementations provided for JWT Cookie and Session based authentication storage.
If you are using JWT authentication not stored in a cookie, you may not need to invoke the /logout endpoint. Since logging out normally means simply deleting the JWT token in the client.
|
Logout Endpoint invocation example
curl -X "POST" "http://localhost:8080/logout"
8.3 Refresh Controller
This controller can only be enabled if you are using JWT authentication. |
By default, issued access tokens expire after a period of time, and they are paired with refresh tokens. To ease the refresh, you can enable OauthController, with configuration property:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether the OauthController is enabled. Default value (false). |
|
java.lang.String |
Sets the path to map the OauthController to. Default value ("/oauth/access_token"). |
The controller exposes an endpoint as defined by section 6 of the OAuth 2.0 spec - Refreshing an Access Token.
By default, issued Refresh tokens never expire, and can be used to obtain a new access token by sending a POST request to the /oauth/access_token
endpoint:
POST /myApp/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.
By default refresh tokens never expire, they must be securely stored in your client application. See section 10.4 of the OAuth 2.0 spec for more information. |
8.4 Keys Controller
This controller can only be enabled if you are using JWT authentication. |
A JSON Web Key (JWK) is a JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value.
Meanwhile, a JWK Set is a JSON object that represents a set of JWKs. The JSON object MUST have a "keys" member, which is an array of JWKs.
You can enable the KeysController to expose an endpoint which returns a JWK Set. You can configure it with:
Property | Type | Description |
---|---|---|
|
boolean |
Enables KeysController. Default value false. |
|
java.lang.String |
Path to the KeysController. Default value "/keys". |
Moreover, you will need to provide beans to type JwkProvider.
9 Retrieve the authenticated user
Often you may want to retrieve the authenticated user.
You can bind java.security.Principal
as a method’s parameter in a controller.
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import javax.annotation.Nullable;
import java.security.Principal;
@Controller("/user")
public class UserController {
@Secured("isAnonymous()")
@Get("/myinfo")
public Map myinfo(@Nullable Principal principal) {
if (principal == null) {
return Collections.singletonMap("isLoggedIn", false);
}
return CollectionUtils.mapOf("isLoggedIn", true, "username", principal.getName());
}
}
If you need a greater level of detail, you can bind Authentication as a method’s parameter in a controller.
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import javax.annotation.Nullable
@Controller("/user")
public class UserController {
@Secured("isAnonymous()")
@Get("/myinfo")
public Map myinfo(@Nullable Authentication authentication) {
if (authentication == null) {
return Collections.singletonMap("isLoggedIn", false);
}
return CollectionUtils.mapOf("isLoggedIn", true,
"username", authentication.getName(),
"roles", authentication.getAttributes().get("roles")
);
}
}
9.1 User outside of a controller
If you need to access the currently authenticated user outside of a controller, you can inject SecurityService bean, which provides a set of convenient methods related to authentication and authorization.
10 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.
11 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:
enabled: true
oauth2:
enabled: true
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 RedirectingLoginHandler 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 the following options are available out of the box.
Type |
Configuration Required |
Module |
JWT Cookie Storage |
|
|
Session Storage |
|
|
11.1 Installation
implementation("io.micronaut.configuration:micronaut-security-oauth2:1.3.1")
<dependency>
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-security-oauth2</artifactId>
<version>1.3.1</version>
</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.
11.2 OpenID Connect
OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
If you are new to OpenID Connect, we recommend watching OAuth 2.0 and OpenID Connect to get a better understanding.
To use OpenID client flows, the security-jwt dependency must be in your build because OpenID relies on JWT tokens.
|
implementation("io.micronaut:micronaut-security-jwt")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-security-jwt</artifactId>
</dependency>
Even though OpenID providers return JWT tokens, that token is not used directly to authorize requests with Micronaut. Instead a new token is created if the application is using JWT. This allows for authorization to be standardized across custom authentication providers, normal OAuth 2.0 flows, and OpenID flows. It also allows for purely session based authorization as a result of OpenID authentication. |
11.3 Flows
11.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 (false). |
|
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 URI template that is used to initiate an OAuth 2.0 authorization code grant flow. Default value ("/oauth/login{/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.
|
11.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.
11.3.1.1.1 Configuration
The minimum requirements to allow authorization with an OAuth 2.0 provider are:
-
Configuration of the authorization endpoint
-
Configuration of the token endpoint
-
Configuration of the client id and secret
-
An implementation of OauthUserDetailsMapper
Configuration is quite simple. For example to configure authorization with Github:
micronaut:
security:
enabled: true
oauth2:
enabled: true
clients:
github: (1)
client-id: <<my client id>> (2)
client-secret: <<my client secret>> (3)
scopes: (4)
- user:email
- read:user
authorization:
url: https://github.com/login/oauth/authorize (5)
token:
url: https://github.com/login/oauth/access_token (6)
auth-method: client-secret-post (7)
1 | Configure a client. The name here is arbitrary |
2 | The client id |
3 | The client secret |
4 | The desired scopes (OPTIONAL) |
5 | The authorization endpoint URL |
6 | The token endpoint URL |
7 | The token endpoint authentication method. One of AuthenticationMethod. Choose the one your provider requires. |
Authentication methods are not clearly defined in RFC 6749, however most OAuth 2.0 providers either accept client-secret-basic
(basic authentication with id and secret), or client-secret-post
(client id and secret are sent in the request body).
To disable a specific client for any given environment, set enabled: false within the client configuration.
|
11.3.1.1.2 User Details Mapper
Beyond configuration, an implementation of OauthUserDetailsMapper is required by the user to be implemented. The implementation must be qualified by name that matches the name present in the client configuration.
The purpose of the user details mapper is to transform the TokenResponse into a UserDetails. That will entail calling some endpoint the provider exposes to retrieve the user’s information. Once that information is received, the user details can be populated per your requirements.
Common requirements of a user details mapper may be to combine data from the OAuth 2.0 provider with data from a remote database and/or create new user records. The UserDetails object stores three basic properties: username
, roles
, and arbitrary attributes
. All data stored in the user details will be retrievable in controllers that accept an Authentication.
For example, here is how it might be implemented for Github.
Create a class to store the response data:
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.micronaut.core.annotation.Introspected;
@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GithubUser {
private String login;
private String name;
private String email;
// getters and setters ...
}
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.micronaut.core.annotation.Introspected
@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class GithubUser {
String login
String name
String email
}
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.micronaut.core.annotation.Introspected
@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
class GithubUser {
var login: String? = null
var name: String? = null
var email: String? = null
}
Create an HTTP client to make the request:
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;
@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
public interface GithubApiClient {
@Get("/user")
Flowable<GithubUser> getUser(@Header("Authorization") String authorization);
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Header
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
interface GithubApiClient {
@Get("/user")
Flowable<GithubUser> getUser(@Header("Authorization") String authorization)
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Header
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
interface GithubApiClient {
@Get("/user")
fun getUser(@Header("Authorization") authorization: String): Flowable<GithubUser>
}
Create the user details mapper that pulls it together:
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper;
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse;
import org.reactivestreams.Publisher;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;
@Named("github") (1)
@Singleton
class GithubUserDetailsMapper implements OauthUserDetailsMapper {
private final GithubApiClient apiClient;
GithubUserDetailsMapper(GithubApiClient apiClient) {
this.apiClient = apiClient;
} (2)
@Override
public Publisher<UserDetails> createUserDetails(TokenResponse tokenResponse) { (3)
return apiClient.getUser("token " + tokenResponse.getAccessToken())
.map(user -> {
List<String> roles = Collections.singletonList("ROLE_GITHUB");
return new UserDetails(user.getLogin(), roles); (4)
});
}
}
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse
import org.reactivestreams.Publisher
import javax.inject.Named
import javax.inject.Singleton
@Named("github") (1)
@Singleton
class GithubUserDetailsMapper implements OauthUserDetailsMapper {
private final GithubApiClient apiClient
GithubUserDetailsMapper(GithubApiClient apiClient) { (2)
this.apiClient = apiClient
}
@Override
Publisher<UserDetails> createUserDetails(TokenResponse tokenResponse) { (3)
apiClient.getUser("token ${tokenResponse.accessToken}")
.map({ user ->
new UserDetails(user.login, ["ROLE_GITHUB"]) (4)
})
}
}
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse
import org.reactivestreams.Publisher
import javax.inject.Named
import javax.inject.Singleton
@Named("github") (1)
@Singleton
internal class GithubUserDetailsMapper(private val apiClient: GithubApiClient) (2)
: OauthUserDetailsMapper {
override fun createUserDetails(tokenResponse: TokenResponse): Publisher<UserDetails> { (3)
return apiClient.getUser("token " + tokenResponse.accessToken)
.map { user ->
UserDetails(user.login, listOf("ROLE_GITHUB")) (4)
}
}
}
1 | The bean must have a named qualifier that matches the name in configuration. |
2 | How the request is made to retrieve the user information is totally up to you, however in this example we’re using a declarative client. |
3 | The token endpoint response is passed to the method. |
4 | The user information is converted to a UserDetails. |
11.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.
11.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:
enabled: true
oauth2:
enabled: true
clients:
okta: (1)
client-id: <<my client id>> (2)
client-secret: <<my client secret>> (3)
openid:
issuer: <<my openid issuer>> (4)
1 | Configure a client. The name here is arbitrary |
2 | The client id |
3 | The client secret |
4 | The OpenID provider issuer |
The issuer URL will be used to discover the endpoints exposed by the provider.
To disable a specific client for any given environment, set enabled: false within the client configuration.
|
See the following tables for the configuration options:
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 |
---|---|---|
|
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 |
The endpoint URL |
Property | Type | Description |
---|---|---|
|
The content type of token endpoint requests. Default value (application/x-www-form-urlencoded). |
|
|
java.lang.String |
The endpoint URL |
|
Authentication Method |
Property | Type | Description |
---|---|---|
|
boolean |
The end session enabled flag. Default value (true). |
|
java.lang.String |
The endpoint URL |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
Property | Type | Description |
---|---|---|
|
java.lang.String |
The endpoint URL |
11.3.1.2.2 User Details Mapper
Because the OpenID standard returns a JWT token in the token response, it is possible to retrieve information about the user without having to make an additional call. In addition, the data stored in the JWT is standardized so you can use the same code to retrieve that information across providers.
A default implementation of OpenIdUserDetailsMapper has been provided for you to map the JWT token to a UserDetails. The default implementation will carry over any of the specific OpenID JWT claims, as well as potentially include other claims based on configuration. The original provider name will always be included in the JWT with the claim key "oauth2Provider". The following table explains the additional claims.
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.
To override the global default mapper, register a bean that replaces DefaultOpenIdUserDetailsMapper.
import io.micronaut.context.annotation.Replaces;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper;
import javax.annotation.Nonnull;
import javax.inject.Singleton;
import java.util.Collections;
@Singleton
@Replaces(DefaultOpenIdUserDetailsMapper.class)
public class GlobalOpenIdUserDetailsMapper implements OpenIdUserDetailsMapper {
@Override
@Nonnull
public UserDetails createUserDetails(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims) {
return new UserDetails("name", Collections.emptyList());
}
}
import io.micronaut.context.annotation.Replaces
import io.micronaut.context.annotation.Requires
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper
import javax.annotation.Nonnull
import javax.inject.Singleton
@Singleton
@Replaces(DefaultOpenIdUserDetailsMapper.class)
class GlobalOpenIdUserDetailsMapper implements OpenIdUserDetailsMapper {
@Override
@Nonnull
UserDetails createUserDetails(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims) {
new UserDetails("name", [])
}
}
import io.micronaut.context.annotation.Replaces
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdUserDetailsMapper
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper
import javax.inject.Singleton
@Singleton
@Replaces(DefaultOpenIdUserDetailsMapper::class)
class GlobalOpenIdUserDetailsMapper : OpenIdUserDetailsMapper {
override fun createUserDetails(providerName: String, tokenResponse: OpenIdTokenResponse, openIdClaims: OpenIdClaims): UserDetails {
return UserDetails("name", emptyList())
}
}
To override the user detail mapping behavior for a specific provider, register a bean with a named qualifier with a value equal to the name specified in the client configuration.
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper;
import javax.annotation.Nonnull;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;
@Singleton
@Named("okta") (1)
public class OktaUserDetailsMapper implements OpenIdUserDetailsMapper {
@Override
@Nonnull
public UserDetails createUserDetails(String providerName, (2)
OpenIdTokenResponse tokenResponse, (3)
OpenIdClaims openIdClaims) { (4)
return new UserDetails("name", Collections.emptyList()); (5)
}
}
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper
import javax.annotation.Nonnull
import javax.inject.Named
import javax.inject.Singleton
@Singleton
@Named("okta") (1)
class OktaUserDetailsMapper implements OpenIdUserDetailsMapper {
@Override
@Nonnull
UserDetails createUserDetails(String providerName, (2)
OpenIdTokenResponse tokenResponse, (3)
OpenIdClaims openIdClaims) { (4)
new UserDetails("name", []) (5)
}
}
import io.micronaut.security.authentication.UserDetails
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdUserDetailsMapper
import javax.inject.Named
import javax.inject.Singleton
@Singleton
@Named("okta") (1)
class OktaUserDetailsMapper : OpenIdUserDetailsMapper {
override fun createUserDetails(providerName: String, (2)
tokenResponse: OpenIdTokenResponse, (3)
openIdClaims: OpenIdClaims) (4)
: UserDetails {
return UserDetails("name", emptyList()) (5)
}
}
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 | An instance of UserDetails is returned |
11.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 |
---|---|---|
|
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 |
The endpoint URL |
11.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. The default implementation stores the nonce 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.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.lang.Boolean |
Sets whether the cookie is secured. Default value (true). |
|
java.time.Duration |
Sets the maximum age of the cookie. Default value (5 minutes). |
|
java.lang.String |
Cookie Name. Default value ("OPENID_NONCE"). |
You can provide your own implementation, however an implementation of nonce persistence that stores the nonce 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:micronaut-session")
<dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-session</artifactId> </dependency>
-
Set the nonce persistence to
session
application.ymlmicronaut.security.oauth2.nonce.persistence: session
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 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.
11.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.
11.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.
11.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.
11.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.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.lang.Boolean |
Sets whether the cookie is secured. Default value (true). |
|
java.time.Duration |
Sets the maximum age of the cookie. Default value (5 minutes). |
|
java.lang.String |
Cookie Name. Default value ("OAUTH2_STATE"). |
You can provide your own implementation, however an implementation of state persistence that stores the state in an http session has also been provided.
To enable state persistence with an http session:
-
Add a dependency to
micronaut-session
implementation("io.micronaut:micronaut-session")
<dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-session</artifactId> </dependency>
-
Set the state persistence to
session
application.ymlmicronaut.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.
11.3.2 Password
The resource owner password credentials grant is described in RFC 6749. In short, credentials are passed directly to the token endpoint and if authentication succeeds, the token endpoint responds with the appropriate token.
The process that handles the token response onward is the same for both authorization code and password grants. See the following high level flow diagrams:
OAuth 2.0 Provider
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
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.
11.4 Endpoints
11.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 |
---|---|---|
|
boolean |
The end session enabled flag. Default value (true). |
|
java.lang.String |
The endpoint URL |
11.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 |
|
Authentication Method |
11.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 |
|
Authentication Method |
11.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 |
11.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<HttpResponse> 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).