skip to Main Content

I know normally Cognito itself is an authorization server actor in Oauth2 flow.
But as per my custom requirements I want to use spring authorization server with Cognito, basically:

  • A client comes to oauth2/authorize endpoint with its client ID
  • Redirects to login page and spring will know which client is trying a user to login.
  • Client receives an auth code from spring after successful login (via
    Cognito, so auth code from spring, but login should be via cognito)
  • Then sends a POST request to token endpoint of Spring to receive access/ID/refresh tokens from Cognito.
  • All app clients will be taken from Cognito user pool.

I saw the JDBC example but can’t really get it work with Cognito, any help would be appreciated.

2

Answers


  1. I find in that regard the presentation "Want to authenticate using Amazon Cognito? Then use Spring Security!", from Ryosuke Uchitate enlightening, regarding the integration of Amazon Cognito with Spring.

    You can find his implementation at b1a9id/spring-security-with-cognito (2018)

    Spring Security

    The general idea would be to provide a AbstractUserDetailsAuthenticationProvider, using a CognitoService, with AutoWired annotation:

    @Component
    public class CustomCognitoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
        @Autowired
        private CognitoService cognitoService;
    
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            // Perform additional authentication checks if needed.
        }
    
        @Override
        protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            String password = authentication.getCredentials().toString();
    
            // Call your CognitoService to authenticate the user with the provided credentials.
            return cognitoService.authenticate(username, password);
        }
    }
    

    For the Cognito service, you would need to add the AWS Java SDK for Cognito Identity Provider to your project’s dependencies. If you’re using Maven, add the following to your pom.xml:

    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-java-sdk-cognitoidp</artifactId>
        <version>1.12.454</version>
    </dependency>
    

    The service itself would require the appropriate AWS credentials, region, and Amazon Cognito configuration (client ID, client secret):

    @Service
    public class CognitoService {
    
        @Value("${cognito.clientId}")
        private String clientId;
    
        @Value("${cognito.clientSecret}")
        private String clientSecret;
    
        @Value("${cognito.userPoolId}")
        private String userPoolId;
    
        private AWSCognitoIdentityProvider cognitoIdentityProvider;
    
        @PostConstruct
        public void init() {
            // Initialize your Amazon Cognito Identity Provider with your AWS credentials.
            // Consider using a proper credential provider to avoid hardcoding credentials.
            AWSCredentials awsCredentials = new BasicAWSCredentials("YOUR_AWS_ACCESS_KEY", "YOUR_AWS_SECRET_KEY");
            AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
    
            // Create the Amazon Cognito client.
            this.cognitoIdentityProvider = AWSCognitoIdentityProviderClientBuilder.standard()
                    .withCredentials(credentialsProvider)
                    .withRegion(Regions.YOUR_AWS_REGION)
                    .build();
        }
    
        public UserDetails authenticate(String username, String password) {
            // Create the authentication request.
            AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest()
                    .withAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH)
                    .withClientId(clientId)
                    .withUserPoolId(userPoolId)
                    .withAuthParameters(authParameters(username, password));
    
            try {
                // Perform the authentication request.
                AdminInitiateAuthResult authResult = cognitoIdentityProvider.adminInitiateAuth(authRequest);
    
                // If the user is authenticated, return a UserDetails object.
                return new User(username, password, new ArrayList<>());
            } catch (NotAuthorizedException | UserNotFoundException e) {
                throw new BadCredentialsException("Invalid username or password", e);
            } catch (Exception e) {
                throw new AuthenticationServiceException("Error during authentication", e);
            }
        }
    
        private Map<String, String> authParameters(String username, String password) {
            Map<String, String> authParameters = new HashMap<>();
            authParameters.put("USERNAME", username);
            authParameters.put("PASSWORD", password);
            authParameters.put("SECRET_HASH", calculateSecretHash(clientId, clientSecret, username));
    
            return authParameters;
        }
    
        private String calculateSecretHash(String clientId, String clientSecret, String username) {
            try {
                Mac mac = Mac.getInstance("HmacSHA256");
                SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                mac.init(secretKeySpec);
    
                String data = username + clientId;
                byte[] digest = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
                return Base64.getEncoder().encodeToString(digest);
            } catch (NoSuchAlgorithmException | InvalidKeyException e) {
                throw new AuthenticationServiceException("Error calculating the secret hash", e);
            }
        }
    }
    

    You then could configure your Spring Security to use the custom authentication provider and set up the Authorization Server.

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private CustomCognitoAuthenticationProvider customCognitoAuthenticationProvider;
    
        @Autowired
        private OAuth2AuthorizationServerConfiguration oAuth2AuthorizationServerConfiguration;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authenticationProvider(customCognitoAuthenticationProvider)
                .apply(oAuth2AuthorizationServerConfiguration)
                .and()
                .authorizeRequests()
                .antMatchers("/oauth2/authorize").authenticated()
                .anyRequest().permitAll();
        }
    }
    

    BUT: as documented here:

    In Spring Security 5.7.0-M2 (Feb. 2022) we deprecated the WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration.
    In Spring Security 5.4 (Sep. 2020) we introduced the ability to configure HttpSecurity by creating a SecurityFilterChain bean.

    That means the more modern implementation would use a SecurityFilterChain bean:

    @Configuration
    public class SecurityConfiguration {
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                .authorizeHttpRequests((authz) -> authz
                    .anyRequest().authenticated()
                )
                .httpBasic(withDefaults());
            return http.build();
        }
    
    }
    

    You need to pass the original parameter, like .authenticationProvider(customCognitoAuthenticationProvider) to the new HttpSecurity http, as illustrated in "WebSecurityConfigurerAdapter Deprecated in Spring Boot" or in "Spring Security – How to Fix WebSecurityConfigurerAdapter Deprecated".


    Then you configure your Spring Authorization Server (OAuth2AuthorizationServerConfiguration) with your custom token store (using Amazon Cognito’s access/ID/refresh tokens).

    @Configuration
    public class OAuth2AuthorizationServerConfiguration {
    
        @Autowired
        private CustomTokenStore customTokenStore;
    
        public OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServer() {
            OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
            authorizationServerConfigurer
                .tokenStore(customTokenStore)
                .authorizationEndpoint()
                .baseUri("/oauth2/authorize");
    
            return authorizationServerConfigurer;
        }
    }
    

    That means implementing a custom TokenStore, which should be designed to handle the storage and retrieval of OAuth2 tokens (access tokens, ID tokens, and refresh tokens).

    In your case: tokens issued by Amazon Cognito.

    An in-memory one (not suitable for production) would start with:

    public class CustomTokenStore implements TokenStore {
    
        private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore = new ConcurrentHashMap<>();
        private final ConcurrentHashMap<String, OAuth2RefreshToken> refreshTokenStore = new ConcurrentHashMap<>();
    
        @Override
        public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
            return accessTokenStore.values()
                    .stream()
                    .filter(token -> authentication.equals(token.getOAuth2Authentication()))
                    .findFirst()
                    .orElse(null);
        }
        ...
    

    As noted in "TokenStore in Spring Security 5.x after removal of Spring Security OAuth 2.x", you need to check if this is compatible with the latests from Spring Security 5.X/6.0+.


    From what I understand (but cannot durectly test), using your steps:

    1. A client comes to the /oauth2/authorize endpoint with its client ID:

    In SecurityConfig.java:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/cognito/callback", "/login**").permitAll()
                .anyRequest().authenticated()
            .and()
            .oauth2Login()
                .authorizationEndpoint()
                    .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                    .baseUri("/oauth2/authorize")
                    ...
    }
    

    As noted above, this should be done within a SecurityFilterChain if you are using Spring Security 5.X/6.x.

    1. Redirects to the login page, and Spring will know which client is trying a user to log in:

    In application.yml:

    spring:
      security:
        oauth2:
          client:
            registration:
              cognito:
                client-id: your-cognito-app-client-id
                client-secret: your-cognito-app-client-secret
                ...
            provider:
              cognito:
                issuer-uri: https://cognito-idp.your-aws-region.amazonaws.com/your-cognito-user-pool-id
    
    1. Client receives an auth code from Spring after successful login (via Cognito, so auth code from Spring, but login should be via Cognito):

    In SecurityConfig.java:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .oauth2Login()
                ...
                .redirectionEndpoint()
                    .baseUri("/cognito/callback")
                    ...
    }
    

    That redirection endpoint configured in the SecurityConfig would be responsible for handling the callback from Amazon Cognito after a successful login. The auth code is actually received from Amazon Cognito, not from Spring.

    Meaning:

    • The user logs in via the Amazon Cognito login page.
    • Upon successful login, Amazon Cognito sends the user back to the configured callback URL (e.g., /cognito/callback) along with an authorization code as a query parameter.
    • Spring Security handles the request to the /cognito/callback endpoint and extracts the authorization code from the query parameter.
    1. Then sends a POST request to the token endpoint of Spring to receive access/ID/refresh tokens from Cognito:

    In CognitoOAuth2LoginSuccessHandler.java:

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        OAuth2AuthenticationToken oAuth2Authentication = (OAuth2AuthenticationToken) authentication;
    
        // Retrieve the authorized client.
        OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(oAuth2Authentication.getAuthorizedClientRegistrationId(), oAuth2Authentication.getName());
    
        // Extract access token, ID token, and refresh token from the authorized client.
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
        OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
        String idToken = authorizedClient.getAdditionalParameters().get("id_token").toString();
    
        // Store tokens or use them as needed.
    
        // Redirect the user to the desired page after successful login.
        getRedirectStrategy().sendRedirect(request, response, "/success-page");
    }
    
    1. All app clients will be taken from the Cognito user pool:

    In CognitoOAuth2UserService.java:

    @Service
    public class CognitoOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            // Extract user attributes from the OAuth2UserRequest.
            Map<String, Object> userAttributes = userRequest.getUserInfo().getAttributes();
            return new DefaultOAuth2User(Collections.singleton(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()), userAttributes, userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
        }
    }
    

    Regarding OAuth2AuthorizationService: there should be no need for a direct replacement of OAuth2AuthorizationService.
    Spring Security’s built-in support for OAuth2 should take care of managing the authorization code flow, including sending the authorization code to Amazon Cognito’s token endpoint and exchanging it for access, ID, and refresh tokens.
    The CognitoOAuth2UserService is responsible for loading user details from Amazon Cognito, while the CognitoOAuth2LoginSuccessHandler handles the successful authentication and provides access to the tokens.

    Login or Signup to reply.
  2. Thought I’d add some notes here on the OAuth architecture to aim for, and how I think of it, since your question had a couple of points that don’t quite seem right. I can’t help you on Spring AS specifics though.

    ROLES

    • Client: triggers user login via Spring AS
    • Authorization Server: issues tokens to the client
    • Identity Provider: one of many potential login methods

    The client implements a code flow at the AS. The AS runs another code flow to the IDP. Chaining these systems together is very standard and should require only configuration, with zero code changes in the client.

    REGISTRATION

    • The client is registered only in Spring AS
    • Spring AS is registered as a client in AWS Cognito
    • AWS Cognito is registered as an authentication method (IDP) in Spring AS

    TOKENS ISSUED

    The client always receives tokens from the AS and not the IDP. The AS issues tokens that protect your business data. It enables you to issue whatever scopes and claims you need to lock down tokens.

    UPSTREAM TOKENS

    Clients and APIs should not usually need to deal with tokens from the IDP. There is sometimes an exception, eg also use AWS tokens to access the user’s AWS resources.

    If that is your requirement, aim to use embedded tokens. This means Spring AS issues IDP tokens as custom claims to AS tokens. This enables your APIs to continue to authorize correctly, while also being able to access AWS resources when needed.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search