mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Fix login
This commit is contained in:
@@ -26,6 +26,7 @@ public class SecurityConfig {
|
|||||||
)
|
)
|
||||||
.oauth2Login(oauth -> oauth
|
.oauth2Login(oauth -> oauth
|
||||||
.tokenEndpoint(token -> token.accessTokenResponseClient(twitchAccessTokenResponseClient()))
|
.tokenEndpoint(token -> token.accessTokenResponseClient(twitchAccessTokenResponseClient()))
|
||||||
|
.userInfoEndpoint(user -> user.userService(twitchOAuth2UserService()))
|
||||||
)
|
)
|
||||||
.logout(logout -> logout.logoutSuccessUrl("/").permitAll())
|
.logout(logout -> logout.logoutSuccessUrl("/").permitAll())
|
||||||
.csrf(csrf -> csrf.ignoringRequestMatchers("/ws/**", "/api/**"));
|
.csrf(csrf -> csrf.ignoringRequestMatchers("/ws/**", "/api/**"));
|
||||||
@@ -35,9 +36,15 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> twitchAccessTokenResponseClient() {
|
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> twitchAccessTokenResponseClient() {
|
||||||
DefaultAuthorizationCodeTokenResponseClient delegate = new DefaultAuthorizationCodeTokenResponseClient();
|
DefaultAuthorizationCodeTokenResponseClient delegate = new DefaultAuthorizationCodeTokenResponseClient();
|
||||||
|
delegate.setRequestEntityConverter(new TwitchAuthorizationCodeGrantRequestEntityConverter());
|
||||||
RestTemplate restTemplate = OAuth2RestTemplateFactory.create();
|
RestTemplate restTemplate = OAuth2RestTemplateFactory.create();
|
||||||
restTemplate.setErrorHandler(new TwitchOAuth2ErrorResponseErrorHandler());
|
restTemplate.setErrorHandler(new TwitchOAuth2ErrorResponseErrorHandler());
|
||||||
delegate.setRestOperations(restTemplate);
|
delegate.setRestOperations(restTemplate);
|
||||||
return delegate;
|
return delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
TwitchOAuth2UserService twitchOAuth2UserService() {
|
||||||
|
return new TwitchOAuth2UserService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.imgfloat.app.config;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures Twitch token requests always include {@code client_id} and {@code client_secret} in the
|
||||||
|
* request body. Twitch ignores HTTP Basic authentication and responds with "missing client id" if
|
||||||
|
* those parameters are absent.
|
||||||
|
*/
|
||||||
|
final class TwitchAuthorizationCodeGrantRequestEntityConverter implements
|
||||||
|
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
|
||||||
|
|
||||||
|
private final Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> delegate =
|
||||||
|
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest request) {
|
||||||
|
RequestEntity<?> entity = delegate.convert(request);
|
||||||
|
if (entity == null || !(entity.getBody() instanceof MultiValueMap<?, ?> existingBody)) {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientRegistration registration = request.getClientRegistration();
|
||||||
|
|
||||||
|
MultiValueMap<String, String> body = cloneBody(existingBody);
|
||||||
|
body.set(OAuth2ParameterNames.CLIENT_ID, registration.getClientId());
|
||||||
|
if (registration.getClientSecret() != null) {
|
||||||
|
body.set(OAuth2ParameterNames.CLIENT_SECRET, registration.getClientSecret());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RequestEntity<>(
|
||||||
|
body,
|
||||||
|
entity.getHeaders(),
|
||||||
|
entity.getMethod() == null ? HttpMethod.POST : entity.getMethod(),
|
||||||
|
entity.getUrl() == null ? URI.create(registration.getProviderDetails().getTokenUri()) : entity.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MultiValueMap<String, String> cloneBody(MultiValueMap<?, ?> existingBody) {
|
||||||
|
MultiValueMap<String, String> copy = new LinkedMultiValueMap<>();
|
||||||
|
existingBody.forEach((key, value) ->
|
||||||
|
copy.put(String.valueOf(key), new ArrayList<>((List<String>) value)));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.imgfloat.app.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes Twitch client registration values (especially whitespace or quoted secrets) before
|
||||||
|
* wiring them into Spring Security.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(OAuth2ClientProperties.class)
|
||||||
|
class TwitchClientRegistrationConfig {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(TwitchClientRegistrationConfig.class);
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
ClientRegistrationRepository twitchClientRegistrationRepository(OAuth2ClientProperties properties) {
|
||||||
|
List<ClientRegistration> registrations = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, OAuth2ClientProperties.Registration> entry : properties.getRegistration().entrySet()) {
|
||||||
|
String registrationId = entry.getKey();
|
||||||
|
OAuth2ClientProperties.Registration registration = entry.getValue();
|
||||||
|
String providerId = registration.getProvider() != null ? registration.getProvider() : registrationId;
|
||||||
|
OAuth2ClientProperties.Provider provider = properties.getProvider().get(providerId);
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Missing OAuth2 provider configuration for registration '" + registrationId + "'.");
|
||||||
|
}
|
||||||
|
if (!"twitch".equals(registrationId)) {
|
||||||
|
LOG.warn("Unexpected OAuth2 registration '{}' found; only Twitch is supported.", registrationId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
registrations.add(buildTwitchRegistration(registrationId, registration, provider));
|
||||||
|
}
|
||||||
|
return new InMemoryClientRegistrationRepository(registrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration buildTwitchRegistration(
|
||||||
|
String registrationId,
|
||||||
|
OAuth2ClientProperties.Registration registration,
|
||||||
|
OAuth2ClientProperties.Provider provider) {
|
||||||
|
String clientId = sanitize(registration.getClientId(), "TWITCH_CLIENT_ID");
|
||||||
|
String clientSecret = sanitize(registration.getClientSecret(), "TWITCH_CLIENT_SECRET");
|
||||||
|
return ClientRegistration.withRegistrationId(registrationId)
|
||||||
|
.clientName(registration.getClientName())
|
||||||
|
.clientId(clientId)
|
||||||
|
.clientSecret(clientSecret)
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
|
||||||
|
.authorizationGrantType(new AuthorizationGrantType(registration.getAuthorizationGrantType()))
|
||||||
|
.redirectUri(registration.getRedirectUri())
|
||||||
|
.scope(registration.getScope())
|
||||||
|
.authorizationUri(provider.getAuthorizationUri())
|
||||||
|
.tokenUri(provider.getTokenUri())
|
||||||
|
.userInfoUri(provider.getUserInfoUri())
|
||||||
|
.userNameAttributeName(provider.getUserNameAttribute())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitize(String value, String name) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = value.trim();
|
||||||
|
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||||
|
String unquoted = trimmed.substring(1, trimmed.length() - 1).trim();
|
||||||
|
LOG.info("Sanitizing {} by stripping surrounding quotes.", name);
|
||||||
|
return unquoted;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.imgfloat.app.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
|
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||||
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the Twitch required "Client-ID" header to user info requests while preserving the default
|
||||||
|
* Spring behavior for mapping the response into an {@link OAuth2User}.
|
||||||
|
*/
|
||||||
|
class TwitchOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
|
||||||
|
|
||||||
|
private final Function<OAuth2UserRequest, RestTemplate> restTemplateFactory;
|
||||||
|
|
||||||
|
TwitchOAuth2UserService() {
|
||||||
|
this(TwitchOAuth2UserService::createRestTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchOAuth2UserService(Function<OAuth2UserRequest, RestTemplate> restTemplateFactory) {
|
||||||
|
this.restTemplateFactory = restTemplateFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
|
||||||
|
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
|
||||||
|
delegate.setRestOperations(restTemplateFactory.apply(userRequest));
|
||||||
|
OAuth2User delegateUser = delegate.loadUser(twitchUserRequest(userRequest));
|
||||||
|
Map<String, Object> twitchUser = unwrapUserAttributes(delegateUser.getAttributes());
|
||||||
|
return new DefaultOAuth2User(delegateUser.getAuthorities(), twitchUser, "login");
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2UserRequest twitchUserRequest(OAuth2UserRequest userRequest) {
|
||||||
|
return new OAuth2UserRequest(
|
||||||
|
twitchUserRegistration(userRequest),
|
||||||
|
userRequest.getAccessToken(),
|
||||||
|
userRequest.getAdditionalParameters());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration twitchUserRegistration(OAuth2UserRequest userRequest) {
|
||||||
|
ClientRegistration registration = userRequest.getClientRegistration();
|
||||||
|
return ClientRegistration.withClientRegistration(registration)
|
||||||
|
// The Twitch response nests user details under a "data" array, so accept that
|
||||||
|
// shape for the initial parsing step.
|
||||||
|
.userNameAttributeName("data")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> unwrapUserAttributes(Map<String, Object> responseAttributes) {
|
||||||
|
Object data = responseAttributes.get("data");
|
||||||
|
if (!(data instanceof List<?> list) || list.isEmpty() || !(list.get(0) instanceof Map<?, ?> first)) {
|
||||||
|
throw invalidUserInfo("Missing Twitch user array in user info response");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> twitchUser = new HashMap<>();
|
||||||
|
first.forEach((key, value) -> twitchUser.put(String.valueOf(key), value));
|
||||||
|
|
||||||
|
Object login = twitchUser.get("login");
|
||||||
|
if (!(login instanceof String loginValue) || loginValue.isBlank()) {
|
||||||
|
throw invalidUserInfo("Missing Twitch login in user info response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return twitchUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2AuthenticationException invalidUserInfo(String message) {
|
||||||
|
return new OAuth2AuthenticationException(new OAuth2Error("invalid_user_info_response", message, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
static RestTemplate createRestTemplate(OAuth2UserRequest userRequest) {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
restTemplate.setRequestFactory(createRequestFactory(restTemplate.getRequestFactory()));
|
||||||
|
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(restTemplate.getInterceptors());
|
||||||
|
interceptors.add((request, body, execution) -> {
|
||||||
|
request.getHeaders().add("Client-ID", userRequest.getClientRegistration().getClientId());
|
||||||
|
return execution.execute(request, body);
|
||||||
|
});
|
||||||
|
restTemplate.setInterceptors(interceptors);
|
||||||
|
restTemplate.setErrorHandler(new TwitchOAuth2ErrorResponseErrorHandler());
|
||||||
|
return restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClientHttpRequestFactory createRequestFactory(ClientHttpRequestFactory existing) {
|
||||||
|
if (existing instanceof SimpleClientHttpRequestFactory simple) {
|
||||||
|
simple.setConnectTimeout(30_000);
|
||||||
|
simple.setReadTimeout(30_000);
|
||||||
|
return simple;
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ spring:
|
|||||||
twitch:
|
twitch:
|
||||||
client-id: ${TWITCH_CLIENT_ID}
|
client-id: ${TWITCH_CLIENT_ID}
|
||||||
client-secret: ${TWITCH_CLIENT_SECRET}
|
client-secret: ${TWITCH_CLIENT_SECRET}
|
||||||
|
client-authentication-method: client_secret_post
|
||||||
redirect-uri: "${TWITCH_REDIRECT_URI:{baseUrl}/login/oauth2/code/twitch}"
|
redirect-uri: "${TWITCH_REDIRECT_URI:{baseUrl}/login/oauth2/code/twitch}"
|
||||||
authorization-grant-type: authorization_code
|
authorization-grant-type: authorization_code
|
||||||
scope: ["user:read:email"]
|
scope: ["user:read:email"]
|
||||||
@@ -28,7 +29,7 @@ spring:
|
|||||||
authorization-uri: https://id.twitch.tv/oauth2/authorize
|
authorization-uri: https://id.twitch.tv/oauth2/authorize
|
||||||
token-uri: https://id.twitch.tv/oauth2/token
|
token-uri: https://id.twitch.tv/oauth2/token
|
||||||
user-info-uri: https://api.twitch.tv/helix/users
|
user-info-uri: https://api.twitch.tv/helix/users
|
||||||
user-name-attribute: preferred_username
|
user-name-attribute: login
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
|
|||||||
@@ -37,4 +37,28 @@ class TwitchEnvironmentValidationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stripsQuotesAndWhitespaceFromCredentials() {
|
||||||
|
ConfigurableApplicationContext context = null;
|
||||||
|
try {
|
||||||
|
context = new SpringApplicationBuilder(ImgfloatApplication.class)
|
||||||
|
.properties(
|
||||||
|
"server.port=0",
|
||||||
|
"TWITCH_CLIENT_ID=\" quoted-id \"",
|
||||||
|
"TWITCH_CLIENT_SECRET=' quoted-secret '"
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
var clientRegistrationRepository = context.getBean(org.springframework.security.oauth2.client.registration.ClientRegistrationRepository.class);
|
||||||
|
var twitch = clientRegistrationRepository.findByRegistrationId("twitch");
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(twitch.getClientId()).isEqualTo("quoted-id");
|
||||||
|
org.assertj.core.api.Assertions.assertThat(twitch.getClientSecret()).isEqualTo("quoted-secret");
|
||||||
|
} finally {
|
||||||
|
if (context != null) {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.imgfloat.app.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class TwitchAuthorizationCodeGrantRequestEntityConverterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addsClientIdAndSecretToTokenRequestBody() {
|
||||||
|
ClientRegistration registration = ClientRegistration.withRegistrationId("twitch")
|
||||||
|
.clientId("twitch-id")
|
||||||
|
.clientSecret("twitch-secret")
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.redirectUri("https://example.com/redirect")
|
||||||
|
.scope("user:read:email")
|
||||||
|
.authorizationUri("https://id.twitch.tv/oauth2/authorize")
|
||||||
|
.tokenUri("https://id.twitch.tv/oauth2/token")
|
||||||
|
.userInfoUri("https://api.twitch.tv/helix/users")
|
||||||
|
.userNameAttributeName("preferred_username")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||||
|
.authorizationUri(registration.getProviderDetails().getAuthorizationUri())
|
||||||
|
.clientId(registration.getClientId())
|
||||||
|
.redirectUri(registration.getRedirectUri())
|
||||||
|
.state("state")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse.success("code")
|
||||||
|
.redirectUri(registration.getRedirectUri())
|
||||||
|
.state("state")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||||
|
OAuth2AuthorizationCodeGrantRequest grantRequest = new OAuth2AuthorizationCodeGrantRequest(registration, exchange);
|
||||||
|
|
||||||
|
var converter = new TwitchAuthorizationCodeGrantRequestEntityConverter();
|
||||||
|
RequestEntity<?> requestEntity = converter.convert(grantRequest);
|
||||||
|
|
||||||
|
MultiValueMap<String, String> body = (MultiValueMap<String, String>) requestEntity.getBody();
|
||||||
|
|
||||||
|
assertThat(body.getFirst(OAuth2ParameterNames.CLIENT_ID)).isEqualTo("twitch-id");
|
||||||
|
assertThat(body.getFirst(OAuth2ParameterNames.CLIENT_SECRET)).isEqualTo("twitch-secret");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.imgfloat.app.config;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||||
|
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
class TwitchOAuth2UserServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unwrapsTwitchUserAndAddsClientIdHeaderToUserInfoRequest() {
|
||||||
|
ClientRegistration registration = twitchRegistrationBuilder()
|
||||||
|
.clientId("client-123")
|
||||||
|
.clientSecret("secret")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OAuth2UserRequest userRequest = userRequest(registration);
|
||||||
|
RestTemplate restTemplate = TwitchOAuth2UserService.createRestTemplate(userRequest);
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
|
|
||||||
|
TwitchOAuth2UserService service = new TwitchOAuth2UserService(ignored -> restTemplate);
|
||||||
|
|
||||||
|
server.expect(requestTo("https://api.twitch.tv/helix/users"))
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header("Client-ID", "client-123"))
|
||||||
|
.andRespond(withSuccess(
|
||||||
|
"{\"data\":[{\"id\":\"42\",\"login\":\"demo\",\"display_name\":\"Demo\"}]}",
|
||||||
|
MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
OAuth2User user = service.loadUser(userRequest);
|
||||||
|
|
||||||
|
assertThat(user.getName()).isEqualTo("demo");
|
||||||
|
assertThat(user.getAttributes())
|
||||||
|
.containsEntry("id", "42")
|
||||||
|
.containsEntry("display_name", "Demo");
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2UserRequest userRequest(ClientRegistration registration) {
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||||
|
OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
"token",
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plusSeconds(60),
|
||||||
|
Set.of("user:read:email"));
|
||||||
|
return new OAuth2UserRequest(registration, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration.Builder twitchRegistrationBuilder() {
|
||||||
|
return ClientRegistration.withRegistrationId("twitch")
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
|
||||||
|
.clientName("Twitch")
|
||||||
|
.redirectUri("https://example.com/login/oauth2/code/twitch")
|
||||||
|
.authorizationUri("https://id.twitch.tv/oauth2/authorize")
|
||||||
|
.tokenUri("https://id.twitch.tv/oauth2/token")
|
||||||
|
.userInfoUri("https://api.twitch.tv/helix/users")
|
||||||
|
.userNameAttributeName("login");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user