|
17 | 17 |
|
18 | 18 | import java.net.URLEncoder;
|
19 | 19 | import java.nio.charset.StandardCharsets;
|
| 20 | +import java.security.MessageDigest; |
20 | 21 | import java.security.Principal;
|
| 22 | +import java.security.PublicKey; |
21 | 23 | import java.time.Instant;
|
22 | 24 | import java.util.Base64;
|
| 25 | +import java.util.HashMap; |
23 | 26 | import java.util.HashSet;
|
24 | 27 | import java.util.List;
|
25 | 28 | import java.util.Map;
|
@@ -279,6 +282,105 @@ public void requestWhenRefreshTokenRequestWithPublicClientThenReturnAccessTokenR
|
279 | 282 | .andExpect(jsonPath("$.scope").isNotEmpty());
|
280 | 283 | }
|
281 | 284 |
|
| 285 | + @Test |
| 286 | + public void requestWhenRefreshTokenRequestWithPublicClientAndDPoPProofThenReturnDPoPBoundAccessToken() |
| 287 | + throws Exception { |
| 288 | + this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire(); |
| 289 | + |
| 290 | + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient() |
| 291 | + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) |
| 292 | + .build(); |
| 293 | + this.registeredClientRepository.save(registeredClient); |
| 294 | + |
| 295 | + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.DPOP, |
| 296 | + "dpop-bound-access-token", Instant.now(), Instant.now().plusSeconds(300)); |
| 297 | + Map<String, Object> accessTokenClaims = new HashMap<>(); |
| 298 | + PublicKey publicKey = TestJwks.DEFAULT_EC_JWK.toPublicKey(); |
| 299 | + Map<String, Object> cnfClaim = new HashMap<>(); |
| 300 | + cnfClaim.put("jkt", computeSHA256(publicKey)); |
| 301 | + accessTokenClaims.put("cnf", cnfClaim); |
| 302 | + OAuth2Authorization authorization = TestOAuth2Authorizations |
| 303 | + .authorization(registeredClient, accessToken, accessTokenClaims) |
| 304 | + .build(); |
| 305 | + this.authorizationService.save(authorization); |
| 306 | + |
| 307 | + String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI; |
| 308 | + String dPoPProof = generateDPoPProof(tokenEndpointUri); |
| 309 | + |
| 310 | + this.mvc |
| 311 | + .perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization)) |
| 312 | + .param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()) |
| 313 | + .header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof)) |
| 314 | + .andExpect(status().isOk()) |
| 315 | + .andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue())); |
| 316 | + |
| 317 | + authorization = this.authorizationService.findById(authorization.getId()); |
| 318 | + assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf"); |
| 319 | + @SuppressWarnings("unchecked") |
| 320 | + Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf"); |
| 321 | + assertThat(cnfClaims).containsKey("jkt"); |
| 322 | + } |
| 323 | + |
| 324 | + @Test |
| 325 | + public void requestWhenRefreshTokenRequestWithPublicClientAndDPoPProofAndAccessTokenNotBoundThenBadRequest() |
| 326 | + throws Exception { |
| 327 | + this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire(); |
| 328 | + |
| 329 | + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient() |
| 330 | + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) |
| 331 | + .build(); |
| 332 | + this.registeredClientRepository.save(registeredClient); |
| 333 | + |
| 334 | + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); |
| 335 | + this.authorizationService.save(authorization); |
| 336 | + |
| 337 | + String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI; |
| 338 | + String dPoPProof = generateDPoPProof(tokenEndpointUri); |
| 339 | + |
| 340 | + this.mvc |
| 341 | + .perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization)) |
| 342 | + .param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()) |
| 343 | + .header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof)) |
| 344 | + .andExpect(status().isBadRequest()) |
| 345 | + .andExpect(jsonPath("$.error").value(OAuth2ErrorCodes.INVALID_DPOP_PROOF)) |
| 346 | + .andExpect(jsonPath("$.error_description").value("jkt claim is missing.")); |
| 347 | + } |
| 348 | + |
| 349 | + @Test |
| 350 | + public void requestWhenRefreshTokenRequestWithPublicClientAndDPoPProofAndDifferentPublicKeyThenBadRequest() |
| 351 | + throws Exception { |
| 352 | + this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire(); |
| 353 | + |
| 354 | + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient() |
| 355 | + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) |
| 356 | + .build(); |
| 357 | + this.registeredClientRepository.save(registeredClient); |
| 358 | + |
| 359 | + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.DPOP, |
| 360 | + "dpop-bound-access-token", Instant.now(), Instant.now().plusSeconds(300)); |
| 361 | + Map<String, Object> accessTokenClaims = new HashMap<>(); |
| 362 | + // Bind access token to different public key |
| 363 | + PublicKey publicKey = TestJwks.DEFAULT_RSA_JWK.toPublicKey(); |
| 364 | + Map<String, Object> cnfClaim = new HashMap<>(); |
| 365 | + cnfClaim.put("jkt", computeSHA256(publicKey)); |
| 366 | + accessTokenClaims.put("cnf", cnfClaim); |
| 367 | + OAuth2Authorization authorization = TestOAuth2Authorizations |
| 368 | + .authorization(registeredClient, accessToken, accessTokenClaims) |
| 369 | + .build(); |
| 370 | + this.authorizationService.save(authorization); |
| 371 | + |
| 372 | + String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI; |
| 373 | + String dPoPProof = generateDPoPProof(tokenEndpointUri); |
| 374 | + |
| 375 | + this.mvc |
| 376 | + .perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization)) |
| 377 | + .param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()) |
| 378 | + .header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof)) |
| 379 | + .andExpect(status().isBadRequest()) |
| 380 | + .andExpect(jsonPath("$.error").value(OAuth2ErrorCodes.INVALID_DPOP_PROOF)) |
| 381 | + .andExpect(jsonPath("$.error_description").value("jwk header is invalid.")); |
| 382 | + } |
| 383 | + |
282 | 384 | @Test
|
283 | 385 | public void requestWhenRefreshTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
|
284 | 386 | this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
@@ -327,6 +429,12 @@ private static String generateDPoPProof(String tokenEndpointUri) {
|
327 | 429 | return jwt.getTokenValue();
|
328 | 430 | }
|
329 | 431 |
|
| 432 | + private static String computeSHA256(PublicKey publicKey) throws Exception { |
| 433 | + MessageDigest md = MessageDigest.getInstance("SHA-256"); |
| 434 | + byte[] digest = md.digest(publicKey.getEncoded()); |
| 435 | + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); |
| 436 | + } |
| 437 | + |
330 | 438 | private static MultiValueMap<String, String> getRefreshTokenRequestParameters(OAuth2Authorization authorization) {
|
331 | 439 | MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
332 | 440 | parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue());
|
|
0 commit comments