diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ddf8cff..7e43031 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -15,12 +15,9 @@ - - - - - - + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index c4be380..792bbc5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,7 +11,7 @@ - + diff --git a/build.gradle b/build.gradle index 63d550e..261b0c7 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ dependencies { implementation("org.jetbrains:annotations:26.0.1") implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter-validation") - implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.6.8") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.3.5") implementation("javax.inject:javax.inject:1") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") diff --git a/src/main/java/com/faforever/moderatorclient/api/FafApiCommunicationService.java b/src/main/java/com/faforever/moderatorclient/api/FafApiCommunicationService.java index 764a5c2..e771c3a 100644 --- a/src/main/java/com/faforever/moderatorclient/api/FafApiCommunicationService.java +++ b/src/main/java/com/faforever/moderatorclient/api/FafApiCommunicationService.java @@ -27,7 +27,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.JdkClientHttpRequestFactory; -import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; @@ -124,7 +123,7 @@ public void authorize(HydraAuthorizedEvent event) { try { meResult = getOne("/me", MeResult.class); - } catch (OAuth2AccessDeniedException e) { + } catch (Exception e) { log.error("login failed", e); return; } @@ -220,19 +219,16 @@ public void delete(ElideNavigatorOnId navigator) { } } - @SuppressWarnings("unchecked") @SneakyThrows public T getOne(ElideNavigatorOnId navigator) { return getOne(navigator.build(), navigator.getDtoClass(), Collections.emptyMap()); } - @SuppressWarnings("unchecked") @SneakyThrows public T getOne(String endpointPath, Class type) { return getOne(endpointPath, type, Collections.emptyMap()); } - @SuppressWarnings("unchecked") @SneakyThrows public T getOne(String endpointPath, Class type, java.util.Map params) { cycleAvoidingMappingContext.clearCache(); diff --git a/src/main/java/com/faforever/moderatorclient/api/TokenService.java b/src/main/java/com/faforever/moderatorclient/api/TokenService.java index 25353d7..7bae466 100644 --- a/src/main/java/com/faforever/moderatorclient/api/TokenService.java +++ b/src/main/java/com/faforever/moderatorclient/api/TokenService.java @@ -1,77 +1,126 @@ package com.faforever.moderatorclient.api; import com.faforever.moderatorclient.api.event.HydraAuthorizedEvent; -import com.faforever.moderatorclient.api.event.TokenExpiredEvent; import com.faforever.moderatorclient.config.EnvironmentProperties; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.http.client.JdkClientHttpRequestFactory; -import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; +import java.time.Duration; +import java.time.Instant; import java.util.List; +import java.util.Map; @Service @Slf4j public class TokenService { - private final ApplicationEventPublisher applicationEventPublisher; - private RestTemplate restTemplate; - private EnvironmentProperties environmentProperties; - private OAuth2AccessToken tokenCache; - - public TokenService(ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - public void prepare(EnvironmentProperties environmentProperties) { - this.environmentProperties = environmentProperties; - this.restTemplate = new RestTemplateBuilder() - .requestFactory(JdkClientHttpRequestFactory.class) - .rootUri(environmentProperties.getOauthBaseUrl()) - .build(); - } - - @SneakyThrows - public String getRefreshedTokenValue() { - if (tokenCache == null || tokenCache.isExpired()) { - log.info("Token expired, requesting new login"); - applicationEventPublisher.publishEvent(new TokenExpiredEvent()); - } else { - log.debug("Token still valid for {} seconds", tokenCache.getExpiresIn()); + private final ApplicationEventPublisher applicationEventPublisher; + private RestTemplate restTemplate; + private EnvironmentProperties environmentProperties; + private OAuth2AccessTokenResponse tokenCache; + + public TokenService(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + public void prepare(EnvironmentProperties environmentProperties) { + this.environmentProperties = environmentProperties; + this.restTemplate = new RestTemplateBuilder() + .requestFactory(JdkClientHttpRequestFactory.class) + .rootUri(environmentProperties.getOauthBaseUrl()) + .build(); + } + + @SneakyThrows + public String getRefreshedTokenValue() { + if (tokenCache.getAccessToken().getExpiresAt().isBefore(Instant.now())) { + log.info("Token expired, requesting new with refresh token"); + loginWithRefreshToken(tokenCache.getRefreshToken().getTokenValue(), false); + } else { + log.debug("Token still valid for {} seconds", Duration.between(Instant.now(), tokenCache.getAccessToken().getExpiresAt())); + } + + return tokenCache.getAccessToken().getTokenValue(); + } + + public void loginWithAuthorizationCode(String code) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("code", code); + map.add("client_id", environmentProperties.getClientId()); + map.add("redirect_uri", environmentProperties.getOauthRedirectUrl()); + map.add("grant_type", "authorization_code"); + + Map responseBody = requestToken(headers, map); + if (responseBody != null) { + parseResponse(responseBody); + + applicationEventPublisher.publishEvent(new HydraAuthorizedEvent()); + } + } - return tokenCache.getValue(); - } + private void parseResponse(Map responseBody) { + String accessToken = (String) responseBody.get("access_token"); + String refreshToken = (String) responseBody.get("refresh_token"); + Long expiresIn = Long.valueOf(responseBody.get("expires_in").toString()); + + tokenCache = OAuth2AccessTokenResponse.withToken(accessToken) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .refreshToken(refreshToken) + .expiresIn(expiresIn) + .build(); + } - public void loginWithAuthorizationCode(String code) { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.setAccept(List.of(MediaType.APPLICATION_JSON_UTF8)); + public void loginWithRefreshToken(String refreshToken, boolean fireEvent) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("code", code); - map.add("client_id", environmentProperties.getClientId()); - map.add("redirect_uri", environmentProperties.getOauthRedirectUrl()); - map.add("grant_type", "authorization_code"); + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("refresh_token", refreshToken); + map.add("client_id", environmentProperties.getClientId()); + map.add("grant_type", "refresh_token"); + + Map responseBody = requestToken(headers, map); + + if (responseBody != null) { + parseResponse(responseBody); + + if (fireEvent) { + applicationEventPublisher.publishEvent(new HydraAuthorizedEvent()); + } + } + } - HttpEntity> request = new HttpEntity<>(map, headers); + private Map requestToken(HttpHeaders headers, MultiValueMap map) { + HttpEntity> request = new HttpEntity<>(map, headers); - tokenCache = restTemplate.postForObject( - "/oauth2/token", - request, - OAuth2AccessToken.class - ); + ResponseEntity> responseEntity = restTemplate.exchange( + "/oauth2/token", + HttpMethod.POST, + request, + new ParameterizedTypeReference<>() { + } + ); - if (tokenCache != null) { - applicationEventPublisher.publishEvent(new HydraAuthorizedEvent()); + return responseEntity.getBody(); } - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 78f69cf..e003553 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,7 +13,7 @@ faforever: replay-download-url-format: https://replay.faforever.com/%s oauth-base-url: https://hydra.faforever.com oauth-redirect-url: http://localhost - oauth-scopes: upload_avatar administrative_actions read_sensible_userdata manage_vault + oauth-scopes: offline upload_avatar administrative_actions read_sensible_userdata manage_vault user-base-url: https://user.faforever.com "[test.faforever.com]": base-url: https://api.test.faforever.com @@ -21,7 +21,7 @@ faforever: replay-download-url-format: https://replay.test.faforever.com/%s oauth-base-url: https://hydra.test.faforever.com oauth-redirect-url: http://localhost - oauth-scopes: upload_avatar administrative_actions read_sensible_userdata manage_vault + oauth-scopes: offline upload_avatar administrative_actions read_sensible_userdata manage_vault user-base-url: https://user.test.faforever.com "[localhost:8010]": base-url: http://127.0.0.1:8010 @@ -29,7 +29,7 @@ faforever: replay-download-url-format: https://replay.test.faforever.com/%s oauth-base-url: http://localhost:4444 oauth-redirect-url: http://127.0.0.1 - oauth-scopes: upload_avatar administrative_actions read_sensible_userdata manage_vault + oauth-scopes: offline upload_avatar administrative_actions read_sensible_userdata manage_vault user-base-url: http://localhost:8080 logging: