Spring Boot OAuth2 redirect not working properly after Twitch authentication

I’m working on an Angular app where users can pick their OAuth2 provider for login. Here’s my frontend code:

selectAuthProvider(providerName: string) {
  const authEndpoint = `http://localhost:8080/auth/oauth2/${providerName}`;
  window.location.href = authEndpoint;
}

When the user clicks the button, they get redirected to http://localhost:8080/auth/callback/twitch. I can get the authorization code, exchange it for an access token, and fetch user data successfully.

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth/callback")
public class AuthCallbackController {

    @Value("${oauth.twitch.app-id}")
    private String appId;
    @Value("${oauth.twitch.app-secret}")
    private String appSecret;
    
    private HttpHeaders buildAuthHeaders(String token) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + token);
        headers.set("Client-Id", appId);
        return headers;
    }

    public ResponseEntity<String> fetchUserData(String token) {
        RestTemplate client = new RestTemplate();
        HttpHeaders headers = buildAuthHeaders(token);
        HttpEntity<String> request = new HttpEntity<>(headers);

        String apiUrl = "https://api.twitch.tv/helix/users";
        ResponseEntity<String> result = client.exchange(apiUrl, HttpMethod.GET, request, String.class);

        return result;
    }

    public String exchangeCodeForToken(String code) {
        RestTemplate client = new RestTemplate();
        Map<String, String> payload = new HashMap<>();
        payload.put("client_id", appId);
        payload.put("client_secret", appSecret);
        payload.put("code", code);
        payload.put("grant_type", "authorization_code");
        payload.put("redirect_uri", "http://localhost:8080/auth/callback/twitch");

        String tokenEndpoint = "https://id.twitch.tv/oauth2/token";
        Map<String, Object> result = client.postForObject(tokenEndpoint, payload, Map.class);

        return result != null ? (String) result.get("access_token") : null;
    }

    @GetMapping("/twitch")
    public ResponseEntity<?> processTwitchCallback(@RequestParam("code") String code) {
        String token = exchangeCodeForToken(code);
        ResponseEntity<String> userData = fetchUserData(token);
        return ResponseEntity.ok().body("");
    }
}

Here are my app settings:

oauth.twitch.app-id=hidden
oauth.twitch.app-secret=hidden
spring.security.oauth2.client.registration.twitch.redirect-uri=http://localhost:8080/auth/callback/twitch
spring.security.oauth2.client.registration.twitch.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.twitch.scope=user:read:email

spring.security.oauth2.client.provider.twitch.authorization-uri=https://id.twitch.tv/oauth2/authorize
spring.security.oauth2.client.provider.twitch.token-uri=https://id.twitch.tv/oauth2/token
spring.security.oauth2.client.provider.twitch.user-info-uri=https://api.twitch.tv/helix/users
spring.security.oauth2.client.provider.twitch.user-name-attribute=login

Everything works fine until the redirect part. My security config looks like this:

@Configuration
public class AuthSecurityConfig {
    @Bean
    public SecurityFilterChain configureAuth(HttpSecurity http) throws Exception {
        http
                .oauth2Login()
                .loginPage("/oauth2/authorization/google")
                .defaultSuccessUrl("http://localhost:4200/auth-success")
                .failureUrl("http://localhost:4200/auth-failure")
                .and()
                .oauth2Login()
                .loginPage("/oauth2/authorization/twitch")
                .defaultSuccessUrl("http://localhost:4200/auth-success")
                .failureUrl("http://localhost:4200/auth-failure");
        return http.build();
    }
}

Google OAuth works perfectly and redirects as expected. I think it’s because Google is a built-in provider in Spring Boot, so I don’t need to manually handle tokens.

Why isn’t defaultSuccessUrl("http://localhost:4200/auth-success") working for Twitch? Could it be that my empty response body is preventing the redirect from triggering?

You’ve got conflicting OAuth2 configs in your security setup. Calling .oauth2Login() twice creates overlapping configurations that mess with each other. Your custom controller bypasses Spring Security’s OAuth2 flow entirely. When Spring Security handles OAuth2, it wants to manage the callback itself - not route it through your manual /auth/callback/twitch endpoint. Pick one approach: either go full Spring Security OAuth2 or completely manual. For Spring Security, ditch your custom callback controller and let Spring handle everything:

http.oauth2Login(oauth2 -> oauth2
    .defaultSuccessUrl("http://localhost:4200/auth-success")
    .failureUrl("http://localhost:4200/auth-failure")
);

Then redirect your frontend to /oauth2/authorization/twitch instead of your custom endpoint. Spring will automatically handle token exchange and user data through your properties config. The redirect works because Spring Security keeps the OAuth2 context throughout the whole flow.

the problem is you’re calling two oauth2Login() methods - they don’t work together like that. your custom controller returns an empty body, so there’s no redirect. if u wanna stick with the manual approach, just use response.sendRedirect() in your controller method instead of returning an empty ResponseEntity.

You’re mixing two different approaches here. You’ve got Spring Security OAuth2 set up, but you’re also manually handling the callback with your custom controller.

When you hit /auth/callback/twitch with your custom controller, Spring Security has no clue about it. The defaultSuccessUrl only works when Spring Security handles the entire OAuth2 flow itself.

Your custom controller returns an empty response - that’s why there’s no redirect. You need to manually redirect in your controller:

@GetMapping("/twitch")
public void processTwitchCallback(@RequestParam("code") String code, HttpServletResponse response) throws IOException {
    String token = exchangeCodeForToken(code);
    ResponseEntity<String> userData = fetchUserData(token);
    response.sendRedirect("http://localhost:4200/auth-success");
}

Honestly though, this whole setup’s getting messy with the mixed approaches. I’ve dealt with similar OAuth headaches and found external automation tools handle these flows way cleaner.

Latenode can manage your entire OAuth2 flow without all this Spring Security config mess. You can set up the Twitch OAuth process, handle token exchange, fetch user data, and redirect users seamlessly. Works the same for any OAuth provider too.

No more mixing manual controllers with Spring Security configs. Just clean workflows that actually work.