0

I’m developing a Spring Boot application deployed behind an AWS API Gateway (HTTP API v2) with Lambda (handler based on SpringBootLambdaContainerHandler and HttpApiV2ProxyRequest).

I’m using OAuth2 with Casdoor, but I’m running into an issue with the state parameter:

  • Casdoor redirects back with a state generated by Spring Security.
  • API Gateway seems to pass the state URL-encoded (=%3D).
  • When Spring Security tries to match the returned state with the stored one, I get: authorization_request_not_found.

It works correctly locally. I also tested generating a state without special characters, and in that case the login flow worked behind Lambda/API Gateway.

Logs

DEBUG o.s.security.web.FilterChainProxy - Securing GET /oauth2/authorization/casdoor?
DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to https://casdoor.example.com/login/oauth/authorize?...&state=abd7CZ2NFOsuFT2ivWcun89d8t7Ndnhn4o08AyrXb6A%3D&redirect_uri=https://api.example.com/login/oauth2/code/casdoor
INFO  LambdaContainerHandler - IP xxx.xxx.xxx.xxx --  "GET /oauth2/authorization/casdoor" 302
DEBUG o.s.security.web.FilterChainProxy - Securing GET /login?
INFO  LambdaContainerHandler - IP xxx.xxx.xxx.xxx -- "GET /login" 302
DEBUG o.s.security.web.FilterChainProxy - Securing GET /login/oauth2/code/casdoor?code=xxxxx&state=abd7CZ2NFOsuFT2ivWcun89d8t7Ndnhn4o08AyrXb6A=
ERROR SecurityConfig - OAuth2 login FAILURE
org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found]

My Lambda handler

public class LambdaHandler implements RequestStreamHandler {
    private static final SpringBootLambdaContainerHandler<HttpApiV2ProxyRequest, AwsProxyResponse>
            handler;

    static {
        try {
            handler = SpringBootLambdaContainerHandler.getHttpApiV2ProxyHandler(PojaApplication.class);
        } catch (ContainerInitializationException e) {
            throw new RuntimeException("Initialization of Spring Boot Application failed", e);
        }
    }

    @Override
    public void handleRequest(InputStream input, OutputStream output, Context context)
            throws IOException {
        handler.proxyStream(input, output, context);
    }
}

My Spring Security configuration

package com.example.demo.endpoint.rest.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);

  private final String casdoorClientId;
  private final String casdoorLogoutUrl;

  public SecurityConfig(
      @Value("${spring.security.oauth2.client.registration.casdoor.clientid}")
          String casdoorClientId,
      @Value("${casdoor.logout.url}") String casdoorLogoutUrl) {
    this.casdoorClientId = casdoorClientId;
    this.casdoorLogoutUrl = casdoorLogoutUrl;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(Customizer.withDefaults())
        .authorizeHttpRequests(
            authz ->
                authz
                    .requestMatchers("/casdoor-logout")
                    .permitAll()
                    .requestMatchers("/")
                    .permitAll()
                    .anyRequest()
                    .authenticated())
        .oauth2Login(
            oauth2 ->
                oauth2
                    .successHandler(
                        (request, response, authentication) -> {
                          log.info("OAuth2 login SUCCESS");
                          log.info("User: {}", authentication.getName());
                          log.info("Authorities: {}", authentication.getAuthorities());
                          response.sendRedirect("/welcome");
                        })
                    .failureHandler(
                        (request, response, exception) -> {
                          log.error("OAuth2 login FAILURE");
                          log.error("Message: {}", exception.getMessage());
                          new SimpleUrlAuthenticationFailureHandler("/oauth2/authorization/casdoor")
                              .onAuthenticationFailure(request, response, exception);
                          log.info("Forced redirect to /oauth2/authorization/casdoor executed");
                        }));

    return http.build();
  }
}


What I tried and expected

I ran the OAuth2 login flow locally, and it worked perfectly: Spring Security generated a state, Casdoor redirected back, and the state was correctly matched.

Behind AWS Lambda + HTTP API v2, using the default generated state (with characters like =), the login fails. Generating a state without special characters works correctly.

I expected Spring Security to automatically handle the state parameter and match it correctly behind Lambda + API Gateway.

What actually happens: the state arrives in the Lambda request, but Spring Security cannot match it with the stored state.

Question:

Could anyone please advise how to automatically handle the encoding/decoding of the state parameter so that Spring Security OAuth2 receives it correctly behind Lambda + HTTP API v2, and the Casdoor authentication works without having to generate the state manually?

Thank you in advance for your help!

1 Answer 1

2

Looking at your logs, the problem is that your state parameter is getting mangled somewhere in the flow:

  • Spring generates: abd7CZ2NFOsuFT2ivWcun89d8t7Ndnhn4o08AyrXb6A=

  • After redirect: abd7CZ2NFOsuFT2ivWcun89d8t7Ndnhn4o08A=

The ending yrXb6A= is missing, which is why Spring can't match it.

The Issue

AWS API Gateway has issues with query parameters containing special characters like =. It's either truncating at the = sign or doing some weird double-decoding that corrupts the parameter.

Quick Fix

Since you mentioned that states without special characters work fine, the easiest solution is to override Spring's default state generator to produce URL-safe tokens:

@Component
public class CustomAuthorizationRequestRepository 
    extends HttpSessionOAuth2AuthorizationRequestRepository {
    
    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, 
                                        HttpServletRequest request, 
                                        HttpServletResponse response) {
        if (authorizationRequest != null) {
            // Replace state with URL-safe version (no = or + characters)
            String state = UUID.randomUUID().toString().replace("-", "") + 
                          UUID.randomUUID().toString().replace("-", "");
            
            OAuth2AuthorizationRequest modifiedRequest = OAuth2AuthorizationRequest
                .from(authorizationRequest)
                .state(state)
                .build();
            
            super.saveAuthorizationRequest(modifiedRequest, request, response);
        } else {
            super.saveAuthorizationRequest(null, request, response);
        }
    }
}

Then wire it up in your SecurityConfig:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, 
                                       CustomAuthorizationRequestRepository authRepo) 
                                       throws Exception {
    http
        // ... your existing config ...
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(authorization -> authorization
                .authorizationRequestRepository(authRepo))
            // ... rest of your oauth2 config
        );
    
    return http.build();
}

Alternative: Fix the Corrupted State

If you absolutely need to work with the default Base64 states, you can try to fix them on the way back:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class StateFixFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain chain) throws ServletException, IOException {
        
        String path = request.getRequestURI();
        String state = request.getParameter("state");
        
        // Check if this is the OAuth callback with a potentially broken state
        if (path.contains("/login/oauth2/code/") && state != null && !state.endsWith("=")) {
            // Try to fix truncated Base64 by adding back the padding
            HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
                @Override
                public String getParameter(String name) {
                    if ("state".equals(name)) {
                        // Base64 strings should be multiple of 4 in length
                        int mod = state.length() % 4;
                        if (mod > 0) {
                            return state + "=".repeat(4 - mod);
                        }
                    }
                    return super.getParameter(name);
                }
            };
            chain.doFilter(wrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }
}

But honestly, the first approach (generating alphanumeric-only states) is cleaner and more reliable. API Gateway's query parameter handling can be unpredictable with special characters, so it's better to just avoid them entirely.

Sign up to request clarification or add additional context in comments.

1 Comment

Then update your question, and as this post provides two working solutions, apply the one the author recommends, and once you get things working, accept the answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.