From d1782e728a603953662fb31940b08127dcccc024 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 20 Jun 2023 06:36:16 -0400 Subject: [PATCH 1/4] Release 1.1.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 52e89382..79491835 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=1.1.1-SNAPSHOT +version=1.1.1 org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true org.gradle.caching=true From 5c2a2c0d0c92af4a96096d173dddfa97a0896381 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 20 Jun 2023 06:42:06 -0400 Subject: [PATCH 2/4] Next Development Version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 79491835..69c1853e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=1.1.1 +version=1.1.2-SNAPSHOT org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true org.gradle.caching=true From b6f3b5cc455abe9f88f6d67603aa1b78e244f023 Mon Sep 17 00:00:00 2001 From: cbilodeau Date: Tue, 27 Jun 2023 11:33:34 -0400 Subject: [PATCH 3/4] Fix generating ID token with null sid when refresh_token grant Closes gh-1283 --- .../authorization/token/JwtGenerator.java | 4 +- ...freshTokenAuthenticationProviderTests.java | 82 ++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java index 2bbff980..b02b2ca3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java @@ -134,7 +134,9 @@ public final class JwtGenerator implements OAuth2TokenGenerator { } } else if (AuthorizationGrantType.REFRESH_TOKEN.equals(context.getAuthorizationGrantType())) { OidcIdToken currentIdToken = context.getAuthorization().getToken(OidcIdToken.class).getToken(); - claimsBuilder.claim("sid", currentIdToken.getClaim("sid")); + if (currentIdToken.hasClaim("sid")) { + claimsBuilder.claim("sid", currentIdToken.getClaim("sid")); + } claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, currentIdToken.getClaim(IdTokenClaimNames.AUTH_TIME)); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java index 61f0452d..3f9981e7 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java @@ -47,6 +47,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; @@ -250,7 +251,86 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { assertThat(idTokenContext.getJwsHeader()).isNotNull(); assertThat(idTokenContext.getClaims()).isNotNull(); - verify(this.jwtEncoder, times(2)).encode(any()); // Access token and ID Token + ArgumentCaptor jwtEncoderParametersArgumentCaptor = ArgumentCaptor.forClass(JwtEncoderParameters.class); + verify(this.jwtEncoder, times(2)).encode(jwtEncoderParametersArgumentCaptor.capture()); // Access token and ID Token + JwtEncoderParameters jwtEncoderParameters = jwtEncoderParametersArgumentCaptor.getValue(); + assertThat(jwtEncoderParameters.getClaims().getClaims().get("sid")).isNotNull(); + + ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); + verify(this.authorizationService).save(authorizationCaptor.capture()); + OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); + + assertThat(accessTokenAuthentication.getRegisteredClient().getId()).isEqualTo(updatedAuthorization.getRegisteredClientId()); + assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(clientPrincipal); + assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken().getToken()); + assertThat(updatedAuthorization.getAccessToken()).isNotEqualTo(authorization.getAccessToken()); + OAuth2Authorization.Token idToken = updatedAuthorization.getToken(OidcIdToken.class); + assertThat(idToken).isNotNull(); + assertThat(accessTokenAuthentication.getAdditionalParameters()) + .containsExactly(entry(OidcParameterNames.ID_TOKEN, idToken.getToken().getTokenValue())); + assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getRefreshToken().getToken()); + // By default, refresh token is reused + assertThat(updatedAuthorization.getRefreshToken()).isEqualTo(authorization.getRefreshToken()); + } + + @Test + public void authenticateWhenValidRefreshTokenThenReturnIdTokenWithoutSid() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + OidcIdToken authorizedIdToken = OidcIdToken.withTokenValue("id-token") + .issuer("https://provider.com") + .subject("subject") + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(60)) + .claim(IdTokenClaimNames.AUTH_TIME, Date.from(Instant.now())) + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(authorizedIdToken).build(); + when(this.authorizationService.findByToken( + eq(authorization.getRefreshToken().getToken().getTokenValue()), + eq(OAuth2TokenType.REFRESH_TOKEN))) + .thenReturn(authorization); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken( + authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null); + + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = + (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + ArgumentCaptor jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class); + verify(this.jwtCustomizer, times(2)).customize(jwtEncodingContextCaptor.capture()); + // Access Token context + JwtEncodingContext accessTokenContext = jwtEncodingContextCaptor.getAllValues().get(0); + assertThat(accessTokenContext.getRegisteredClient()).isEqualTo(registeredClient); + assertThat(accessTokenContext.getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName())); + assertThat(accessTokenContext.getAuthorization()).isEqualTo(authorization); + assertThat(accessTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes()); + assertThat(accessTokenContext.getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN); + assertThat(accessTokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); + assertThat(accessTokenContext.getAuthorizationGrant()).isEqualTo(authentication); + assertThat(accessTokenContext.getJwsHeader()).isNotNull(); + assertThat(accessTokenContext.getClaims()).isNotNull(); + Map claims = new HashMap<>(); + accessTokenContext.getClaims().claims(claims::putAll); + assertThat(claims).flatExtracting(OAuth2ParameterNames.SCOPE) + .containsExactlyInAnyOrder(OidcScopes.OPENID, "scope1"); + // ID Token context + JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1); + assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient); + assertThat(idTokenContext.getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName())); + assertThat(idTokenContext.getAuthorization()).isNotEqualTo(authorization); + assertThat(idTokenContext.getAuthorization().getAccessToken()).isNotEqualTo(authorization.getAccessToken()); + assertThat(idTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes()); + assertThat(idTokenContext.getTokenType().getValue()).isEqualTo(OidcParameterNames.ID_TOKEN); + assertThat(idTokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); + assertThat(idTokenContext.getAuthorizationGrant()).isEqualTo(authentication); + assertThat(idTokenContext.getJwsHeader()).isNotNull(); + assertThat(idTokenContext.getClaims()).isNotNull(); + + ArgumentCaptor jwtEncoderParametersArgumentCaptor = ArgumentCaptor.forClass(JwtEncoderParameters.class); + verify(this.jwtEncoder, times(2)).encode(jwtEncoderParametersArgumentCaptor.capture()); // Access token and ID Token + JwtEncoderParameters jwtEncoderParameters = jwtEncoderParametersArgumentCaptor.getValue(); + assertThat(jwtEncoderParameters.getClaims().getClaims().get("sid")).isNull(); ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); verify(this.authorizationService).save(authorizationCaptor.capture()); From 2895169b3025fbe88b888a734576691942517380 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 30 Jun 2023 06:15:56 -0400 Subject: [PATCH 4/4] Polish gh-1289 --- .../authorization/token/JwtGenerator.java | 4 +- ...freshTokenAuthenticationProviderTests.java | 82 +------------------ .../token/JwtGeneratorTests.java | 41 ++++++++++ 3 files changed, 45 insertions(+), 82 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java index b02b2ca3..94ba32e3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/JwtGenerator.java @@ -137,7 +137,9 @@ public final class JwtGenerator implements OAuth2TokenGenerator { if (currentIdToken.hasClaim("sid")) { claimsBuilder.claim("sid", currentIdToken.getClaim("sid")); } - claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, currentIdToken.getClaim(IdTokenClaimNames.AUTH_TIME)); + if (currentIdToken.hasClaim(IdTokenClaimNames.AUTH_TIME)) { + claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, currentIdToken.getClaim(IdTokenClaimNames.AUTH_TIME)); + } } } // @formatter:on diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java index 3f9981e7..61f0452d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java @@ -47,7 +47,6 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; @@ -251,86 +250,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { assertThat(idTokenContext.getJwsHeader()).isNotNull(); assertThat(idTokenContext.getClaims()).isNotNull(); - ArgumentCaptor jwtEncoderParametersArgumentCaptor = ArgumentCaptor.forClass(JwtEncoderParameters.class); - verify(this.jwtEncoder, times(2)).encode(jwtEncoderParametersArgumentCaptor.capture()); // Access token and ID Token - JwtEncoderParameters jwtEncoderParameters = jwtEncoderParametersArgumentCaptor.getValue(); - assertThat(jwtEncoderParameters.getClaims().getClaims().get("sid")).isNotNull(); - - ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); - verify(this.authorizationService).save(authorizationCaptor.capture()); - OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); - - assertThat(accessTokenAuthentication.getRegisteredClient().getId()).isEqualTo(updatedAuthorization.getRegisteredClientId()); - assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(clientPrincipal); - assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken().getToken()); - assertThat(updatedAuthorization.getAccessToken()).isNotEqualTo(authorization.getAccessToken()); - OAuth2Authorization.Token idToken = updatedAuthorization.getToken(OidcIdToken.class); - assertThat(idToken).isNotNull(); - assertThat(accessTokenAuthentication.getAdditionalParameters()) - .containsExactly(entry(OidcParameterNames.ID_TOKEN, idToken.getToken().getTokenValue())); - assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getRefreshToken().getToken()); - // By default, refresh token is reused - assertThat(updatedAuthorization.getRefreshToken()).isEqualTo(authorization.getRefreshToken()); - } - - @Test - public void authenticateWhenValidRefreshTokenThenReturnIdTokenWithoutSid() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); - OidcIdToken authorizedIdToken = OidcIdToken.withTokenValue("id-token") - .issuer("https://provider.com") - .subject("subject") - .issuedAt(Instant.now()) - .expiresAt(Instant.now().plusSeconds(60)) - .claim(IdTokenClaimNames.AUTH_TIME, Date.from(Instant.now())) - .build(); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(authorizedIdToken).build(); - when(this.authorizationService.findByToken( - eq(authorization.getRefreshToken().getToken().getTokenValue()), - eq(OAuth2TokenType.REFRESH_TOKEN))) - .thenReturn(authorization); - - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); - OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken( - authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null); - - OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = - (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - ArgumentCaptor jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class); - verify(this.jwtCustomizer, times(2)).customize(jwtEncodingContextCaptor.capture()); - // Access Token context - JwtEncodingContext accessTokenContext = jwtEncodingContextCaptor.getAllValues().get(0); - assertThat(accessTokenContext.getRegisteredClient()).isEqualTo(registeredClient); - assertThat(accessTokenContext.getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName())); - assertThat(accessTokenContext.getAuthorization()).isEqualTo(authorization); - assertThat(accessTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes()); - assertThat(accessTokenContext.getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN); - assertThat(accessTokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); - assertThat(accessTokenContext.getAuthorizationGrant()).isEqualTo(authentication); - assertThat(accessTokenContext.getJwsHeader()).isNotNull(); - assertThat(accessTokenContext.getClaims()).isNotNull(); - Map claims = new HashMap<>(); - accessTokenContext.getClaims().claims(claims::putAll); - assertThat(claims).flatExtracting(OAuth2ParameterNames.SCOPE) - .containsExactlyInAnyOrder(OidcScopes.OPENID, "scope1"); - // ID Token context - JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1); - assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient); - assertThat(idTokenContext.getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName())); - assertThat(idTokenContext.getAuthorization()).isNotEqualTo(authorization); - assertThat(idTokenContext.getAuthorization().getAccessToken()).isNotEqualTo(authorization.getAccessToken()); - assertThat(idTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes()); - assertThat(idTokenContext.getTokenType().getValue()).isEqualTo(OidcParameterNames.ID_TOKEN); - assertThat(idTokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); - assertThat(idTokenContext.getAuthorizationGrant()).isEqualTo(authentication); - assertThat(idTokenContext.getJwsHeader()).isNotNull(); - assertThat(idTokenContext.getClaims()).isNotNull(); - - ArgumentCaptor jwtEncoderParametersArgumentCaptor = ArgumentCaptor.forClass(JwtEncoderParameters.class); - verify(this.jwtEncoder, times(2)).encode(jwtEncoderParametersArgumentCaptor.capture()); // Access token and ID Token - JwtEncoderParameters jwtEncoderParameters = jwtEncoderParametersArgumentCaptor.getValue(); - assertThat(jwtEncoderParameters.getClaims().getClaims().get("sid")).isNull(); + verify(this.jwtEncoder, times(2)).encode(any()); // Access token and ID Token ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); verify(this.authorizationService).save(authorizationCaptor.capture()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java index 84c8fc17..fc1a7ba6 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/token/JwtGeneratorTests.java @@ -236,6 +236,47 @@ public class JwtGeneratorTests { assertGeneratedTokenType(tokenContext); } + // gh-1283 + @Test + public void generateWhenIdTokenTypeWithoutSidAndRefreshTokenGrantThenReturnJwt() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .scope(OidcScopes.OPENID) + .build(); + OidcIdToken idToken = OidcIdToken.withTokenValue("id-token") + .issuer("https://provider.com") + .subject("subject") + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(60)) + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) + .token(idToken) + .build(); + + OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken(); + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + + OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken( + refreshToken.getTokenValue(), clientPrincipal, null, null); + + Authentication principal = authorization.getAttribute(Principal.class.getName()); + + // @formatter:off + OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(principal) + .authorizationServerContext(this.authorizationServerContext) + .authorization(authorization) + .authorizedScopes(authorization.getAuthorizedScopes()) + .tokenType(ID_TOKEN_TOKEN_TYPE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrant(authentication) + .build(); + // @formatter:on + + assertGeneratedTokenType(tokenContext); + } + private void assertGeneratedTokenType(OAuth2TokenContext tokenContext) { this.jwtGenerator.generate(tokenContext);