diff --git a/server/pom.xml b/server/pom.xml index 465200d..e360256 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -126,6 +126,40 @@ + + org.openapitools + openapi-generator-maven-plugin + 6.3.0 + + + + generate + + + ${project.basedir}/src/main/resources/withings.yaml + java + + src/gen/java/main + java8 + true + true + true + native + true + false + @lombok.Builder @lombok.AllArgsConstructor + + false + false + false + false + false + false + mucsi96.traininglog.withings + + + + diff --git a/server/src/main/java/mucsi96/traininglog/withings/WithingsConfiguration.java b/server/src/main/java/mucsi96/traininglog/withings/WithingsConfiguration.java index 907e58c..135ebb9 100644 --- a/server/src/main/java/mucsi96/traininglog/withings/WithingsConfiguration.java +++ b/server/src/main/java/mucsi96/traininglog/withings/WithingsConfiguration.java @@ -1,16 +1,163 @@ package mucsi96.traininglog.withings; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.mucsi96.kubetools.security.KubetoolsSecurityConfigurer; import lombok.Data; @Data @Configuration @ConfigurationProperties(prefix = "withings") public class WithingsConfiguration { + public static final String registrationId = "withings-client"; + private WithingsApiConfiguration api; + @Bean + SecurityFilterChain withingsSecurityFilterChain( + HttpSecurity http, + KubetoolsSecurityConfigurer kubetoolsSecurityConfigurer) throws Exception { + return kubetoolsSecurityConfigurer.configure(http) + .securityMatcher("/withings/**") + .oauth2Client(configurer -> configurer + .authorizationCodeGrant(customizer -> customizer + .accessTokenResponseClient(withingsAccessTokenResponseClient()))) + .build(); + } + + @Bean + OAuth2AuthorizedClientManager withingsAuthorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + + OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken(configurer -> configurer.accessTokenResponseClient(withingsRefreshTokenResponseClient())) + .build(); + + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; + } + + OAuth2AccessTokenResponseClient withingsAccessTokenResponseClient() { + OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter(); + requestEntityConverter.addParametersConverter(withingsAccessTokenRequestParametersConverter()); + + OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + accessTokenResponseConverter.setAccessTokenResponseConverter(withingsAccessTokenResponseConverter()); + + RestTemplate restTemplate = new RestTemplate(Arrays.asList( + new FormHttpMessageConverter(), + accessTokenResponseConverter)); + + restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + + DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); + accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter); + accessTokenResponseClient.setRestOperations(restTemplate); + return accessTokenResponseClient; + } + + OAuth2AccessTokenResponseClient withingsRefreshTokenResponseClient() { + OAuth2RefreshTokenGrantRequestEntityConverter requestEntityConverter = new OAuth2RefreshTokenGrantRequestEntityConverter(); + requestEntityConverter.addParametersConverter(withingsRefreshTokenRequestParametersConverter()); + + OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + accessTokenResponseConverter.setAccessTokenResponseConverter(withingsAccessTokenResponseConverter()); + + RestTemplate restTemplate = new RestTemplate(Arrays.asList( + new FormHttpMessageConverter(), + accessTokenResponseConverter)); + + restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + + DefaultRefreshTokenTokenResponseClient tokenResponseClient = new DefaultRefreshTokenTokenResponseClient(); + tokenResponseClient.setRequestEntityConverter(requestEntityConverter); + tokenResponseClient.setRestOperations(restTemplate); + return tokenResponseClient; + } + + Converter> withingsAccessTokenRequestParametersConverter() { + return request -> { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("action", "requesttoken"); + parameters.add(OAuth2ParameterNames.CLIENT_ID, request.getClientRegistration().getClientId()); + parameters.add(OAuth2ParameterNames.CLIENT_SECRET, request.getClientRegistration().getClientSecret()); + return parameters; + }; + } + + Converter, OAuth2AccessTokenResponse> withingsAccessTokenResponseConverter() { + return rawResponse -> { + ObjectMapper mapper = new ObjectMapper(); + WithingsGetAccessTokenResponse response = mapper.convertValue(rawResponse, new TypeReference<>() { + }); + + if (response.getStatus() != 0) { + throw new WithingsTechnicalException(); + } + + WithingsGetAccessTokenResponseBody body = response.getBody(); + + return OAuth2AccessTokenResponse + .withToken(body.getAccessToken()) + .refreshToken(body.getRefreshToken()) + .expiresIn(body.getExpiresIn()) + .scopes(Set.of(body.getScope())) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Map.of("userId", body.getUserid())) + .build(); + }; + } + + Converter> withingsRefreshTokenRequestParametersConverter() { + return request -> { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("action", "requesttoken"); + parameters.add(OAuth2ParameterNames.CLIENT_ID, request.getClientRegistration().getClientId()); + parameters.add(OAuth2ParameterNames.CLIENT_SECRET, request.getClientRegistration().getClientSecret()); + return parameters; + }; + } + @Data public static class WithingsApiConfiguration { private String uri; diff --git a/server/src/main/java/mucsi96/traininglog/withings/WithingsController.java b/server/src/main/java/mucsi96/traininglog/withings/WithingsController.java index a820d44..6cd1e1f 100644 --- a/server/src/main/java/mucsi96/traininglog/withings/WithingsController.java +++ b/server/src/main/java/mucsi96/traininglog/withings/WithingsController.java @@ -16,7 +16,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import mucsi96.traininglog.weight.WeightService; -import mucsi96.traininglog.withings.oauth.WithingsClient; @RestController @RequestMapping("/withings") @@ -57,7 +56,7 @@ public RedirectView authorize( private OAuth2AuthorizedClient getAuthorizedClient(Authentication principal, HttpServletRequest servletRequest, HttpServletResponse servletResponse) { OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(WithingsClient.id) + .withClientRegistrationId(WithingsConfiguration.registrationId) .principal(principal) .attribute(HttpServletRequest.class.getName(), servletRequest) .attribute(HttpServletResponse.class.getName(), servletResponse) diff --git a/server/src/main/java/mucsi96/traininglog/withings/WithingsService.java b/server/src/main/java/mucsi96/traininglog/withings/WithingsService.java index 3109570..6aa4ddd 100644 --- a/server/src/main/java/mucsi96/traininglog/withings/WithingsService.java +++ b/server/src/main/java/mucsi96/traininglog/withings/WithingsService.java @@ -18,11 +18,6 @@ import lombok.RequiredArgsConstructor; import mucsi96.traininglog.weight.Weight; -import mucsi96.traininglog.withings.data.GetMeasureResponse; -import mucsi96.traininglog.withings.data.GetMeasureResponseBody; -import mucsi96.traininglog.withings.data.Measure; -import mucsi96.traininglog.withings.data.MeasureGroup; -import mucsi96.traininglog.withings.oauth.WithingsClient; @Service @RequiredArgsConstructor @@ -46,20 +41,20 @@ private String getMeasureUrl() { .toUriString(); } - private GetMeasureResponseBody getMeasure(OAuth2AuthorizedClient authorizedClient) { + private WithingsGetMeasureResponseBody getMeasure(OAuth2AuthorizedClient authorizedClient) { HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()); HttpEntity request = new HttpEntity<>("", headers); RestTemplate restTemplate = new RestTemplate(); - GetMeasureResponse response = restTemplate - .postForObject(getMeasureUrl(), request, GetMeasureResponse.class); + WithingsGetMeasureResponse response = restTemplate + .postForObject(getMeasureUrl(), request, WithingsGetMeasureResponse.class); if (response == null) { throw new WithingsTechnicalException(); } if (response.getStatus() == 401) { - throw new ClientAuthorizationRequiredException(WithingsClient.id); + throw new ClientAuthorizationRequiredException(WithingsConfiguration.registrationId); } if (response.getStatus() != 0) { @@ -69,22 +64,22 @@ private GetMeasureResponseBody getMeasure(OAuth2AuthorizedClient authorizedClien return response.getBody(); } - private Optional getFirstMeasureValue(GetMeasureResponseBody measureResponseBody) { - List measureGroups = measureResponseBody.getMeasureGroups(); + private Optional getFirstMeasureValue(WithingsGetMeasureResponseBody measureResponseBody) { + List measureGroups = measureResponseBody.getMeasuregrps(); if (measureGroups == null || measureGroups.isEmpty()) { return Optional.empty(); } - MeasureGroup measureGroup = measureGroups.get(0); + WithingsMeasureGroup measureGroup = measureGroups.get(0); - List measures = measureGroup.getMeasures(); + List measures = measureGroup.getMeasures(); if (measures == null || measures.isEmpty()) { return Optional.empty(); } - Measure measure = measures.get(0); + WithingsMeasure measure = measures.get(0); double weight = measure.getValue() * Math.pow(10, measure.getUnit()); return Optional.of(Weight.builder().value(weight).createdAt(Instant.ofEpochSecond(measureGroup.getDate())).build()); diff --git a/server/src/main/java/mucsi96/traininglog/withings/data/GetAccessTokenResponseBody.java b/server/src/main/java/mucsi96/traininglog/withings/data/GetAccessTokenResponseBody.java deleted file mode 100644 index 49a821d..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/data/GetAccessTokenResponseBody.java +++ /dev/null @@ -1,30 +0,0 @@ -package mucsi96.traininglog.withings.data; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -import java.io.Serializable; - -@Data -public class GetAccessTokenResponseBody implements Serializable { - @JsonProperty("userid") - private String userId; - - @JsonProperty("access_token") - private String accessToken; - - @JsonProperty("refresh_token") - private String refreshToken; - - @JsonProperty("expires_in") - private int expiresIn; - - @JsonProperty("scope") - private String scope; - - @JsonProperty("csrf_token") - private String csrfToken; - - @JsonProperty("token_type") - private String tokenType; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/data/GetMeasureResponse.java b/server/src/main/java/mucsi96/traininglog/withings/data/GetMeasureResponse.java deleted file mode 100644 index 152ba88..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/data/GetMeasureResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package mucsi96.traininglog.withings.data; - -import lombok.Data; - -@Data -public class GetMeasureResponse { - int status; - GetMeasureResponseBody body; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/data/GetMeasureResponseBody.java b/server/src/main/java/mucsi96/traininglog/withings/data/GetMeasureResponseBody.java deleted file mode 100644 index ea04d59..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/data/GetMeasureResponseBody.java +++ /dev/null @@ -1,17 +0,0 @@ -package mucsi96.traininglog.withings.data; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -import java.util.List; - -@Data -public class GetMeasureResponseBody { - @JsonProperty("updatetime") - String updateTime; - String timezone; - @JsonProperty("measuregrps") - List measureGroups; - int more; - int offset; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/data/Measure.java b/server/src/main/java/mucsi96/traininglog/withings/data/Measure.java deleted file mode 100644 index 795e9e8..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/data/Measure.java +++ /dev/null @@ -1,12 +0,0 @@ -package mucsi96.traininglog.withings.data; - -import lombok.Data; - -import java.io.Serializable; - -@Data -public class Measure implements Serializable { - private int value; - private int type; - private int unit; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/data/MeasureGroup.java b/server/src/main/java/mucsi96/traininglog/withings/data/MeasureGroup.java deleted file mode 100644 index dc767ef..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/data/MeasureGroup.java +++ /dev/null @@ -1,22 +0,0 @@ -package mucsi96.traininglog.withings.data; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -import java.io.Serializable; -import java.util.List; - -@Data -public class MeasureGroup implements Serializable { - @JsonProperty("grpid") - private long grpId; - private int attrib; - private int date; - private int created; - private int category; - @JsonProperty("deviceid") - private String deviceId; - private List measures; - private String comment; - private String timezone; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/data/WithingsResponse.java b/server/src/main/java/mucsi96/traininglog/withings/data/WithingsResponse.java deleted file mode 100644 index 864f4ec..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/data/WithingsResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package mucsi96.traininglog.withings.data; - -import lombok.Data; - -import java.io.Serializable; - -@Data -public class WithingsResponse implements Serializable { - private int status; - private T body; - private String error; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenRequestParametersConverter.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenRequestParametersConverter.java deleted file mode 100644 index 319cf99..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenRequestParametersConverter.java +++ /dev/null @@ -1,21 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -@Component -public class WithingsAccessTokenRequestParametersConverter implements Converter> { - - @Override - public MultiValueMap convert(OAuth2AuthorizationCodeGrantRequest source) { - MultiValueMap parameters = new LinkedMultiValueMap<>(); - parameters.add("action", "requesttoken"); - parameters.add(OAuth2ParameterNames.CLIENT_ID, source.getClientRegistration().getClientId()); - parameters.add(OAuth2ParameterNames.CLIENT_SECRET, source.getClientRegistration().getClientSecret()); - return parameters; - } -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenResponseClient.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenResponseClient.java deleted file mode 100644 index 1fd30b2..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenResponseClient.java +++ /dev/null @@ -1,47 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -import java.util.Arrays; - -import org.springframework.http.converter.FormHttpMessageConverter; -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.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; -import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -@Component -public class WithingsAccessTokenResponseClient implements OAuth2AccessTokenResponseClient { - - private final DefaultAuthorizationCodeTokenResponseClient tokenResponseClient; - - public WithingsAccessTokenResponseClient( - WithingsAccessTokenRequestParametersConverter withingsAccessTokenRequestParametersConverter, - WithingsAccessTokenResponseConverter withingsAccessTokenResponseConverter - - ) { - OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter(); - requestEntityConverter.addParametersConverter(withingsAccessTokenRequestParametersConverter); - - OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); - accessTokenResponseConverter.setAccessTokenResponseConverter(withingsAccessTokenResponseConverter); - - RestTemplate restTemplate = new RestTemplate(Arrays.asList( - new FormHttpMessageConverter(), - accessTokenResponseConverter)); - - restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); - - this.tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); - this.tokenResponseClient.setRequestEntityConverter(requestEntityConverter); - this.tokenResponseClient.setRestOperations(restTemplate); - } - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { - return this.tokenResponseClient.getTokenResponse(authorizationGrantRequest); - } -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenResponseConverter.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenResponseConverter.java deleted file mode 100644 index e8f8520..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsAccessTokenResponseConverter.java +++ /dev/null @@ -1,47 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import mucsi96.traininglog.withings.WithingsTechnicalException; -import mucsi96.traininglog.withings.data.GetAccessTokenResponseBody; -import mucsi96.traininglog.withings.data.WithingsResponse; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.Set; - -@Slf4j -@Component -public class WithingsAccessTokenResponseConverter implements Converter, OAuth2AccessTokenResponse> { - @Override - public OAuth2AccessTokenResponse convert(Map source) { - ObjectMapper mapper = new ObjectMapper(); - WithingsResponse response = - mapper.convertValue(source, new TypeReference<>() {}); - - if (response.getError() != null) { - log.error("Withings error {}", response.getError()); - throw new WithingsTechnicalException(); - } - - if (response.getStatus() != 0) { - throw new WithingsTechnicalException(); - } - - GetAccessTokenResponseBody body = response.getBody(); - - return OAuth2AccessTokenResponse - .withToken(body.getAccessToken()) - .refreshToken(body.getRefreshToken()) - .expiresIn(body.getExpiresIn()) - .scopes(Set.of(body.getScope())) - .tokenType(OAuth2AccessToken.TokenType.BEARER) - .additionalParameters(Map.of("userId", body.getUserId())) - .build(); - } -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsClient.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsClient.java deleted file mode 100644 index 74e9c3b..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsClient.java +++ /dev/null @@ -1,5 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -public class WithingsClient { - public static final String id = "withings-client"; -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsOAuthConfiguration.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsOAuthConfiguration.java deleted file mode 100644 index 968da04..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsOAuthConfiguration.java +++ /dev/null @@ -1,50 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -import org.springframework.security.web.SecurityFilterChain; - -import io.github.mucsi96.kubetools.security.KubetoolsSecurityConfigurer; - -@Configuration -public class WithingsOAuthConfiguration { - - @Bean - SecurityFilterChain withingsSecurityFilterChain( - HttpSecurity http, - KubetoolsSecurityConfigurer kubetoolsSecurityConfigurer, - WithingsAccessTokenResponseClient accessTokenResponseClient) throws Exception { - return kubetoolsSecurityConfigurer.configure(http) - .securityMatcher("/withings/**") - .oauth2Client(configurer -> configurer - .authorizationCodeGrant(customizer -> customizer - .accessTokenResponseClient(accessTokenResponseClient))) - .build(); - } - - @Bean - OAuth2AuthorizedClientManager withingsAuthorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository, - WithingsRefreshTokenResponseClient refreshTokenResponseClient) { - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - - OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenResponseClient)) - .build(); - - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; - } -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsRefreshTokenRequestParametersConverter.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsRefreshTokenRequestParametersConverter.java deleted file mode 100644 index 76092a0..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsRefreshTokenRequestParametersConverter.java +++ /dev/null @@ -1,18 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -public class WithingsRefreshTokenRequestParametersConverter implements Converter> { - @Override - public MultiValueMap convert(OAuth2RefreshTokenGrantRequest source) { - MultiValueMap parameters = new LinkedMultiValueMap<>(); - parameters.add("action", "requesttoken"); - parameters.add(OAuth2ParameterNames.CLIENT_ID, source.getClientRegistration().getClientId()); - parameters.add(OAuth2ParameterNames.CLIENT_SECRET, source.getClientRegistration().getClientSecret()); - return parameters; - } -} diff --git a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsRefreshTokenResponseClient.java b/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsRefreshTokenResponseClient.java deleted file mode 100644 index f5ab6d5..0000000 --- a/server/src/main/java/mucsi96/traininglog/withings/oauth/WithingsRefreshTokenResponseClient.java +++ /dev/null @@ -1,45 +0,0 @@ -package mucsi96.traininglog.withings.oauth; - -import java.util.Arrays; - -import org.springframework.http.converter.FormHttpMessageConverter; -import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequestEntityConverter; -import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -@Component -public class WithingsRefreshTokenResponseClient implements OAuth2AccessTokenResponseClient { - - private final DefaultRefreshTokenTokenResponseClient tokenResponseClient; - - public WithingsRefreshTokenResponseClient( - WithingsAccessTokenResponseConverter withingsAccessTokenResponseConverter - ) { - OAuth2RefreshTokenGrantRequestEntityConverter requestEntityConverter = new OAuth2RefreshTokenGrantRequestEntityConverter(); - requestEntityConverter.addParametersConverter(new WithingsRefreshTokenRequestParametersConverter()); - - OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); - accessTokenResponseConverter.setAccessTokenResponseConverter(withingsAccessTokenResponseConverter); - - RestTemplate restTemplate = new RestTemplate(Arrays.asList( - new FormHttpMessageConverter(), - accessTokenResponseConverter)); - - restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); - - this.tokenResponseClient = new DefaultRefreshTokenTokenResponseClient(); - this.tokenResponseClient.setRequestEntityConverter(requestEntityConverter); - this.tokenResponseClient.setRestOperations(restTemplate); - } - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) { - return tokenResponseClient.getTokenResponse(authorizationGrantRequest); - } -} diff --git a/server/src/main/resources/withings.yaml b/server/src/main/resources/withings.yaml new file mode 100644 index 0000000..5ea81f2 --- /dev/null +++ b/server/src/main/resources/withings.yaml @@ -0,0 +1,123 @@ +openapi: '3.0.2' +info: + title: API + version: '1.0' + +servers: + - url: https://wbsapi.withings.net + +paths: + /v2/oauth2: + post: + responses: + '200': + description: OK + content: + application/json: + schema: + title: WithingsGetAccessTokenResponse + type: object + required: + - "status" + - "body" + properties: + status: + type: integer + body: + type: object + required: + - "userid" + - "access_token" + - "refresh_token" + - "expires_in" + - "scope" + - "csrf_token" + - "token_type" + properties: + userid: + type: string + access_token: + type: string + refresh_token: + type: string + expires_in: + type: integer + scope: + type: string + csrf_token: + type: string + token_type: + type: string + + /measure: + post: + responses: + '200': + description: OK + content: + application/json: + schema: + title: WithingsGetMeasureResponse + type: object + required: + - "status" + - "body" + properties: + status: + type: integer + body: + type: object + properties: + updatetime: + type: string + timezone: + type: string + measuregrps: + type: array + items: + title: WithingsMeasureGroup + type: object + properties: + grpid: + type: integer + attrib: + type: integer + date: + type: integer + created: + type: integer + modified: + type: integer + category: + type: integer + deviceid: + type: string + measures: + type: array + items: + title: WithingsMeasure + type: object + properties: + value: + type: integer + type: + type: integer + unit: + type: integer + algo: + type: integer + fm: + type: integer + fw: + type: integer + comment: + type: string + timezone: + type: string + more: + type: integer + offset: + type: integer + + + diff --git a/server/src/test/java/mucsi96/traininglog/WithingsControllerTests.java b/server/src/test/java/mucsi96/traininglog/WithingsControllerTests.java index 55b836b..cc6e296 100644 --- a/server/src/test/java/mucsi96/traininglog/WithingsControllerTests.java +++ b/server/src/test/java/mucsi96/traininglog/WithingsControllerTests.java @@ -41,7 +41,7 @@ import mucsi96.traininglog.repository.TestAuthorizedClientRepository; import mucsi96.traininglog.weight.Weight; import mucsi96.traininglog.weight.WeightRepository; -import mucsi96.traininglog.withings.oauth.WithingsClient; +import mucsi96.traininglog.withings.WithingsConfiguration; @RequiredArgsConstructor @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) @@ -175,7 +175,7 @@ public void requests_access_token_after_consent_is_granted() throws Exception { assertThat(requests).hasSize(1); URI uri = new URI("?" + requests.get(0).getBodyAsString()); - Optional authorizedClient = authorizedClientRepository.findById(WithingsClient.id); + Optional authorizedClient = authorizedClientRepository.findById(WithingsConfiguration.registrationId); assertThat(authorizedClient.isPresent()).isTrue(); assertThat(authorizedClient.get().getPrincipalName()).isEqualTo("rob"); @@ -209,7 +209,7 @@ public void requests_new_access_token_if_its_expired() throws Exception { authorizeWithingsOAuth2Client(); LocalDateTime expiredAccessTokenIssuedAt = LocalDateTime.now().minusDays(2); LocalDateTime expiredAccessTokenExpiresAt = LocalDateTime.now().minusDays(1); - authorizedClientRepository.findById(WithingsClient.id).ifPresent(authorizedClient -> { + authorizedClientRepository.findById(WithingsConfiguration.registrationId).ifPresent(authorizedClient -> { authorizedClient.setAccessTokenValue("expired-access-token".getBytes(StandardCharsets.UTF_8)); authorizedClient.setAccessTokenIssuedAt(expiredAccessTokenIssuedAt); authorizedClient.setAccessTokenExpiresAt(expiredAccessTokenExpiresAt); @@ -222,7 +222,7 @@ public void requests_new_access_token_if_its_expired() throws Exception { .headers(getAuthHeaders("user"))) .andReturn().getResponse(); - Optional authorizedClient = authorizedClientRepository.findById(WithingsClient.id); + Optional authorizedClient = authorizedClientRepository.findById(WithingsConfiguration.registrationId); assertThat(authorizedClient.isPresent()).isTrue(); assertThat(authorizedClient.get().getPrincipalName()).isEqualTo("rob");