From 39442efb09635a4258a0e61b6cf84988a5d27c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Wed, 3 Dec 2025 11:43:47 +0100 Subject: [PATCH] Fix oauth model --- .../app/config/OAuth2RestTemplateFactory.java | 28 +++++++++++++ .../imgfloat/app/config/SecurityConfig.java | 18 ++++++++- ...TwitchOAuth2ErrorResponseErrorHandler.java | 39 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/imgfloat/app/config/OAuth2RestTemplateFactory.java create mode 100644 src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java diff --git a/src/main/java/com/imgfloat/app/config/OAuth2RestTemplateFactory.java b/src/main/java/com/imgfloat/app/config/OAuth2RestTemplateFactory.java new file mode 100644 index 0000000..562491f --- /dev/null +++ b/src/main/java/com/imgfloat/app/config/OAuth2RestTemplateFactory.java @@ -0,0 +1,28 @@ +package com.imgfloat.app.config; + +import java.util.Arrays; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +final class OAuth2RestTemplateFactory { + + private OAuth2RestTemplateFactory() { + } + + static RestTemplate create() { + RestTemplate restTemplate = new RestTemplate(Arrays.asList( + new FormHttpMessageConverter(), + new OAuth2AccessTokenResponseHttpMessageConverter() + )); + ClientHttpRequestFactory requestFactory = restTemplate.getRequestFactory(); + if (requestFactory instanceof SimpleClientHttpRequestFactory simple) { + simple.setConnectTimeout(30_000); + simple.setReadTimeout(30_000); + } + return restTemplate; + } +} diff --git a/src/main/java/com/imgfloat/app/config/SecurityConfig.java b/src/main/java/com/imgfloat/app/config/SecurityConfig.java index f16940a..1b90631 100644 --- a/src/main/java/com/imgfloat/app/config/SecurityConfig.java +++ b/src/main/java/com/imgfloat/app/config/SecurityConfig.java @@ -2,11 +2,14 @@ package com.imgfloat.app.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.client.RestTemplate; @Configuration @EnableWebSecurity @@ -21,9 +24,20 @@ public class SecurityConfig { .requestMatchers("/ws/**").permitAll() .anyRequest().authenticated() ) - .oauth2Login(Customizer.withDefaults()) + .oauth2Login(oauth -> oauth + .tokenEndpoint(token -> token.accessTokenResponseClient(twitchAccessTokenResponseClient())) + ) .logout(logout -> logout.logoutSuccessUrl("/").permitAll()) .csrf(csrf -> csrf.ignoringRequestMatchers("/ws/**", "/api/**")); return http.build(); } + + @Bean + OAuth2AccessTokenResponseClient twitchAccessTokenResponseClient() { + DefaultAuthorizationCodeTokenResponseClient delegate = new DefaultAuthorizationCodeTokenResponseClient(); + RestTemplate restTemplate = OAuth2RestTemplateFactory.create(); + restTemplate.setErrorHandler(new TwitchOAuth2ErrorResponseErrorHandler()); + delegate.setRestOperations(restTemplate); + return delegate; + } } diff --git a/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java b/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java new file mode 100644 index 0000000..a1223d3 --- /dev/null +++ b/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java @@ -0,0 +1,39 @@ +package com.imgfloat.app.config; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; +import org.springframework.util.StreamUtils; + +/** + * Twitch occasionally returns error payloads without an {@code error} code field. The default + * {@link OAuth2ErrorHttpMessageConverter} refuses to deserialize such payloads and throws an + * {@link HttpMessageNotReadableException}. That propagates up as a 500 before we can surface a + * meaningful login failure to the user. This handler falls back to a safe, synthetic + * {@link OAuth2Error} so the login flow can fail gracefully. + */ +class TwitchOAuth2ErrorResponseErrorHandler extends OAuth2ErrorResponseErrorHandler { + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + try { + super.handleError(response); + } catch (HttpMessageNotReadableException ex) { + throw asAuthorizationException(response, ex); + } + } + + private OAuth2AuthorizationException asAuthorizationException(ClientHttpResponse response, + HttpMessageNotReadableException ex) throws IOException { + String body = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); + String description = "Failed to parse Twitch OAuth error response" + (body.isBlank() ? "." : ": " + body); + OAuth2Error oauth2Error = new OAuth2Error("invalid_token_response", description, null); + return new OAuth2AuthorizationException(oauth2Error, ex); + } +}