'How to onfigure Spring Boot to authenticate Web-app users and REST clients using AWS Cognito (OAuth2/OIDC)

I need to configure a Spring Boot server to authenticate web-users and REST clients using AWS Cognito user-pool:

  1. Interactive/Web users that are using the ReachJS frontend should be redirected to Cognito for authentication, and are redirected back once the user's credentials are verified.
  2. Other machines using the server's REST API directly should get a token from Cognito and send it to my server as the Authorization: Bearer ... header.

Questions are:

  1. How to configure spring to authenticate using Cognito
  2. How do you make spring supporting these two distinct types of authentication simultaneously


Solution 1:[1]

Overview

Lets start with terminology:

  1. IDP (Identity Provider) is a 3rd party providing user management and authentication service, AWS Cognito in my case.
  2. Authentication of interactive/web users by redirecting them to the IDP is referred to in OAuth2/OIDC as the "Authorization Code Grant Flow".
  3. Client sending JWT tokent to a REST API is known as the "Client Credentials Flow".

Spring's spring-security-oauth2-client module is responsible for the "Authorization Code Grant Flow" and the spring-security-oauth2-resource-server module is responsible for the "Client Credentials Flow".

In order to use both flows/methods simultaneously, we need to tell spring how do determine what authentication method to use with an incoming HTTP request. As explained in https://stackoverflow.com/a/64752665/2692895, this can be done by looking for the Authorization: bearer ... header:

  1. If the request includes the Authorization header, assume its a REST client and use the "Client Credentials Flow".
  2. Else, its an interactive user, redirect to Cognito if not already authenticated.

Dependencies

I'm using Spring-Boot 2.6.6 (Spring 5.6.2).

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>

External Configuration - application.yaml

spring:
  security:
    oauth2:
      # Interactive/web users authentication
      client:
        registration:
          cognito:
            clientId: ${COGNITO_CLIENT_ID}
            clientSecret: ${COGNITO_CLIENT_SECRET}
            scope: openid
            clientName: ${CLIENT_APP_NAME}
        provider:
          cognito:
            issuerUri: https://cognito-idp.eu-central-1.amazonaws.com/${COGNITO_POOL_ID}
            user-name-attribute: email

      # REST API authentication
      resourceserver:
        jwt:
          issuer-uri: https://cognito-idp.eu-central-1.amazonaws.com/${COGNITO_POOL_ID}

Spring Security Configuration

Interactive/web users authentication:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
    // Needed for method access control via the @Secured annotation
    prePostEnabled = true,
    jsr250Enabled = true,
    securedEnabled = true
)
@Profile({"cognito"})
@Order(2)
public class CognitoSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @SneakyThrows
    @Override
    protected void configure(HttpSecurity http) {
        http
            // TODO disable CSRF because when enabled controllers aren't initialized
            //  and if they are, POST are getting 403
            .csrf().disable()

            .authorizeRequests()
            .anyRequest().authenticated()

            .and()
            .oauth2Client()

            .and()
            .logout()

            .and()
            .oauth2Login()
            .redirectionEndpoint().baseUri("/login/oauth2/code/cognito")
            .and()
        ;
    }
}

REST clients authentication:

/**
 * Allow users to use a token (id-token, jwt) instead of the interactive login.
 * The token is specified as the "Authorization: Bearer ..." header.
 * </p>
 * To get a token, the cognito client-app needs to support USER_PASSWORD_AUTH then use the following command:
 * <pre>
 *     aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --output json \
 *         --region $region --client-id $clientid --auth-parameters "USERNAME=$username,PASSWORD=$password" \
 *         | jq .AuthenticationResult.IdToken
 * </pre>
 */
@Slf4j
@Configuration
@Profile({"cognito"})
@Order(1)
public class CognitoTokenBasedSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @SneakyThrows
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
            .authorizeRequests().anyRequest().authenticated()
            .and().oauth2ResourceServer().jwt()
        ;
    }

}

Cognito Configuration Notes

  • In AWS Cognito, you need to create a user-pool and two client-applications, a "Public client" for the interactive/web users and a "Confidential client" for the token based REST clients.
  • In the "Public client", make sure to define the "allowed callback URL" for all your environments (localhost, production etc), they all should be similar to http://localhost:8080/login/oauth2/code/cognito (with the correct host-name and port of course).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Michael Yakobi