Skip to content

Commit

Permalink
custom withings authorization endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
mucsi96 committed Jul 14, 2023
1 parent d73b5b2 commit 0df4d9a
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 123 deletions.
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
- Adjust the UI
- Create separate E2E test project which locally tests agains running angular + Spring API using Selnium. On CI it uses test containers of both images. Adding authontication headers with own server.
- Create dedicated AuthorizedClientManager for withings and get rid of AccessTokenResponseClient, RefreshTokenResponseClient
- Try using WebTestClient https://docs.spring.io/spring-framework/reference/testing/webtestclient.html#webtestclient-json
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@

import org.springframework.hateoas.Link;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import mucsi96.traininglog.withings.WithingsAuthorizationException;
import mucsi96.traininglog.withings.WithingsController;

@ControllerAdvice
public class AppControllerAdvice {
@ExceptionHandler(ClientAuthorizationRequiredException.class)

@ExceptionHandler(WithingsAuthorizationException.class)
public ResponseEntity<RepresentationModel<?>> handleClientAuthorizationRequired(
ClientAuthorizationRequiredException ex) {
String oauth2LoginUrl = ServletUriComponentsBuilder.fromCurrentServletMapping().path(
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/"
+ ex.getClientRegistrationId())
.build().toString();
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(RepresentationModel
.of(null)
.add(Link.of(oauth2LoginUrl).withRel("oauth2Login")));
WithingsAuthorizationException ex) {

Link oauth2LogLink = WebMvcLinkBuilder
.linkTo(WebMvcLinkBuilder.methodOn(WithingsController.class).authorize(null))
.withRel("oauth2Login");

RepresentationModel<?> model = RepresentationModel.of(null).add(oauth2LogLink);

return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(model);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ SecurityFilterChain securityFilterChain(
KubetoolsSecurityConfigurer kubetoolsSecurityConfigurer,
AccessTokenResponseClient accessTokenResponseClient) throws Exception {
return kubetoolsSecurityConfigurer.configure(http)
.requestCache(configurer -> configurer.requestCache(new RedirectToHomeRequestCache()))
.oauth2Client(configurer -> configurer
.authorizationCodeGrant(customizer -> customizer
.accessTokenResponseClient(accessTokenResponseClient)))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package mucsi96.traininglog.withings;

public class WithingsAuthorizationException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
package mucsi96.traininglog.withings;

import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.view.RedirectView;

import io.swagger.v3.oas.annotations.Parameter;
import jakarta.annotation.security.RolesAllowed;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import mucsi96.traininglog.weight.WeightService;
import mucsi96.traininglog.withings.oauth.WithingsClient;

@RestController
@RequestMapping(value = "/withings", produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping("/withings")
@RequiredArgsConstructor
@RolesAllowed("user")
public class WithingsController {

private final WithingsService withingsService;
private final WeightService weightService;
private final OAuth2AuthorizedClientManager authorizedClientManager;

@PostMapping("/sync")
void sync(
@Parameter(hidden = true) @RegisteredOAuth2AuthorizedClient(WithingsClient.id) OAuth2AuthorizedClient withingsAuthorizedClient) {
public void sync(
Authentication principal,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(WithingsClient.id)
.principal(principal)
.attribute(HttpServletRequest.class.getName(), servletRequest)
.attribute(HttpServletResponse.class.getName(), servletResponse)
.build();

if (!weightService.getTodayWeight().isPresent()) {
withingsService.getTodayWeight(withingsAuthorizedClient).ifPresent(weightService::saveWeight);
try {
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
if (!weightService.getTodayWeight().isPresent()) {
withingsService.getTodayWeight(authorizedClient).ifPresent(weightService::saveWeight);
}
} catch (ClientAuthorizationRequiredException ex) {
throw new WithingsAuthorizationException();
}

}

@GetMapping("/authorize")
public RedirectView authorize(
@Parameter(hidden = true) @RegisteredOAuth2AuthorizedClient(WithingsClient.id) OAuth2AuthorizedClient withingsAuthorizedClient) {
return new RedirectView("/");
}
}
4 changes: 3 additions & 1 deletion server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
server:
shutdown: graceful
servlet:
context-path: /api
management:
endpoint:
health:
Expand All @@ -20,7 +22,7 @@ spring:
client-id: ${WITHINGS_CLIENT_ID}
client-secret: ${WITHINGS_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/authorize/oauth2/code/{registrationId}"
redirect-uri: "{baseUrl}/withings/authorize"
scope: user.metrics
datasource:
url: jdbc:postgresql://${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
public class WithingsControllerTests extends BaseIntegrationTest {

// RefreshTokenOAuth2AuthorizedClientProvider

private final MockMvc mockMvc;
private final TestAuthorizedClientRepository authorizedClientRepository;
private final WeightRepository weightRepository;
Expand Down Expand Up @@ -109,7 +107,7 @@ public void returns_not_authorized_if_authorized_client_is_not_found() throws Ex

assertThat(response.getStatus()).isEqualTo(401);
assertThat(JsonPath.parse(response.getContentAsString()).read("$._links.oauth2Login.href", String.class))
.isEqualTo("http://localhost/oauth2/authorization/withings-client");
.isEqualTo("http://localhost/withings/authorize");
}

@Test
Expand All @@ -128,7 +126,7 @@ public void returns_forbidden_if_user_has_no_user_role() throws Exception {
public void redirects_to_withings_request_authorization_page() throws Exception {
MockHttpServletResponse response = mockMvc
.perform(
get("/oauth2/authorization/withings-client"))
get("/withings/authorize").headers(getAuthHeaders("user")))
.andReturn().getResponse();

assertThat(response.getStatus()).isEqualTo(302);
Expand All @@ -141,7 +139,7 @@ public void redirects_to_withings_request_authorization_page() throws Exception
assertThat(redirectUrl).hasParameter(OAuth2ParameterNames.SCOPE, "user.metrics");
assertThat(redirectUrl).hasParameter(OAuth2ParameterNames.STATE);
assertThat(redirectUrl).hasParameter(OAuth2ParameterNames.REDIRECT_URI,
"http://localhost/authorize/oauth2/code/withings-client");
"http://localhost/withings/authorize");
}

@Test
Expand All @@ -153,24 +151,24 @@ public void requests_access_token_after_consent_is_granted() throws Exception {

MockHttpSession mockHttpSession = new MockHttpSession();
MockHttpServletResponse response1 = mockMvc.perform(
get("/oauth2/authorization/withings-client")
.headers(getAuthHeaders("user"))
get("/withings/authorize").headers(getAuthHeaders("user"))
.session(mockHttpSession))
.andReturn().getResponse();
UriComponents components = UriComponentsBuilder.fromUriString(response1.getRedirectedUrl()).build();
String state = URLDecoder.decode(
components.getQueryParams().getFirst(OAuth2ParameterNames.STATE),
StandardCharsets.UTF_8);

MockHttpServletResponse response2 = mockMvc.perform(get("/authorize/oauth2/code/withings-client")
.headers(getAuthHeaders("user"))
.queryParam(OAuth2ParameterNames.STATE, state)
.queryParam(OAuth2ParameterNames.CODE, "test-authorization-code")
.session(mockHttpSession))
MockHttpServletResponse response2 = mockMvc
.perform(get(components.getQueryParams().getFirst(OAuth2ParameterNames.REDIRECT_URI))
.headers(getAuthHeaders("user"))
.queryParam(OAuth2ParameterNames.STATE, state)
.queryParam(OAuth2ParameterNames.CODE, "test-authorization-code")
.session(mockHttpSession))
.andReturn().getResponse();

assertThat(response2.getStatus()).isEqualTo(302);
assertThat(response2.getRedirectedUrl()).isEqualTo("http://localhost/");
assertThat(response2.getRedirectedUrl()).isEqualTo("http://localhost/withings/authorize");

List<LoggedRequest> requests = mockWithingsServer
.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/v2/oauth2")));
Expand Down

0 comments on commit 0df4d9a

Please sign in to comment.