2

Body: I developed an IAM system using Spring Boot where I integrated multi-factor authentication (MFA).

Here’s the flow I implemented:

A client sends a GET request to /oauth2/authorize.

This redirects to /login.

If the user enters the correct credentials, an OTP is sent via email.

After OTP verification, the system should generate the auth code.

To achieve this, I’m caching the /oauth2/authorize request until the OTP is verified. After verification, I recall the cached request and generate the auth code.

The issue:

On my local machine, everything works fine.

On the server, the cached request becomes null after OTP verification.

So it seems like the /oauth2/authorize request is not being cached properly on the server environment.

Question: Why is my cached request becoming null on the server but working fine locally? How can I fix this issue so the /oauth2/authorize request is retained until OTP verification is complete?

Notes:

Using Spring Boot (OAuth2 Authorization Server + custom MFA).

Session management and caching are being used to store the request.

No errors in logs, just null when trying to retrieve the cached request.

@Configuration
public class DefaultSecurityConfig {

    private final CorsConfigurationSource corsConfigurationSource;
    private final UserDetailsService userDetailsService;
    private final PasswordEncoderFactory passwordEncoderFactory;
    private static final Logger LOGGER = LogManager.getLogger(DefaultSecurityConfig.class);
    private final MFASettings mfaSettings;

    public DefaultSecurityConfig(CorsConfigurationSource corsConfigurationSource,
                                 UserDetailsService userDetailsService,
                                 PasswordEncoderFactory passwordEncoderFactory, final MFASettings mfaSettings) {

        this.corsConfigurationSource = corsConfigurationSource;
        this.userDetailsService = userDetailsService;
        this.passwordEncoderFactory = passwordEncoderFactory;
        this.mfaSettings = mfaSettings;
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, JwtDecoder jwtDecoder) throws Exception {

        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource))
            .csrf(crs -> crs.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/oauth2/token").permitAll()
                .requestMatchers("/.well-known/jwks.json").permitAll()
                .requestMatchers("/swagger-ui/**",
                                 "/v3/api-docs/**",
                                 "/swagger-ui.html",
                                 "/webjars/**"
                ).permitAll()
                .anyRequest().authenticated())
            .userDetailsService(userDetailsService)
            .formLogin(form -> form
                .loginPage("/login") // Use custom login page
                .failureUrl("/login?error=true")
                .successHandler(mfaAuthenticationSuccessHandler()) // hook MFA check here
                .permitAll()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.decoder(jwtDecoder)));

        return http.build();
    }

    @Bean
    public AuthenticationSuccessHandler mfaAuthenticationSuccessHandler() {

        return (request, response, authentication) -> {
            // Check if MFA is required
            LOGGER.info("is Multi-Factor authentication enabled : {}", mfaSettings.getMFAEnable());
            if (mfaSettings.getMFAEnable() && authentication.getAuthorities().stream()
                .noneMatch(a -> a.getAuthority().equals("MFA_VERIFIED"))) {
                // Generate 6-digit code
                String code = RandomStringUtils.randomNumeric(6);

                // Send email (get MfaController bean)
                WebApplicationContext ctx =
                    WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
                MfaController mfaController = ctx.getBean(MfaController.class);

                LoginUser user = (LoginUser) authentication.getPrincipal();

                if(mfaSettings.getEmailEnable()) {
                    mfaController.sendEmail(user.getEmail(), code);
                }

                if(mfaSettings.getMFAEnable()) {
                    // TODO: have to implement
                }

                request.getSession().setAttribute("MFA_CODE", code);// Store in session
                request.getSession().setAttribute("MFA_TIMESTAMP", System.currentTimeMillis());
                LOGGER.info("SuccessHandler session ID: {} code {}", request.getSession().getId(), code);

                // Save the original request (/oauth2/authorize with params)
                RequestCache requestCache = new HttpSessionRequestCache();
                SavedRequest savedRequest = requestCache.getRequest(request, response);
                if (savedRequest == null || !savedRequest.getRedirectUrl().contains("/oauth2/authorize")) {
                    // Save the original request if not already saved or incorrect
                    requestCache.saveRequest(request, response);
                }

                // Redirect to MFA verify
                response.sendRedirect("/mfa/verify");
            }
            else {
                // Proceed with default redirect (to saved request, e.g., /oauth2/authorize)
                RequestCache requestCache = new HttpSessionRequestCache();
                SavedRequest savedRequest = requestCache.getRequest(request, response);
                LOGGER.info("Saved Request URL (no MFA): {}", savedRequest != null ? savedRequest.getRedirectUrl() : "null");
                new SavedRequestAwareAuthenticationSuccessHandler().onAuthenticationSuccess(request, response, authentication);
            }
        };
    }


}

I want to properly cache that /oauth2/authorize request with all the details

I am using this script to run application

nohup java/bin/java -Xmx2G -Xms2G -Xss256M -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+UseG1GC -server -jar ./code-0.0.1-SNAPSHOT.jar --mode.server > IAMNohup &

0

1 Answer 1

0

1. Do you have the same session id when the client is redirected back to your application?

2. Are your client and OAuth2 server on the same host? If not, you should be aware that Cookie shouldn't be set to Strict, because the browser will not send it back to a different domain. It should be set to Lax in this case scenario.

EDIT:

I assume that when you click the link in the email message, your request ID is different from what you are expecting.
It's because your cookie is probably set to Strict - the cookie is not sent with your link.
There is a similar issue described with an explanation: troypoulter.com/blog/…. It works on localhost because you have the same domain.

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

4 Comments

1. No. After verification is completed, the session ID is different from the one created during login. 2. Yes. Both the Login UI and the MFA Verification UI are implemented in the same Spring Boot backend server (login_page.html, mfa_verify.html).
As you surely know, answers should (at least attempt to) answer the question. I'd recommend formulating your answer in a way such that it doesn't look like you'd be asking the OP for details but rather of explainig how these things could cause the issue and how you'd fix that.
Following the suggestion of @dan1st, I assume that when you click the link in the email message, your request ID is different from what you are expecting. I assume it's because your cookie is set to Strict, thus the cookie is not sent with your link. There is a similar issue described with explanation: troypoulter.com/blog/…. It works on localhost, because you have the same domain.
Please edit your answer and make sure everything important is there. Comments are often considered to be temporary and have less visibility than the actual answers.

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.