Secure an Application with Amazon Cognito

In this guide, you will create an application and secure it with an Authorization Server provided by Amazon Cognito. Amazon Cognito provides authentication, authorization, and user management for your web and mobile applications.

Prerequisites

Follow the steps below to create the application from scratch. However, you can also download the completed example:

The application ZIP file will be downloaded in your default downloads directory. Unzip it and proceed to the next steps.

A note regarding your development environment

Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit for Micronaut Extension Pack.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.

1. Create the Application

Create an application using the GDK Launcher.

  1. Open the GDK Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: aws-security-demo
    • Base Package: com.example (Default)
    • Clouds: AWS
    • Build Tool: Gradle (Groovy) or Maven
    • Language: Java (Default)
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Security
    • Features: GraalVM Native Image (Default)
    • Sample Code: Yes (Default)
  3. Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the package com.example in a directory named aws-security-demo. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.

Alternatively, use the GDK CLI as follows:

gdk create-app com.example.aws-security-demo \
 --clouds=aws \
 --services=security \
 --features=graalvm \
 --build=gradle \
 --jdk=17  \
 --lang=java

Open the micronaut-cli.yml file, you can see what features are packaged with the application:

features: [app-name, gdk-aws-cloud-app, gdk-aws-security, gdk-bom, gdk-license, graalvm, http-client, java, java-application, junit, logback, maven, maven-enforcer-plugin, micronaut-http-validation, netty-server, properties, readme, security-annotations, security-jwt, security-oauth2, serialization-jackson, shade, static-resources, views-jte]

The GDK Launcher creates a multi-module project with two subprojects: aws for Amazon Web Services, and lib for common code and configuration shared across cloud platforms. You develop the application logic in the lib subproject, and keep the Amazon Web Services-specific configurations in the aws subproject.

1.1. AuthController

The GDK Launcher created a class named AuthController to handle requests to /. It displays the email of an authenticated person, if any. The controller endpoint is annotated with a @Viewannotation that uses a JTE template The file named aws/src/main/java/com/example/AuthController.java has the following contents:

package com.example;

import io.micronaut.core.annotation.Nullable;
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 io.micronaut.views.View;

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

import static io.micronaut.security.rules.SecurityRule.IS_ANONYMOUS;
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED;

@Controller (1)
class AuthController {

    @Secured(IS_ANONYMOUS) (2)
    @View("auth") (3)
    @Get (4)
    Map<String, Object> index(@Nullable Authentication authentication) { (5)
        Map<String, Object> model = new HashMap<>();
        if (authentication != null) {
            model.put("username", authentication.getAttributes().get("email"));
        } else {
            model.put("username", "Anonymous");
        }
        return model;
    }

    @Secured(IS_AUTHENTICATED) (6)
    @Get("/secure") (7)
    Map<String, Object> secured() {
        return Collections.singletonMap("secured", true); (8)
    }
}

1 The class is defined as a controller with the @Controller annotation mapped to the path /.

2 Annotate with @Secured to configure secured access. The SecurityRule.IS_ANONYMOUS expression allows access without authentication.

3 Use the @View annotation to specify which template to use to render the response.

4 The @Get annotation maps the index method to an HTTP GET request on /.

5 Micronaut Security will inject the Authentication instance as a method parameter; by annotating with @Nullable, you can determine whether the user is authenticated or not, and populate the model map accordingly.

6 Annotate with @Secured to configure secured access. The SecurityRule.IS_AUTHENTICATED expression allows access only to authenticated users.

7 The @Get annotation maps the secured method to an HTTP GET request on /secure.

8 This method simply returns a model map that will be rendered as JSON (because there is no @View annotation).

1.2. JTE Template

The GDK Launcher created a JTE templatein a file named aws/src/main/jte/auth.jte to render the UI for the controller. It has the following contents:

@param String username
@param java.util.Map<?, ?> security

<!DOCTYPE html>
<html lang="en">
<head>
    <title>GDK - Cognito example</title>
</head>
<body>
<h1>GDK - Cognito example</h1>

<h2>username: <span>${username}</span></h2>

<nav>
    <ul>
    @if(security == null)
        <li><a href="/oauth/login/cognito">Enter</a></li>
    @else
        <li><a href="/logout">Logout</a></li>
    @endif
    </ul>
</nav>
</body>
</html>

2. Access Amazon Web Services

2.1. Create an Administrator IAM User

Use an IAM administrative user instead of using your AWS root account.

Instead of using your AWS root account, use an administrator account. If you do not have one already, see Setting up Your Cloud Accounts.

2.2. Create an OAuth 2.0 Pool and Client

  1. Create the user pool and save its id to a variable

    export COGNITO_POOL_ID=$(aws cognito-idp create-user-pool \
        --pool-name "Micronaut Guides" \
        --username-attributes "email" \
        --auto-verified-attributes "email" \
        --query "UserPool.Id" --output text)
  2. Create a client and save the output to a variable

    export OAUTH_CLIENT_ID=$(aws cognito-idp create-user-pool-client \
        --generate-secret --user-pool-id $COGNITO_POOL_ID \
        --client-name "AWS Cognito Micronaut Tutorial" \
        --callback-urls "http://localhost:8080/oauth/callback/cognito" \
        --logout-urls "http://localhost:8080/logout" \
        --supported-identity-providers COGNITO \
        --allowed-o-auth-flows "code" \
        --allowed-o-auth-scopes "phone" "email" "openid" "profile" "aws.cognito.signin.user.admin" \
        --allowed-o-auth-flows-user-pool-client \
        --query 'UserPoolClient.ClientId' --output text)
  3. Get the client’s secret and save the output to a variable:

    export OAUTH_CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \
        --user-pool-id $COGNITO_POOL_ID --client-id $OAUTH_CLIENT_ID \
        --query 'UserPoolClient.ClientSecret' --output text)
  4. Create a domain named micronaut-guides:

    export COGNITO_DOMAIN="micronaut-guides"
    aws cognito-idp create-user-pool-domain --domain $COGNITO_DOMAIN --user-pool-id $COGNITO_POOL_ID
  5. Set a variable corresponding to the region you created your cognito pool in, for example:

    export COGNITO_REGION=us-east-2

    Note: the COGNITO_REGION, COGNITO_POOL_ID, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET variables are required to be set every time before starting the application. To print out the variables use:

    echo "OAuth Client Id: $OAUTH_CLIENT_ID, OAuth Client Secret: $OAUTH_CLIENT_SECRET, Cognito Pool Id: $COGNITO_POOL_ID"

Note: You can view the created user pool in the AWS Console by opening the Amazon Cognito Service and navigating to User Pools. Open the "Micronaut Guides" user pool and click the App Integration tab to inspect that an app domain and an app client were created. The client will have the Authorization code grant OAuth Flow and email and openid OAuth Scopes.

2.3. Configure OAuth 2.0 in the Application

The initial security configuration was generated in the file named aws/src/main/resources/application.properties:

micronaut.application.name=aws
(1)
micronaut.security.authentication=idtoken
(2)
micronaut.security.endpoints.logout.get-allowed=true
(3) (4)
micronaut.security.oauth2.clients.cognito.client-id=${OAUTH_CLIENT_ID\:xxx}
(3) (5)
micronaut.security.oauth2.clients.cognito.client-secret=${OAUTH_CLIENT_SECRET\:yyy}
(6)
micronaut.security.oauth2.clients.cognito.openid.issuer=https\://cognito-idp.${COGNITO_REGION\:zzz}.amazonaws.com/${COGNITO_POOL_ID\:www}
micronaut.security.token.jwt.signatures.secret.generator.secret=${JWT_GENERATOR_SIGNATURE_SECRET\:pleaseChangeThisSecretForANewOne}

1 Set micronaut.security.authentication as idtoken. The idtoken provided by Cognito when the OAuth 2.0 Authorization code flow ends will be saved in a cookie. The id token is a signed JSON Web Token (JWT). For every request, the Micronaut framework extracts the JWT from the Cookie and validates the JWT signature with the remote JSON Web Key Set (JWKS) exposed by Cognito. JWKS is exposed by the jws-uri entry of Cognito .well-known/openid-configuration.

2 Accept GET request to the /logout endpoint.

3 The provider identifier must match the last part of the URL you entered as a redirect URL: /oauth/callback/cognito.

4 Client ID. See previous screenshot.

5 Client Secret. See previous screenshot.

6 The issuer URL. It allows the Micronaut framework to discover the configuration of the OpenID Connect server. Note: use the pool id and region mentioned previously.

The application configuration uses certain placeholders filled by environment variables. If you did not use AWS CLI or started a new terminal session, set up the OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, COGNITO_REGION and COGNITO_POOL_ID environment variables.

export COGNITO_POOL_ID=us-east-2XXCCCAZZZ
export OAUTH_CLIENT_SECRET=YYYYYYYYYY
export OAUTH_CLIENT_ID=XXXXXXXXXX
export COGNITO_REGION=us-east-2

Although undocumented in the Amazon Cognito User Pools Auth API Reference, Cognito provides an openid-configuration endpoint which the Micronaut framework internally uses to configure your application. The endpoint is available at https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_POOL_ID}/.well-known/openid-configuration.

You will use an Authorization Code grant type flow which it is described in the following diagram:

diagramm

3. Run the Application

To run the application, use the following command, which starts the application on port 8080.

./gradlew :aws:run

Open http://localhost:8080/ in a browser and click Enter. Then enter your sign-in information or click Sign Up to create a new account with your email:

Cognito login video

4. Generate a Native Executable Using GraalVM

The GDK supports compiling Java applications ahead-of-time into native executables using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.

Prerequisites: Make sure you have installed a GraalVM JDK. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.

  1. To generate a native executable, use the following command:

    ./gradlew :aws:nativeCompile

    The native executable is created in the aws/build/native/nativeCompile/ directory

  2. You can then run the native executable with the following command:

    aws/build/native/nativeCompile/aws-security-demo-aws

    You can customize the name of the resulting binary by updating the Maven/Gradle plugin for GraalVM Native Image configuration.

Run the native executable, then navigate to localhost:8080 and authenticate with Cognito.

5. Cleanup Cloud Resources

Once you are done with this guide, you can stop and delete the AWS resources created to avoid incurring unnecessary charges.

aws cognito-idp delete-user-pool-domain --domain $COGNITO_DOMAIN --user-pool-id $COGNITO_POOL_ID
aws cognito-idp delete-user-pool-client --client-id $OAUTH_CLIENT_ID --user-pool-id $COGNITO_POOL_ID
aws cognito-idp delete-user-pool --user-pool-id $COGNITO_POOL_ID

Summary

This guide demonstrated how to use the GDK to create an OAuth 2.0 application and secure it with an Authorization Server provided by Amazon Cognito. Then you packaged that application into a native executable with GraalVM Native Image for faster startup and lower memory footprint.