Simplified Authentication using Spring Security with AWS Cognito and AWS CDK

By Adam McQuistan in Security  08/10/2022 Comment

Introduction

This article presents an example of using AWS Cloud Development Kit (CDK) to deploy an AWS Cognito User Pool as an Identity Provider for authenticating a Spring Security enabled Spring Boot REST API.

Initializing AWS CDK Project

To follow along readers need to have the AWS CDK installed along with the necessary prerequisites of NodeJS and the AWS CLI. Refer to Getting Started with AWS CDK.

Once the CDK CLI is installed use it to initialize a new project inside a empty directory. For example, in this tutorial I'll call the directory cognito-spring-security.

mkdir cognito-spring-security
cd cognito-spring-security
cdk init app --language typescript

Provisioning Cognito User Pool and App Client

Now that I have an empty Typescript CDK project my next course of action is to code up the definition and configuration of my Cognito User Pool along with App Client. The User Pool is to be configured to allow users to sign up (aka register themselves) and to allow users to sign in using either their email or a username they select. An app client is configured to use the OAuth 2 based Authorization Code Grant to generate a authentication token after a user authenticates with the Congito Hosted UI.

In the lib/cognito-spring-security-stack.ts I place to following code to provision the Cognito User Pool as described.

import { Stack, StackProps, CfnOutput, Aws } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cognito from "aws-cdk-lib/aws-cognito";

export class CognitoSpringSecurityStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const userPool = new cognito.UserPool(this, "cognito-spring-security-userpool", {
      userPoolName: 'cognito-spring-security',
      selfSignUpEnabled: true,
      userVerification: {
        emailSubject: "Email Verification",
        emailBody: "Your verification code is {####}",
        emailStyle: cognito.VerificationEmailStyle.CODE
      },
      signInAliases: {
        email: true,
        username: true
      },
      signInCaseSensitive: false,
      standardAttributes: {
        email: {
          required: true
        }
      }
    });

    const appClient = userPool.addClient('cognito-spring-security-appclient', {
      userPoolClientName: 'cognito-spring-security',
      oAuth: {
        callbackUrls: [ 'http://localhost:8080' ],
        flows: {
          authorizationCodeGrant: true
        },
        scopes: [ cognito.OAuthScope.OPENID, cognito.OAuthScope.EMAIL ],
        
      }
    });

    const domainPrefix = 'tci-userpool-spring-security';
    const domain = userPool.addDomain('cognito-spring-security-domain', {
      cognitoDomain: {
        domainPrefix
      }
    });
    new CfnOutput(this, 'auth-url', {
      value: `${domain.baseUrl()}/login`
    });
    new CfnOutput(this, "auth-token-url", {
      value: `${domain.baseUrl()}/oauth2/token`
    });
    new CfnOutput(this, 'client-id', {
      value: appClient.userPoolClientId
    });
    new CfnOutput(this, 'jwt-issuer-uri', {
      value: userPool.userPoolProviderUrl
    });
  }
}

Deploy CDK Project

At this point I can build, synthesize and deploy the CDK project using the following commands.

npm run build

If you get the following error.

node_modules/@types/prettier/index.d.ts:41:54 - error TS2315: Type 'IsTuple' is not generic.

41 type IndexProperties<T extends { length: number }> = IsTuple<T> extends true

Then add the prettier types as a dev dependency and build again.

npm install --save-dev @types/prettier@2.6.0
npm run build

Next synthesize the CDK project into a CloudFormation template then deploy it to the AWS Cloud.

npm run cdk synth
npm run cdk deploy --all --require-approval=never

This last command should output something similar to what is shown below (yours will vary slightly).

 ✅  CognitoSpringSecurityStack

Outputs:
CognitoSpringSecurityStack.authtokenurl = https://tci-userpool-spring-security.auth.us-east-1.amazoncognito.com/oauth2/token
CognitoSpringSecurityStack.authurl = https://tci-userpool-spring-security.auth.us-east-1.amazoncognito.com/login
CognitoSpringSecurityStack.clientid = 75q4j2k7gmr465dritcf2bjp8c
CognitoSpringSecurityStack.jwtissueruri = https://cognito-idp.us-east-1.amazonaws.com/us-east-1_CCcH79CXU

Configure Spring Boot Demo App with Spring Security

In this section I show how to utilize a spring Boot app to expose a REST API endpoint that is secured by my Cognito User Pool and Client App integration.

To generate the Spring Boot project I'll use the defacto Spring Initializer site. Please visit this link to generate the exact same starter project which utilizes the spring boot web and security starter along with OAuth Resource Server dependencies.

https://start.spring.io/#!type=gradle-project&language=java&platformVersion=2.7.2&packaging=jar&jvmVersion=17&groupId=com.thecodinginterface&artifactId=cognito-spring-security&name=cognito-spring-security&description=Demo%20project%20for%20Spring%20Boot&packageName=com.thecodinginterface.cognitospringsecurity&dependencies=web,security,oauth2-resource-server

After you've downloaded and extracted the project then modify the application.properties file to configure the Boot application to use the Cognito IdP JWT Issuer url output from the deployment step earlier.

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_CCcH79CXU

Next add a new Java class containing the security configuration named SpringSecurity.java which allows for an anonymous unathenticated request to a /hello endpoint while all other endpoints being locked down to authenticated requests utilizing oauth2 JSON Web Tokens (JWT).

package com.thecodinginterface.cognitospringsecurity;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
            .authorizeRequests(authReqCustomizer
                    -> authReqCustomizer
                        .antMatchers("/hello").permitAll()
                        .anyRequest().authenticated())
            .oauth2ResourceServer().jwt();
        return http.build();
    }
}

Next I add a REST Controller packaged in a new Java class file named GreetingsController.java. This class defines a method mapping to the root GET route which extracts the authenticated principal as a JWT token as an instance of the JwtAuthenticationToken class and returns a string "Howdy, USERNAME!". A second method mapping to another GET request for incoming request to /hello which returns the string "Hello".

package com.thecodinginterface.cognitospringsecurity;

import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingsController {
    @GetMapping
    public String greet(JwtAuthenticationToken principal) {
        var username = principal.getToken().getClaimAsString("username");
        return String.format("Howdy, %s!", username);
    }

    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }
}

Demo Signup / Signin and Token Generation

To demonstrate how to use the Cognito Hosted UI to signup a new user then authenticate and generate access token I'll use Postman.

Start a new request tab in Postman and switch to the Authorization tab. Use the output values from the CDK project deployment step as shown in the image below then click "Get New Access Token".

In the sign in dialog click the signup link.

Fill in the username, email, and password then click "Sign up".

The email inbox associated with the email you entered will receive a message containing a confirmation code.

Enter the confirmation code from the email into the dialog back in Postman and click "Confirm Account".

You can now view your OAuth tokens then click "Use Token".

If you haven't already select GET as the request method and enter a url of http://localhost:8080 then click "Send". You should see a response in the console of Postman with the username you entered at signup.

Conclusion

In this short tutorial I demonstrated how to deploy an AWS Cognito User Pool with an App Client integrated with OAuth2 code grant using AWS CDK and use it with Spring Security enabled Spring Boot Resource server to secure a REST API.

As always, I thank you for reading and please feel free to ask questions or critique in the comments section below.

Share with friends and colleagues

[[ likes ]] likes

Community favorites for Security

theCodingInterface