preface

This article introduces the OAuth authorization code mode authentication process based on Amazon Cognito in the BFF case based on Spring Cloud Gateway + AWS Cognito. In this article, we examine possible malicious attacks against this process and how to prevent them. You will learn how to use state nonce to prevent possible CSRF attacks and the PKCE (Proof Key for Code Exchange) mechanism recommended by OAuth to prevent redirected interception attacks. Finally, a Spring Security-based code implementation is attached.

CSRF

Cognito’s LOGIN Endpoint requires only a few parameters:

  • response_type=code
  • client_id
  • redirect_uri

This is public information because all users use the same values. Therefore, if the user is on a page controlled by the attacker, the attacker can redirect the user to a valid login URL. If the user has recently logged in, the AUTHORIZATION endpoint in Cognito will be redirected directly with a valid code, and the logging endpoint may not even prompt the user to log in. Thus, an attacker can get a user to log in to a Web application without lifting a finger.

The attacker cannot control the redirect_URI parameter because the Cognito user pool is configured with a list of valid redirection URIs. So there’s no vulnerability, no way for an attacker to change it.

However, there is an optional parameter in the logon process controlled by the attacker: state. If this value is provided in the LOGIN redirect, Cognito returns it to the Web application intact.

State itself is not vulnerable to any attack. But Web applications may implement custom logic that exploits attackers.

Imagine a CRM application that handles customer accounts by automatically retrying failed operations when access tokens expire. For example, an administrator might want to delete a customer account, but the back end returns an error indicating that the browser needs to log in again. Webapp redirects to the Cognito login page, webApp retrieves a new token, and then retries the operation with the new token. The easiest way to do this is to add the failed operation to the state parameter so that WebApp can know what operation to retry.

attack

Such an implementation provides an attacker with a way to trick the user into sending arbitrary requests back end. This is known as a CSRF (Cross-site Request Forgery) attack. Here’s how it works:

Even if the consequences are not so severe, WebApp should prevent the acquisition of tokens outside of legitimate login attempts.

defense

To defend against CSRF attacks, WebApp needs to generate and store a Nonce (a randomly generated value is used only once) and use it as a state. Then, when Cognito redirects back to WebApp, the WebApp needs to compare the stored status value with the received status value and throw an error if it doesn’t match.

Using this security mechanism, the attacker is at a loss.

PKCE

PKCE, pronounced “pixy,” is an acronym for Proof Key for Code Exchange. The main difference between the PKCE process and the standard authorization code process is that the user does not need to provide client_secret. PKCE reduces the security risk for Native applications (a type of OAuth client) because secret does not need to be embedded in the source code, which limits the possibility of obtaining secret through reverse engineering.

Authorization Code redirection interception attacks defined in the RFC7636-Proof Key for Code Exchange by OAuth Public Clients standard are as follows:

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ | End Device (e.g., Smartphone) | | | | +-------------+ +----------+ | (6) Access Token +----------+ | |Legitimate | | Malicious | < -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | | | the 2.0 App | | App | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > | | | + -- -- -- -- -- -- -- -- -- -- -- -- -- + + -- -- -- -- -- -- -- -- -- -- + | (5) Authorization | | | | ^ ^ | Grant | | | | \ | | | | | | \ (4) | | | | | (1) | \ Authz| | | | | Authz| \ Code | | | Authz | | Request| \ | | | Server | | | \ | | | | | | \ | | | | | v \ | | | | | +----------------------------+ | | | | |  | | (3) Authz Code | | | | Operating System/ |<--------------------| | | | Browser |-------------------->| | | | | | (2) Authz Request | | | +----------------------------+ | +----------+ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ Figure 1: Authorization Code Interception AttackCopy the code

How does it work?

In place of client_secret, the client application creates a unique string value code_verifier, which is hashed to generate code_challenge. When the client application starts the first part of the authorization code process, it sends a code_challenge.

Once the user authenticates and returns the authorization code to the client application, it requests the authorization code in exchange for an Access_token.

In this step, the client application must include the original unique string value in the code_verifier parameter. If a match is successful, authentication is complete and access_token is returned.

attack

The above security issues occur when Cognito uses the authorization code parameter to redirect to a Web application. The TOKEN endpoint is then invoked, exchanging the resulting authorization code for an access TOKEN. The following parameters are required for this call:

  • grant_type=authorization_code
  • client_id
  • redirect_uri
  • code

All information except the authorization code is public because every user is the same. Therefore, anyone with a valid authorization code can obtain a valid access token.

Similarly, attackers have no control over where Cognito redirects users after logging in, because the user pool is configured with a list of legitimate redirect addresses. But in many cases, requests can be captured. For example, a mobile application could register a handler for that particular URL, or a back-end application could log those requests to an insecure place. In both cases, an attacker can read the authorization code parameters and then obtain a valid token.

defense

PKCE is an OAuth 2.0 extension that protects authorization code redirection. This is similar to the state argument, but it is enforced by the TOKEN endpoint. When WebApp redirects to the LOGIN endpoint, it sets a code_challenge. Cognito generates the authorization code associated with the code_challenge, which requires a code_verifier parameter in addition to the authorization code to obtain the token.

Even if an attacker can intercept the redirect and retrieve the code parameter, code_verifier without webApp is useless.

implementation

We use Spring Security to support the OAuth2 authentication process. Fortunately, Spring Security already natively supports CSRF and PKCE. The state and nonce parameters are automatically set when jumping to a LOGIN endpoint using Spring Security, which is really a relief for developers.

Note, however, that for PKCE, Spring Security only supports Public Client types by default, such as Native Applications or browser-based spAs. The following conditions must be met for the virtual Authorization Request:

  1. No configuration in application.yamlclient-secret, or the value is null
  2. client-authentication-methodSet to “None” (ClientAuthenticationMethod.NONE)

The problem is that our OAuth Client is using the Spring Cloud Gateway, which belongs to the Confidential Client. Spring Security does not support PKCE mechanism for this Client by default. Fortunately, the Spring Security design is very easy to expand, we can customize a ServerOAuth2AuthorizationRequestResolver, used to add code_verifier and code_challenge parameters. The code is as follows:

@Component
public class  MyOAuth2AuthorizationRequestResolver implements ServerOAuth2AuthorizationRequestResolver {

    private DefaultServerOAuth2AuthorizationRequestResolver defaultResolver;

    private final StringKeyGenerator secureKeyGenerator =
        new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);

    public MyOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        defaultResolver = new DefaultServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
    }

    @Override
    public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {
        return defaultResolver.resolve(exchange).map(req -> customizeAuthorizationRequest(req));
    }

    @Override
    public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange, String clientRegistrationId) {
        return defaultResolver.resolve(exchange, clientRegistrationId).map(req -> customizeAuthorizationRequest(req));
    }

    private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest req) {
        if (req == null) { return null; }

        Map<String, Object> attributes = new HashMap<>(req.getAttributes());
        Map<String, Object> additionalParameters = new HashMap<>(req.getAdditionalParameters());
        addPkceParameters(attributes, additionalParameters);
        return OAuth2AuthorizationRequest.from(req)
            .attributes(attributes)
            .additionalParameters(additionalParameters)
            .build();
    }

    private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
        String codeVerifier = this.secureKeyGenerator.generateKey();
        attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
        try {
            String codeChallenge = createHash(codeVerifier);
            additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
            additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
        } catch(NoSuchAlgorithmException e) { additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); }}private static String createHash(String value) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
        returnBase64.getUrlEncoder().withoutPadding().encodeToString(digest); }}Copy the code

At the same time, in the setting of SecurityWebFilterChain, need to add custom ServerOAuth2AuthorizationRequestResolver. As follows:


@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository, MyAuthorizationManager authorizationManager, MyOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver) {
    // Authenticate through configured OpenID Provider
    // user custom request resolver to add code_challenge & code_challenge_method(S256) for PKCE
    http.oauth2Login(oAuth2LoginSpec -> oAuth2LoginSpec.authorizationRequestResolver(oAuth2AuthorizationRequestResolver));
    // ...
    
    return http.build();
}
Copy the code

Thus, code_verifier and code_challenge parameters are added to the LOGIN request made by the API gateway to Cognito.

conclusion

This paper introduces the Security problems that oAuth-based microservices may encounter, namely CSRF attacks and redirection attacks, and also introduces Spring Security-based solutions.

If it helps you, please click “like” and subscribe to share. Thanks!

Related articles

  • Microservices: How to test oAUth-certified microservices
  • Microservices: BFF case based on Spring Cloud Gateway + AWS Cognito

Refer to the link

  • rfc7636 – Proof Key for Code Exchange by OAuth Public Clients
  • How to secure the Cognito login flow with a state nonce and PKCE
  • OpenID Connect Core 1.0 incorporating errata set 1