Secure a Micronaut app with Okta

Learn how to create Micronaut app and secure it with an Authorization Server provided by Okta.

Authors: Sergio del Amo

Micronaut Version: 1.1.0.M1

1 Getting Started

In this guide we are going to create a Micronaut app written in Java.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

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

or

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

2 Using Snapshots

In this guide, we use some features which are currently only available in the BUILD-SNAPSHOT version of Micronaut.

Please, read Using snapshots section of the Micronaut documentation.

3 Writing the App

Create an app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete

The previous command creates a micronaut app with the default package example.micronaut in a folder named complete.

By default, create-app generates a Java Micronaut app and it uses Gradle build system. However, you could use other build tool such as Maven or other programming languages such as Groovy or Kotlin.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

3.1 Views

Although Micronaut is primarily designed around message encoding / decoding, there are occasions where it is convenient to render a view on the server side.

To use the view rendering features, add the following dependency on your classpath. For example, in build.gradle

build.gradle
dependencies {
  ...
  ..
    compile "io.micronaut:micronaut-views"
}

To use Thymeleaf Java template engine, add the thymeleaf dependency:

build.gradle
dependencies {
  ...
  ..
    runtime "org.thymeleaf:thymeleaf:3.0.11.RELEASE"
}

3.2 OAuth 2.0

Sign up at developer.okta.com and create a Web app with the following characteristics:

okta app

To use OAuth 2.0 integration, add the next dependency:

build.gradle
dependencies {
  ...
  ..
    compile "io.micronaut.configuration:micronaut-oauth2:1.0.0.BUILD-SNAPSHOT"
}

Add also JWT Micronaut’s JWT support dependencies:

build.gradle
dependencies {
  ...
  ..
    annotationProcessor "io.micronaut:micronaut-security"
    compile "io.micronaut:micronaut-security-jwt"
}

Add the following Oauth2 Configuration:

src/main/resources/application.yml
    security:
        enabled: true (1)
        oauth2:
            client-secret: '${OAUTH_CLIENT_SECRET}' (2)
            client-id: '${OAUTH_CLIENT_ID}' (3)
            issuer: '${OIDC_ISSUER_DOMAIN}/oauth2/${OIDC_ISSUER_AUTHSERVERID}' (4)
            authorization:
                scopes: (5)
                    - 'openid'
                    - 'email'
        token:
            jwt:
                enabled: true (6)
                cookie:
                   enabled: true (7)
        endpoints:
            logout:
                enabled: true (8)
                get-allowed: true (9)
1 Enable security
2 Client Secret. See previous screenshot.
3 Client ID. See previous screenshot.
4 issuer url. It allows micronaut to discover the configuration of the OpenID Connect server.
5 By default openid scope isued. You can supply multiple scopes by providing a list.
6 ID Token is a JWT token. We need to enable Micronaut’s JWT support to validate it.
7 Once validated, we are going to save the ID Token in a Cookie. To read in subsequent requests, enable Cookie Token Reader.
8 Enable Logout Controller
9 Accept GET request to the /logout endpoint.

The previous configuration uses several placeholders. You will need to setup OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OIDC_ISSUER_DOMAIN and OIDC_ISSUER_AUTHSERVERID environment variables.

export OAUTH_CLIENT_ID=XXXXXXXXXX
export OAUTH_CLIENT_SECRET=YYYYYYYYYY
export OIDC_ISSUER_DOMAIN=https://dev-XXXXX.oktapreview.com
export OIDC_ISSUER_AUTHSERVERID=default

We want to use an Authorization Code grant type flow which it is described in the following diagram:

diagramm

3.3 Auth State Parameter

Micronaut seamlessly configures the request towards the authorization endpoint of the Authorization server.

However, we need to provide an authentication state parameter:

Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie.

To do that, we need to create a State Provider bean.

src/main/java/example/micronaut/DefaultStateProvider.java
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.security.oauth2.openid.endpoints.authorization.StateProvider;
import javax.annotation.Nonnull;
import javax.inject.Singleton;

@Singleton (1)
public class DefaultStateProvider implements StateProvider {
    @Nonnull
    @Override
    public String generateState(@Nonnull HttpRequest<?> request) {
        Cookie cookie = request.getCookies().get(StateFilter.STATE_COOKIENAME);
        if (cookie != null) {
            return cookie.getValue();
        }
        throw new RuntimeException("Authorization code state parameter could not be retrieved from cookie");
    }
}
1 To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton.

The previous implementation retrieves the state from a cookie. To be sure that such cookie exits, create a Http Filter which creates a cookie with a random state value if such cookie does not exist.

The Micronaut HTTP server supports the ability to apply filters to request/response processing in a similar, but reactive, way to Servlet filters in traditional Java applications.

src/main/java/example/micronaut/StateFilter.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.filter.OncePerRequestHttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import io.micronaut.security.filters.SecurityFilterOrderProvider;
import io.micronaut.security.utils.SecurityService;
import org.reactivestreams.Publisher;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.UUID;

@Requires(beans = {
        SecurityService.class
})
@Filter("/**") (1)
public class StateFilter extends OncePerRequestHttpServerFilter { (2)

    protected final Integer order;

    private final static int ORDER_OFFSET = 100;

    public final static String STATE_COOKIENAME = "AUTHORIZATION_STATE";

    @Nonnull
    private final SecurityService securityService;

    public StateFilter(@Nonnull SecurityService securityService,
                       @Nullable SecurityFilterOrderProvider securityFilterOrderProvider) {
        this.securityService = securityService;
        this.order = securityFilterOrderProvider != null ?
                securityFilterOrderProvider.getOrder() + ORDER_OFFSET :
                ORDER_OFFSET; (3)
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    protected Publisher<MutableHttpResponse<?>> doFilterOnce(HttpRequest<?> request, ServerFilterChain chain) {
        if(securityService.isAuthenticated()) {
            return Publishers.map(chain.proceed(request), response -> {
                if (request.getCookies().contains(STATE_COOKIENAME)) { (4)
                    response.cookie(request.getCookies().get(STATE_COOKIENAME).maxAge(0));
                }
                return response;
            });

        } else {
            return Publishers.map(chain.proceed(request), response -> {
            if (!request.getCookies().contains(STATE_COOKIENAME)) { (5)
                String state = generateState();
                Cookie cookie = Cookie.of(STATE_COOKIENAME, state);
                cookie.maxAge(Integer.MAX_VALUE);
                response.cookie(cookie);
            }
                return response;
            });
        }
    }

    private String generateState() {
        return UUID.randomUUID().toString();
    }
}
1 The Filter annotation is used to define the URI patterns the filter matches
2 A filter that is only executed once per request.
3 Ensure filter is triggered after SecurityFilter.
4 If the user is authenticated and the cookie exists, remove the Auth state parameter cookie.
5 if the user is not authenticated, create a cookie for the Auth state parameter.

To validate the state received in the response, provide an implementation of StateValidator bean.

src/main/java/example/micronaut/DefaultStateValidator.java
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.security.oauth2.openid.endpoints.authorization.StateValidator;
import javax.annotation.Nonnull;
import javax.inject.Singleton;

@Singleton (1)
public class DefaultStateValidator implements StateValidator {

    @Override
    public boolean validate(@Nonnull HttpRequest<?> request, @Nonnull String state) {
        Cookie cookie = findCookie(request);
        if (cookie == null) {
            return false;
        }
        String serverState = cookie.getValue();
        return serverState.equals(state);
    }

    private Cookie findCookie(HttpRequest request) {
        return request.getCookies().get(StateFilter.STATE_COOKIENAME);
    }
}
1 To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton.

3.4 Home

Create a controller to handle the requests to /. You are going to display the email of the authenticated person if any. Annotate the controller endpoint with @View since we are going to use a Thymeleaf template.

src/main/java/example/micronaut/HomeController.java
package example.micronaut;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import io.micronaut.views.View;

import java.util.HashMap;
import java.util.Map;

@Controller (1)
public class HomeController {

    @Secured(SecurityRule.IS_ANONYMOUS) (2)
    @View("home") (3)
    @Get (4)
    public Map<String, Object> index() {
        return new HashMap<>();
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /.
2 Annotate with io.micronaut.security.Secured to configure secured access. The SecurityRule.IS_ANONYMOUS expression will allow access without authentication.
3 Use View annotation to specify which template would you like to render the response against.
4 The @Get annotation is used to map the index method to GET / requests.

Create a thymeleaf template:

src/main/resources/views/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Home</title>
</head>
<body>
<h1>Micronaut - Okta example</h1>

<h2 th:if="${security}">username: <span th:text="${security.attributes.get('email')}"></span></h2>
<h2 th:unless="${security}">username: Anonymous</h2>

<nav>
    <ul>
        <li th:unless="${security}"><a href="/enter">Enter</a></li>
        <li th:if="${security && endsessionurl}"><a th:href="${endsessionurl}">Logout</a></li>

    </ul>
</nav>


</body>
</html>

Please, note that we don’t even have an /enter endpoint, thus if you click it, you are accessing an unauthorized url and a redirection to the authentication endpoint of the Okta Authorization server is triggered.

Also, note that we return an empty model in the controller. However, we are accessing security and endsessionurl in the thymeleaf template.

4 Running the Application

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

video

5 Learn More

Read Micronaut OAuth 2.0 documentation and Micronaut Security documentation to learn more.