Skip to content

Commit 0df4d9a

Browse files
committed
custom withings authorization endpoint
1 parent d73b5b2 commit 0df4d9a

File tree

8 files changed

+69
-123
lines changed

8 files changed

+69
-123
lines changed

TODO.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
- Adjust the UI
22
- 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.
3+
- Create dedicated AuthorizedClientManager for withings and get rid of AccessTokenResponseClient, RefreshTokenResponseClient
4+
- Try using WebTestClient https://docs.spring.io/spring-framework/reference/testing/webtestclient.html#webtestclient-json

server/src/main/java/mucsi96/traininglog/core/AppControllerAdvice.java

+15-14
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,28 @@
22

33
import org.springframework.hateoas.Link;
44
import org.springframework.hateoas.RepresentationModel;
5+
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
56
import org.springframework.http.HttpStatus;
67
import org.springframework.http.ResponseEntity;
7-
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
8-
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
98
import org.springframework.web.bind.annotation.ControllerAdvice;
109
import org.springframework.web.bind.annotation.ExceptionHandler;
11-
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
10+
11+
import mucsi96.traininglog.withings.WithingsAuthorizationException;
12+
import mucsi96.traininglog.withings.WithingsController;
1213

1314
@ControllerAdvice
1415
public class AppControllerAdvice {
15-
@ExceptionHandler(ClientAuthorizationRequiredException.class)
16+
17+
@ExceptionHandler(WithingsAuthorizationException.class)
1618
public ResponseEntity<RepresentationModel<?>> handleClientAuthorizationRequired(
17-
ClientAuthorizationRequiredException ex) {
18-
String oauth2LoginUrl = ServletUriComponentsBuilder.fromCurrentServletMapping().path(
19-
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/"
20-
+ ex.getClientRegistrationId())
21-
.build().toString();
22-
return ResponseEntity
23-
.status(HttpStatus.UNAUTHORIZED)
24-
.body(RepresentationModel
25-
.of(null)
26-
.add(Link.of(oauth2LoginUrl).withRel("oauth2Login")));
19+
WithingsAuthorizationException ex) {
20+
21+
Link oauth2LogLink = WebMvcLinkBuilder
22+
.linkTo(WebMvcLinkBuilder.methodOn(WithingsController.class).authorize(null))
23+
.withRel("oauth2Login");
24+
25+
RepresentationModel<?> model = RepresentationModel.of(null).add(oauth2LogLink);
26+
27+
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(model);
2728
}
2829
}

server/src/main/java/mucsi96/traininglog/core/RedirectToHomeRequestCache.java

-88
This file was deleted.

server/src/main/java/mucsi96/traininglog/core/SecurityConfiguration.java

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ SecurityFilterChain securityFilterChain(
2929
KubetoolsSecurityConfigurer kubetoolsSecurityConfigurer,
3030
AccessTokenResponseClient accessTokenResponseClient) throws Exception {
3131
return kubetoolsSecurityConfigurer.configure(http)
32-
.requestCache(configurer -> configurer.requestCache(new RedirectToHomeRequestCache()))
3332
.oauth2Client(configurer -> configurer
3433
.authorizationCodeGrant(customizer -> customizer
3534
.accessTokenResponseClient(accessTokenResponseClient)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package mucsi96.traininglog.withings;
2+
3+
public class WithingsAuthorizationException extends RuntimeException {
4+
}
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,61 @@
11
package mucsi96.traininglog.withings;
22

3-
import org.springframework.http.MediaType;
3+
import org.springframework.security.core.Authentication;
4+
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
5+
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
46
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
7+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
58
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
9+
import org.springframework.web.bind.annotation.GetMapping;
610
import org.springframework.web.bind.annotation.PostMapping;
711
import org.springframework.web.bind.annotation.RequestMapping;
812
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.servlet.view.RedirectView;
914

1015
import io.swagger.v3.oas.annotations.Parameter;
1116
import jakarta.annotation.security.RolesAllowed;
17+
import jakarta.servlet.http.HttpServletRequest;
18+
import jakarta.servlet.http.HttpServletResponse;
1219
import lombok.RequiredArgsConstructor;
1320
import mucsi96.traininglog.weight.WeightService;
1421
import mucsi96.traininglog.withings.oauth.WithingsClient;
1522

1623
@RestController
17-
@RequestMapping(value = "/withings", produces = MediaType.APPLICATION_JSON_VALUE)
24+
@RequestMapping("/withings")
1825
@RequiredArgsConstructor
1926
@RolesAllowed("user")
2027
public class WithingsController {
2128

2229
private final WithingsService withingsService;
2330
private final WeightService weightService;
31+
private final OAuth2AuthorizedClientManager authorizedClientManager;
2432

2533
@PostMapping("/sync")
26-
void sync(
27-
@Parameter(hidden = true) @RegisteredOAuth2AuthorizedClient(WithingsClient.id) OAuth2AuthorizedClient withingsAuthorizedClient) {
34+
public void sync(
35+
Authentication principal,
36+
HttpServletRequest servletRequest,
37+
HttpServletResponse servletResponse) {
38+
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
39+
.withClientRegistrationId(WithingsClient.id)
40+
.principal(principal)
41+
.attribute(HttpServletRequest.class.getName(), servletRequest)
42+
.attribute(HttpServletResponse.class.getName(), servletResponse)
43+
.build();
2844

29-
if (!weightService.getTodayWeight().isPresent()) {
30-
withingsService.getTodayWeight(withingsAuthorizedClient).ifPresent(weightService::saveWeight);
45+
try {
46+
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
47+
if (!weightService.getTodayWeight().isPresent()) {
48+
withingsService.getTodayWeight(authorizedClient).ifPresent(weightService::saveWeight);
49+
}
50+
} catch (ClientAuthorizationRequiredException ex) {
51+
throw new WithingsAuthorizationException();
3152
}
53+
54+
}
55+
56+
@GetMapping("/authorize")
57+
public RedirectView authorize(
58+
@Parameter(hidden = true) @RegisteredOAuth2AuthorizedClient(WithingsClient.id) OAuth2AuthorizedClient withingsAuthorizedClient) {
59+
return new RedirectView("/");
3260
}
3361
}

server/src/main/resources/application.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
server:
22
shutdown: graceful
3+
servlet:
4+
context-path: /api
35
management:
46
endpoint:
57
health:
@@ -20,7 +22,7 @@ spring:
2022
client-id: ${WITHINGS_CLIENT_ID}
2123
client-secret: ${WITHINGS_CLIENT_SECRET}
2224
authorization-grant-type: authorization_code
23-
redirect-uri: "{baseUrl}/authorize/oauth2/code/{registrationId}"
25+
redirect-uri: "{baseUrl}/withings/authorize"
2426
scope: user.metrics
2527
datasource:
2628
url: jdbc:postgresql://${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}

server/src/test/java/mucsi96/traininglog/WithingsControllerTests.java

+11-13
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@
4747
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
4848
public class WithingsControllerTests extends BaseIntegrationTest {
4949

50-
// RefreshTokenOAuth2AuthorizedClientProvider
51-
5250
private final MockMvc mockMvc;
5351
private final TestAuthorizedClientRepository authorizedClientRepository;
5452
private final WeightRepository weightRepository;
@@ -109,7 +107,7 @@ public void returns_not_authorized_if_authorized_client_is_not_found() throws Ex
109107

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

115113
@Test
@@ -128,7 +126,7 @@ public void returns_forbidden_if_user_has_no_user_role() throws Exception {
128126
public void redirects_to_withings_request_authorization_page() throws Exception {
129127
MockHttpServletResponse response = mockMvc
130128
.perform(
131-
get("/oauth2/authorization/withings-client"))
129+
get("/withings/authorize").headers(getAuthHeaders("user")))
132130
.andReturn().getResponse();
133131

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

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

154152
MockHttpSession mockHttpSession = new MockHttpSession();
155153
MockHttpServletResponse response1 = mockMvc.perform(
156-
get("/oauth2/authorization/withings-client")
157-
.headers(getAuthHeaders("user"))
154+
get("/withings/authorize").headers(getAuthHeaders("user"))
158155
.session(mockHttpSession))
159156
.andReturn().getResponse();
160157
UriComponents components = UriComponentsBuilder.fromUriString(response1.getRedirectedUrl()).build();
161158
String state = URLDecoder.decode(
162159
components.getQueryParams().getFirst(OAuth2ParameterNames.STATE),
163160
StandardCharsets.UTF_8);
164161

165-
MockHttpServletResponse response2 = mockMvc.perform(get("/authorize/oauth2/code/withings-client")
166-
.headers(getAuthHeaders("user"))
167-
.queryParam(OAuth2ParameterNames.STATE, state)
168-
.queryParam(OAuth2ParameterNames.CODE, "test-authorization-code")
169-
.session(mockHttpSession))
162+
MockHttpServletResponse response2 = mockMvc
163+
.perform(get(components.getQueryParams().getFirst(OAuth2ParameterNames.REDIRECT_URI))
164+
.headers(getAuthHeaders("user"))
165+
.queryParam(OAuth2ParameterNames.STATE, state)
166+
.queryParam(OAuth2ParameterNames.CODE, "test-authorization-code")
167+
.session(mockHttpSession))
170168
.andReturn().getResponse();
171169

172170
assertThat(response2.getStatus()).isEqualTo(302);
173-
assertThat(response2.getRedirectedUrl()).isEqualTo("http://localhost/");
171+
assertThat(response2.getRedirectedUrl()).isEqualTo("http://localhost/withings/authorize");
174172

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

0 commit comments

Comments
 (0)