diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 3789561536..301df20365 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -694,8 +694,9 @@ private static JwtDecoder getJwtDecoder() { claims.put(IdTokenClaimNames.ISS, "http://localhost/iss"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId", "a", "u", "d")); claims.put(IdTokenClaimNames.AZP, "clientId"); - Jwt jwt = new Jwt("token123", Instant.now(), Instant.now().plusSeconds(3600), - Collections.singletonMap("header1", "value1"), claims); + claims.put(IdTokenClaimNames.IAT, Instant.now()); + claims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600)); + Jwt jwt = new Jwt("token123", Collections.singletonMap("header1", "value1"), claims); JwtDecoder jwtDecoder = mock(JwtDecoder.class); when(jwtDecoder.decode(any())).thenReturn(jwt); return jwtDecoder; @@ -738,8 +739,11 @@ private static OAuth2UserService createOauth2User } private static OAuth2UserService createOidcUserService() { - OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), - Instant.now().plusSeconds(3600), Collections.singletonMap(IdTokenClaimNames.SUB, "sub123")); + final Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.SUB, "sub123"); + claims.put(IdTokenClaimNames.IAT, Instant.now()); + claims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600)); + OidcIdToken idToken = new OidcIdToken("token123", claims); return request -> new DefaultOidcUser( Collections.singleton(new OidcUserAuthority(idToken)), idToken); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index f6734af2d2..f0defcde5b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -29,6 +29,7 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.PreDestroy; @@ -141,8 +142,14 @@ public class OAuth2ResourceServerConfigurerTests { private static final String JWT_TOKEN = "token"; private static final String JWT_SUBJECT = "mock-test-subject"; private static final Map JWT_HEADERS = Collections.singletonMap("alg", JwsAlgorithms.RS256); - private static final Map JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT); - private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS); + private static final Map JWT_CLAIMS() { + final Map claims = new HashMap<>(); + claims.put(JwtClaimNames.SUB, JWT_SUBJECT); + claims.put(JwtClaimNames.IAT, Instant.MIN); + claims.put(JwtClaimNames.EXP,Instant.MAX); + return claims; + } + private static final Jwt JWT = new Jwt(JWT_TOKEN, JWT_HEADERS, JWT_CLAIMS()); private static final String JWK_SET_URI = "https://mock.org"; private static final JwtAuthenticationToken JWT_AUTHENTICATION_TOKEN = new JwtAuthenticationToken(JWT, Collections.emptyList()); diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index 6857bd6ce5..34a58293e0 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -343,8 +343,9 @@ private ReactiveJwtDecoder getJwtDecoder() { claims.put(IdTokenClaimNames.ISS, "http://localhost/issuer"); claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client")); claims.put(IdTokenClaimNames.AZP, "client"); - Jwt jwt = new Jwt("id-token", Instant.now(), Instant.now().plusSeconds(3600), - Collections.singletonMap("header1", "value1"), claims); + claims.put(IdTokenClaimNames.IAT, Instant.now()); + claims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600)); + Jwt jwt = new Jwt("id-token", Collections.singletonMap("header1", "value1"), claims); return Mono.just(jwt); }; } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index 5edd88a005..1d0b77a8a3 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -16,6 +16,15 @@ package org.springframework.security.config.web.server; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import java.io.IOException; import java.math.BigInteger; import java.security.KeyFactory; @@ -27,21 +36,18 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; + import javax.annotation.PreDestroy; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; import org.apache.http.HttpHeaders; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import reactor.core.publisher.Mono; - import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; @@ -62,6 +68,7 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; @@ -79,14 +86,11 @@ import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.config.EnableWebFlux; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.hamcrest.core.StringStartsWith.startsWith; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import reactor.core.publisher.Mono; /** * Tests for {@link org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec} @@ -112,10 +116,18 @@ public class OAuth2ResourceServerSpecTests { " }\n" + " ]\n" + "}\n"; + + private Map claims() { + final Map claims = new HashMap<>(); + claims.put("sub", "user"); + claims.put(JwtClaimNames.IAT, Instant.MIN); + claims.put(JwtClaimNames.EXP, Instant.MAX); + return claims; + } - private Jwt jwt = new Jwt("token", Instant.MIN, Instant.MAX, + private Jwt jwt = new Jwt("token", Collections.singletonMap("alg", JwsAlgorithms.RS256), - Collections.singletonMap("sub", "user")); + claims()); private String clientId = "client"; private String clientSecret = "secret"; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 73227b6989..4530c14159 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -208,7 +208,7 @@ private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null); throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex); } - OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getClaims()); return idToken; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java index 8a215efdf8..3aebccd4a3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -190,6 +190,6 @@ private Mono createOidcToken(ClientRegistration clientRegistration, ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); String rawIdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); return jwtDecoder.decode(rawIdToken) - .map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims())); + .map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getClaims())); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientServiceTests.java index 52d85be817..6c7c0851da 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientServiceTests.java @@ -28,11 +28,15 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; + import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.when; @@ -54,10 +58,16 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests { private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused"); + private Map attributes(final Instant iat, final Instant exp) { + final Map attributes = new HashMap(); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", - Instant.now(), - Instant.now().plus(Duration.ofDays(1))); + attributes(Instant.now(), Instant.now().plus(Duration.ofDays(1)))); private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId) .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java index 77dba3d87a..b436cbdeab 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java @@ -307,8 +307,12 @@ private void setUpIdToken(Map claims) { private void setUpIdToken(Map claims, Instant issuedAt, Instant expiresAt) { Map headers = new HashMap<>(); headers.put("alg", "RS256"); + + Map attributes = new HashMap<>(claims); + headers.put(IdTokenClaimNames.IAT, issuedAt); + headers.put(IdTokenClaimNames.EXP, expiresAt); - Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, headers, claims); + Jwt idToken = new Jwt("id-token", headers, Collections.unmodifiableMap(attributes)); JwtDecoder jwtDecoder = mock(JwtDecoder.class); when(jwtDecoder.decode(anyString())).thenReturn(idToken); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java index 937babdea2..f38352e4da 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java @@ -79,9 +79,22 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests { private OAuth2AuthorizationResponse.Builder authorizationResponseBldr = OAuth2AuthorizationResponse .success("code") .state("state"); + + private Map withInstants(final Map claims, final Instant iat, final Instant exp) { + Map attributes = new HashMap<>(claims); + if(iat != null) { + attributes.put(IdTokenClaimNames.IAT, iat); + } + if(exp != null) { + attributes.put(IdTokenClaimNames.EXP, exp); + } + return attributes; + } - private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), - Instant.now().plusSeconds(3600), Collections.singletonMap(IdTokenClaimNames.SUB, "sub123")); + private OidcIdToken idToken = new OidcIdToken("token123", withInstants( + Collections.singletonMap(IdTokenClaimNames.SUB, "sub123"), + Instant.now(), + Instant.now().plusSeconds(3600))); private OidcAuthorizationCodeReactiveAuthenticationManager manager; @@ -167,13 +180,15 @@ public void authenticationWhenOAuth2UserNotFoundThenEmpty() { .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.")) .build(); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); Map claims = new HashMap<>(); claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); claims.put(IdTokenClaimNames.SUB, "rob"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id")); - Instant issuedAt = Instant.now(); - Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); - Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + claims.put(IdTokenClaimNames.IAT, issuedAt); + claims.put(IdTokenClaimNames.EXP, expiresAt); + Jwt idToken = new Jwt("id-token", claims, claims); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); when(this.userService.loadUser(any())).thenReturn(Mono.empty()); @@ -189,13 +204,15 @@ public void authenticationWhenOAuth2UserFoundThenSuccess() { .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue())) .build(); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); Map claims = new HashMap<>(); claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); claims.put(IdTokenClaimNames.SUB, "rob"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id")); - Instant issuedAt = Instant.now(); - Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); - Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + claims.put(IdTokenClaimNames.IAT, issuedAt); + claims.put(IdTokenClaimNames.EXP, expiresAt); + Jwt idToken = new Jwt("id-token", claims, claims); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); @@ -218,13 +235,15 @@ public void authenticationWhenRefreshTokenThenRefreshTokenInAuthorizedClient() { .refreshToken("refresh-token") .build(); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); Map claims = new HashMap<>(); claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); claims.put(IdTokenClaimNames.SUB, "rob"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id")); - Instant issuedAt = Instant.now(); - Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); - Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + claims.put(IdTokenClaimNames.IAT, issuedAt); + claims.put(IdTokenClaimNames.EXP, expiresAt); + Jwt idToken = new Jwt("id-token", claims, claims); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); @@ -253,13 +272,15 @@ public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToU .additionalParameters(additionalParameters) .build(); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); Map claims = new HashMap<>(); claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); claims.put(IdTokenClaimNames.SUB, "rob"); claims.put(IdTokenClaimNames.AUD, Arrays.asList(clientRegistration.getClientId())); - Instant issuedAt = Instant.now(); - Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); - Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + claims.put(IdTokenClaimNames.IAT, issuedAt); + claims.put(IdTokenClaimNames.EXP, expiresAt); + Jwt idToken = new Jwt("id-token", claims, claims); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java index 5ef5f7d25f..e643238649 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java @@ -44,8 +44,7 @@ public class OidcIdTokenValidatorTests { private ClientRegistration.Builder registration = TestClientRegistrations.clientRegistration(); private Map headers = new HashMap<>(); private Map claims = new HashMap<>(); - private Instant issuedAt = Instant.now(); - private Instant expiresAt = this.issuedAt.plusSeconds(3600); + private Instant now = Instant.now(); private Duration clockSkew = Duration.ofSeconds(60); @Before @@ -54,6 +53,8 @@ public void setup() { this.claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); this.claims.put(IdTokenClaimNames.SUB, "rob"); this.claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client-id")); + this.claims.put(IdTokenClaimNames.IAT, now); + this.claims.put(IdTokenClaimNames.EXP, now.plusSeconds(3600)); } @Test @@ -105,7 +106,7 @@ public void validateWhenAudNullThenHasErrors() { @Test public void validateWhenIssuedAtNullThenHasErrors() { - this.issuedAt = null; + this.claims.remove(IdTokenClaimNames.IAT); assertThat(this.validateIdToken()) .hasSize(1) .extracting(OAuth2Error::getDescription) @@ -114,7 +115,7 @@ public void validateWhenIssuedAtNullThenHasErrors() { @Test public void validateWhenExpiresAtNullThenHasErrors() { - this.expiresAt = null; + this.claims.remove(IdTokenClaimNames.EXP); assertThat(this.validateIdToken()) .hasSize(1) .extracting(OAuth2Error::getDescription) @@ -167,16 +168,18 @@ public void validateWhenAudNotClientIdThenHasErrors() { @Test public void validateWhenExpiredAnd60secClockSkewThenNoErrors() { - this.issuedAt = Instant.now().minus(Duration.ofSeconds(60)); - this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(30)); + final Instant now = Instant.now().minus(Duration.ofSeconds(60)); + this.claims.put(IdTokenClaimNames.IAT, now); + this.claims.put(IdTokenClaimNames.EXP, now.plus(Duration.ofSeconds(30))); this.clockSkew = Duration.ofSeconds(60); assertThat(this.validateIdToken()).isEmpty(); } @Test public void validateWhenExpiredAnd0secClockSkewThenHasErrors() { - this.issuedAt = Instant.now().minus(Duration.ofSeconds(60)); - this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(30)); + final Instant now = Instant.now().minus(Duration.ofSeconds(60)); + this.claims.put(IdTokenClaimNames.IAT, now); + this.claims.put(IdTokenClaimNames.EXP, now.plus(Duration.ofSeconds(30))); this.clockSkew = Duration.ofSeconds(0); assertThat(this.validateIdToken()) .hasSize(1) @@ -186,16 +189,18 @@ public void validateWhenExpiredAnd0secClockSkewThenHasErrors() { @Test public void validateWhenIssuedAt5minAheadAnd5minClockSkewThenNoErrors() { - this.issuedAt = Instant.now().plus(Duration.ofMinutes(5)); - this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(60)); + final Instant now = Instant.now().plus(Duration.ofMinutes(5)); + this.claims.put(IdTokenClaimNames.IAT, now); + this.claims.put(IdTokenClaimNames.EXP, now.plus(Duration.ofSeconds(60))); this.clockSkew = Duration.ofMinutes(5); assertThat(this.validateIdToken()).isEmpty(); } @Test public void validateWhenIssuedAt1minAheadAnd0minClockSkewThenHasErrors() { - this.issuedAt = Instant.now().plus(Duration.ofMinutes(1)); - this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(60)); + final Instant now = Instant.now().plus(Duration.ofMinutes(1)); + this.claims.put(IdTokenClaimNames.IAT, now); + this.claims.put(IdTokenClaimNames.EXP, now.plus(Duration.ofSeconds(60))); this.clockSkew = Duration.ofMinutes(0); assertThat(this.validateIdToken()) .hasSize(1) @@ -205,8 +210,9 @@ public void validateWhenIssuedAt1minAheadAnd0minClockSkewThenHasErrors() { @Test public void validateWhenExpiresAtBeforeNowThenHasErrors() { - this.issuedAt = Instant.now().minus(Duration.ofSeconds(10)); - this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(5)); + final Instant now = Instant.now().minus(Duration.ofSeconds(10)); + this.claims.put(IdTokenClaimNames.IAT, now); + this.claims.put(IdTokenClaimNames.EXP, now.plus(Duration.ofSeconds(5))); this.clockSkew = Duration.ofSeconds(0); assertThat(this.validateIdToken()) .hasSize(1) @@ -218,8 +224,8 @@ public void validateWhenExpiresAtBeforeNowThenHasErrors() { public void validateWhenMissingClaimsThenHasErrors() { this.claims.remove(IdTokenClaimNames.SUB); this.claims.remove(IdTokenClaimNames.AUD); - this.issuedAt = null; - this.expiresAt = null; + this.claims.remove(IdTokenClaimNames.IAT); + this.claims.remove(IdTokenClaimNames.EXP); assertThat(this.validateIdToken()) .hasSize(1) .extracting(OAuth2Error::getDescription) @@ -230,7 +236,7 @@ public void validateWhenMissingClaimsThenHasErrors() { } private Collection validateIdToken() { - Jwt idToken = new Jwt("token123", this.issuedAt, this.expiresAt, this.headers, this.claims); + Jwt idToken = new Jwt("token123", this.headers, this.claims); OidcIdTokenValidator validator = new OidcIdTokenValidator(this.registration.build()); validator.setClockSkew(this.clockSkew); return validator.validate(idToken).getErrors(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java index 15d9574aae..70cd0fcbf3 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java @@ -58,15 +58,22 @@ public class OidcReactiveOAuth2UserServiceTests { private ClientRegistration.Builder registration = TestClientRegistrations.clientRegistration() .userNameAttributeName(IdTokenClaimNames.SUB); + + private Map withInstants(final Map claims, final Instant iat, final Instant exp) { + final Map attributes = new HashMap(claims); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } - private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), - Instant.now().plusSeconds(3600), Collections - .singletonMap(IdTokenClaimNames.SUB, "sub123")); + private OidcIdToken idToken = new OidcIdToken("token123", withInstants( + Collections.singletonMap(IdTokenClaimNames.SUB, "sub123"), + Instant.now(), + Instant.now().plusSeconds(3600))); private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", - Instant.now(), - Instant.now().plus(Duration.ofDays(1)), + withInstants(Collections.emptyMap(), Instant.now(), Instant.now().plus(Duration.ofDays(1))), Collections.singleton("read:user")); private OidcReactiveOAuth2UserService userService = new OidcReactiveOAuth2UserService(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java index 770bb0e028..487b591061 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java @@ -26,6 +26,7 @@ import java.time.Instant; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -43,6 +44,13 @@ public class OidcUserRequestTests { private OAuth2AccessToken accessToken; private OidcIdToken idToken; private Map additionalParameters; + + private Map withInstants(final Map claims, final Instant iat, final Instant exp) { + final Map attributes = new HashMap(claims); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } @Before public void setUp() { @@ -59,14 +67,17 @@ public void setUp() { .clientName("Client 1") .build(); this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "access-token-1234", Instant.now(), Instant.now().plusSeconds(60), + "access-token-1234", + withInstants(Collections.emptyMap(), Instant.now(), Instant.now().plusSeconds(60)), new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))); Map claims = new HashMap<>(); claims.put(IdTokenClaimNames.ISS, "https://provider.com"); claims.put(IdTokenClaimNames.SUB, "subject1"); claims.put(IdTokenClaimNames.AZP, "client-1"); - this.idToken = new OidcIdToken("id-token-1234", Instant.now(), - Instant.now().plusSeconds(3600), claims); + this.idToken = new OidcIdToken("id-token-1234", withInstants( + claims, + Instant.now(), + Instant.now().plusSeconds(3600))); this.additionalParameters = new HashMap<>(); this.additionalParameters.put("param1", "value1"); this.additionalParameters.put("param2", "value2"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java index 53ca0a094a..0b72578ebd 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java @@ -27,6 +27,8 @@ import java.time.Duration; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.*; @@ -36,15 +38,24 @@ */ public class OidcUserRequestUtilsTests { private ClientRegistration.Builder registration = TestClientRegistrations.clientRegistration(); + + private Map withInstants(final Map claims, final Instant iat, final Instant exp) { + final Map attributes = new HashMap(claims); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } - OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), - Instant.now().plusSeconds(3600), Collections - .singletonMap(IdTokenClaimNames.SUB, "sub123")); + OidcIdToken idToken = new OidcIdToken("token123", withInstants( + Collections.singletonMap(IdTokenClaimNames.SUB, "sub123"), + Instant.now(), + Instant.now().plusSeconds(3600))); OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "token", - Instant.now(), - Instant.now().plus(Duration.ofDays(1)), + "token", withInstants( + Collections.emptyMap(), + Instant.now(), + Instant.now().plus(Duration.ofDays(1))), Collections.singleton("read:user")); @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index 15d4015a5e..e3e464b0ad 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -17,6 +17,7 @@ import java.time.Instant; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -67,6 +68,13 @@ public class OidcUserServiceTests { @Rule public ExpectedException exception = ExpectedException.none(); + + private Map withInstants(final Map claims, final Instant iat, final Instant exp) { + final Map attributes = new HashMap(claims); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } @Before public void setup() throws Exception { @@ -82,7 +90,7 @@ public void setup() throws Exception { Map idTokenClaims = new HashMap<>(); idTokenClaims.put(IdTokenClaimNames.ISS, "https://provider.com"); idTokenClaims.put(IdTokenClaimNames.SUB, "subject1"); - this.idToken = new OidcIdToken("access-token", Instant.MIN, Instant.MAX, idTokenClaims); + this.idToken = new OidcIdToken("access-token", withInstants(idTokenClaims, Instant.MIN, Instant.MAX)); this.userService.setOauth2UserService(new DefaultOAuth2UserService()); } @@ -118,8 +126,10 @@ public void loadUserWhenAuthorizedScopesDoesNotContainUserInfoScopesThenUserInfo Set authorizedScopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", - Instant.MIN, Instant.MAX, authorizedScopes); + OAuth2AccessToken.TokenType.BEARER, + "access-token", + withInstants(Collections.emptyMap(), Instant.MIN, Instant.MAX), + authorizedScopes); OidcUser user = this.userService.loadUser( new OidcUserRequest(clientRegistration, accessToken, this.idToken)); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java index b856c80252..b2187c5f36 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java @@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; @@ -38,6 +39,8 @@ import java.time.Duration; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.*; @@ -49,9 +52,16 @@ public class DefaultReactiveOAuth2UserServiceTests { private ClientRegistration.Builder clientRegistration; private DefaultReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService(); + + private Map attributes(final Instant iat, final Instant exp) { + final Map attributes = new HashMap(); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } private OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plus(Duration.ofDays(1))); + OAuth2AccessToken.TokenType.BEARER, "access-token", attributes(Instant.now(), Instant.now().plus(Duration.ofDays(1)))); private MockWebServer server; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserServiceTests.java index f43b18a0ae..726c6d6807 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserServiceTests.java @@ -47,7 +47,7 @@ public void constructorWhenUserServicesIsEmptyThenThrowIllegalArgumentException( @SuppressWarnings("unchecked") public void loadUserWhenUserRequestIsNullThenThrowIllegalArgumentException() { DelegatingOAuth2UserService delegatingUserService = - new DelegatingOAuth2UserService<>( + new DelegatingOAuth2UserService( Arrays.asList(mock(OAuth2UserService.class), mock(OAuth2UserService.class))); delegatingUserService.loadUser(null); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestEntityConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestEntityConverterTests.java index 4ad9d4e4aa..88b063bde2 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestEntityConverterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestEntityConverterTests.java @@ -15,6 +15,15 @@ */ package org.springframework.security.oauth2.client.userinfo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; + +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; + import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -25,15 +34,9 @@ import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.util.MultiValueMap; -import java.time.Instant; -import java.util.Arrays; -import java.util.LinkedHashSet; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; - /** * Tests for {@link OAuth2UserRequestEntityConverter}. * @@ -42,7 +45,6 @@ public class OAuth2UserRequestEntityConverterTests { private OAuth2UserRequestEntityConverter converter = new OAuth2UserRequestEntityConverter(); - @SuppressWarnings("unchecked") @Test public void convertWhenAuthenticationMethodHeaderThenGetRequest() { ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); @@ -87,9 +89,14 @@ public void convertWhenAuthenticationMethodFormThenPostRequest() { } private OAuth2AccessToken createAccessToken() { + final Map attributes = new HashMap(); + attributes.put(IdTokenClaimNames.IAT, Instant.now()); + attributes.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600)); OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token-1234", Instant.now(), - Instant.now().plusSeconds(3600), new LinkedHashSet<>(Arrays.asList("read", "write"))); + OAuth2AccessToken.TokenType.BEARER, + "access-token-1234", + attributes, + new LinkedHashSet<>(Arrays.asList("read", "write"))); return accessToken; } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java index 6e2222e5f1..ff50fa8b66 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java @@ -21,6 +21,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import java.time.Instant; import java.util.Arrays; @@ -54,8 +55,13 @@ public void setUp() { .tokenUri("https://provider.com/oauth2/token") .clientName("Client 1") .build(); - this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "access-token-1234", Instant.now(), Instant.now().plusSeconds(60), + final Map attributes = new HashMap(); + attributes.put(IdTokenClaimNames.IAT, Instant.now()); + attributes.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(60)); + this.accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "access-token-1234", + attributes, new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))); this.additionalParameters = new HashMap<>(); this.additionalParameters.put("param1", "value1"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java index 7b8ba1889f..2c70f22b58 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -49,6 +49,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.reactive.function.BodyInserter; @@ -106,13 +107,25 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { private ClientRegistration registration = TestClientRegistrations.clientRegistration() .build(); - private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "token-0", - Instant.now(), - Instant.now().plus(Duration.ofDays(1))); + private Map attributes(final Instant iat, final Instant exp) { + final Map attributes = new HashMap(); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } + + private Map attributes() { + return attributes(Instant.now(), Instant.now().plus(Duration.ofDays(1))); + } + + private OAuth2AccessToken accessToken; @Before public void setup() { + accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token-0", + attributes()); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, this.authorizedClientRepository); } @@ -164,8 +177,7 @@ public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() { OAuth2AccessToken newAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "new-token", - Instant.now(), - Instant.now().plus(Duration.ofDays(1))); + attributes()); OAuth2AuthorizedClient newAuthorizedClient = new OAuth2AuthorizedClient(registration, "principalName", newAccessToken, null); Request r = new Request(clientRegistrationId, authentication, null); @@ -179,8 +191,7 @@ public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() { OAuth2AccessToken accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(registration, @@ -250,10 +261,9 @@ public void filterWhenRefreshRequiredThenRefresh() { this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(issuedAt, null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -301,10 +311,9 @@ public void filterWhenRefreshRequiredThenRefreshAndResponseDoesNotContainRefresh this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(issuedAt, null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -352,10 +361,9 @@ public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(issuedAt, null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -405,7 +413,7 @@ public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { @Test public void filterWhenNotExpiredThenShouldRefreshFalse() { - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(this.accessToken.getIssuedAt(), null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -426,7 +434,7 @@ public void filterWhenNotExpiredThenShouldRefreshFalse() { @Test public void filterWhenClientRegistrationIdThenAuthorizedClientResolved() { - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(this.accessToken.getIssuedAt(), null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.just(authorizedClient)); @@ -450,7 +458,7 @@ public void filterWhenClientRegistrationIdThenAuthorizedClientResolved() { @Test public void filterWhenDefaultClientRegistrationIdThenAuthorizedClientResolved() { this.function.setDefaultClientRegistrationId(this.registration.getRegistrationId()); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(this.accessToken.getIssuedAt(), null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.just(authorizedClient)); @@ -474,7 +482,7 @@ public void filterWhenDefaultClientRegistrationIdThenAuthorizedClientResolved() public void filterWhenClientRegistrationIdFromAuthenticationThenAuthorizedClientResolved() { this.function.setDefaultOAuth2AuthorizedClient(true); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(this.accessToken.getIssuedAt(), null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.just(authorizedClient)); @@ -522,7 +530,7 @@ public void filterWhenDefaultOAuth2AuthorizedClientFalseThenEmpty() { @Test public void filterWhenClientRegistrationIdAndServerWebExchangeFromContextThenServerWebExchangeFromContext() { - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(this.accessToken.getIssuedAt(), null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.just(authorizedClient)); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java index d99de2db28..fe61058549 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -56,6 +56,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -115,10 +116,16 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { private ClientRegistration registration = TestClientRegistrations.clientRegistration() .build(); + private Map attributes(final Instant iat, final Instant exp) { + final Map attributes = new HashMap(); + if(iat != null) attributes.put(IdTokenClaimNames.IAT, iat); + if(exp != null) attributes.put(IdTokenClaimNames.EXP, exp); + return attributes; + } + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token-0", - Instant.now(), - Instant.now().plus(Duration.ofDays(1))); + attributes(Instant.now(), Instant.now().plus(Duration.ofDays(1)))); @Before public void setup() { @@ -390,12 +397,11 @@ public void filterWhenRefreshRequiredThenRefresh() { this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, this.authorizedClientRepository); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(issuedAt, null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -440,12 +446,11 @@ public void filterWhenRefreshRequiredThenRefreshAndResponseDoesNotContainRefresh this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, this.authorizedClientRepository); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(issuedAt, null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -522,8 +527,7 @@ public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() { this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, this.authorizedClientRepository); this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient); @@ -564,12 +568,11 @@ public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), this.accessToken.getTokenValue(), - issuedAt, - accessTokenExpiresAt); + attributes(issuedAt, accessTokenExpiresAt)); this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, this.authorizedClientRepository); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(issuedAt, null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) @@ -625,7 +628,7 @@ public void filterWhenNotExpiredThenShouldRefreshFalse() { this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, this.authorizedClientRepository); - OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", attributes(this.accessToken.getIssuedAt(), null)); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, "principalName", this.accessToken, refreshToken); ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilterTests.java index fd50f3f72a..80b10a8cd5 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilterTests.java @@ -33,6 +33,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.web.server.handler.DefaultWebFilterChain; @@ -41,6 +42,8 @@ import java.time.Duration; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -84,10 +87,12 @@ public void onAuthenticationSuccessWhenOAuth2LoginAuthenticationTokenThenSavesAu } private OAuth2LoginAuthenticationToken loginToken() { + final Map attributes = new HashMap(); + attributes.put(IdTokenClaimNames.IAT, Instant.now()); + attributes.put(IdTokenClaimNames.EXP, Instant.now().plus(Duration.ofDays(1))); OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", - Instant.now(), - Instant.now().plus(Duration.ofDays(1)), + attributes, Collections.singleton("user")); DefaultOAuth2User user = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections .singletonMap("user", "rob"), "user"); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java index e1dc4d5903..696a7e3714 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java @@ -15,13 +15,15 @@ */ package org.springframework.security.oauth2.core; +import java.io.Serializable; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + import org.springframework.lang.Nullable; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.Assert; -import java.io.Serializable; -import java.time.Instant; - /** * Base class for OAuth 2.0 Token implementations. * @@ -32,33 +34,22 @@ public abstract class AbstractOAuth2Token implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final String tokenValue; - private final Instant issuedAt; - private final Instant expiresAt; - - /** - * Sub-class constructor. - * - * @param tokenValue the token value - */ - protected AbstractOAuth2Token(String tokenValue) { - this(tokenValue, null, null); - } + private final Map attributes; /** * Sub-class constructor. * * @param tokenValue the token value - * @param issuedAt the time at which the token was issued, may be null - * @param expiresAt the expiration time on or after which the token MUST NOT be accepted, may be null + * @param attributes the token attributes (AKA claims) */ - protected AbstractOAuth2Token(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt) { + protected AbstractOAuth2Token(final String tokenValue, final Map attributes) { Assert.hasText(tokenValue, "tokenValue cannot be empty"); - if (issuedAt != null && expiresAt != null) { - Assert.isTrue(expiresAt.isAfter(issuedAt), "expiresAt must be after issuedAt"); - } this.tokenValue = tokenValue; - this.issuedAt = issuedAt; - this.expiresAt = expiresAt; + Assert.notEmpty(attributes, "claims cannot be empty"); + this.attributes = Collections.unmodifiableMap(attributes); + if (getIssuedAt() != null && getExpiresAt() != null) { + Assert.isTrue(getExpiresAt().isAfter(getIssuedAt()), "expiresAt must be after issuedAt"); + } } /** @@ -70,49 +61,45 @@ public String getTokenValue() { return this.tokenValue; } + public Map getAttributes() { + return attributes; + } + /** * Returns the time at which the token was issued. * * @return the time the token was issued or null */ - public @Nullable Instant getIssuedAt() { - return this.issuedAt; - } + public abstract @Nullable Instant getIssuedAt(); /** * Returns the expiration time on or after which the token MUST NOT be accepted. * * @return the expiration time of the token or null */ - public @Nullable Instant getExpiresAt() { - return this.expiresAt; - } + public abstract @Nullable Instant getExpiresAt(); @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - - AbstractOAuth2Token that = (AbstractOAuth2Token) obj; - - if (!this.getTokenValue().equals(that.getTokenValue())) { - return false; - } - if (this.getIssuedAt() != null ? !this.getIssuedAt().equals(that.getIssuedAt()) : that.getIssuedAt() != null) { - return false; - } - return this.getExpiresAt() != null ? this.getExpiresAt().equals(that.getExpiresAt()) : that.getExpiresAt() == null; + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((attributes == null) ? 0 : attributes.hashCode()); + result = prime * result + ((tokenValue == null) ? 0 : tokenValue.hashCode()); + return result; } @Override - public int hashCode() { - int result = this.getTokenValue().hashCode(); - result = 31 * result + (this.getIssuedAt() != null ? this.getIssuedAt().hashCode() : 0); - result = 31 * result + (this.getExpiresAt() != null ? this.getExpiresAt().hashCode() : 0); - return result; + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AbstractOAuth2Token other = (AbstractOAuth2Token) obj; + if (attributes == null) { + if (other.attributes != null) return false; + } else if (!attributes.equals(other.attributes)) return false; + if (tokenValue == null) { + if (other.tokenValue != null) return false; + } else if (!tokenValue.equals(other.tokenValue)) return false; + return true; } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index b2ff587f0d..3742c25cf6 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -15,14 +15,16 @@ */ package org.springframework.security.oauth2.core; -import org.springframework.security.core.SpringSecurityCoreVersion; -import org.springframework.util.Assert; - import java.io.Serializable; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Set; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + /** * An implementation of an {@link AbstractOAuth2Token} representing an OAuth 2.0 Access Token. * @@ -36,10 +38,21 @@ * @since 5.0 * @see Section 1.4 Access Token */ -public class OAuth2AccessToken extends AbstractOAuth2Token { +public class OAuth2AccessToken extends AbstractOAuth2Token implements ClaimAccessor { private final TokenType tokenType; private final Set scopes; + /** + * Constructs an {@code OAuth2AccessToken} using the provided parameters. + * + * @param tokenType the token type + * @param tokenValue the token value + * @param attributes the token attributes + */ + public OAuth2AccessToken(final TokenType tokenType, final String tokenValue, final Map attributes) { + this(tokenType, tokenValue, attributes, Collections.emptySet()); + } + /** * Constructs an {@code OAuth2AccessToken} using the provided parameters. * @@ -47,9 +60,18 @@ public class OAuth2AccessToken extends AbstractOAuth2Token { * @param tokenValue the token value * @param issuedAt the time at which the token was issued * @param expiresAt the expiration time on or after which the token MUST NOT be accepted + * @deprecated since 5.2 provide issue and expiration instants as claims. If non null "issuedAt" is provided and "iat" claim is there too, then first wins (claim is overridden). Same for expiration. */ + @Deprecated public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) { - this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet()); + this(tokenType, tokenValue, attributes(issuedAt, expiresAt), Collections.emptySet()); + } + + private static Map attributes(final Instant issuedAt, final Instant expiresAt) { + final Map attributes = new HashMap<>(); + if(issuedAt != null) attributes.put("iat", issuedAt); + if(expiresAt != null) attributes.put("exp", expiresAt); + return attributes; } /** @@ -61,8 +83,8 @@ public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedA * @param expiresAt the expiration time on or after which the token MUST NOT be accepted * @param scopes the scope(s) associated to the token */ - public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, Set scopes) { - super(tokenValue, issuedAt, expiresAt); + public OAuth2AccessToken(TokenType tokenType, String tokenValue, final Map attributes, Set scopes) { + super(tokenValue, attributes); Assert.notNull(tokenType, "tokenType cannot be null"); this.tokenType = tokenType; this.scopes = Collections.unmodifiableSet( @@ -128,4 +150,19 @@ public int hashCode() { return this.getValue().hashCode(); } } + + @Override + public Instant getIssuedAt() { + return getClaimAsInstant("iat"); + } + + @Override + public Instant getExpiresAt() { + return getClaimAsInstant("exp"); + } + + @Override + public Map getClaims() { + return getAttributes(); + } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java index e52f364399..f19fd39a6a 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.core; import java.time.Instant; +import java.util.Collections; +import java.util.Map; /** * An implementation of an {@link AbstractOAuth2Token} representing an OAuth 2.0 Refresh Token. @@ -33,6 +35,16 @@ */ public class OAuth2RefreshToken extends AbstractOAuth2Token { + /** + * Constructs an {@code OAuth2RefreshToken} using the provided parameters. + * + * @param tokenValue the token value + * @param attributes the troken attributes + */ + public OAuth2RefreshToken(final String tokenValue, final Map attributes) { + super(tokenValue, attributes); + } + /** * Constructs an {@code OAuth2RefreshToken} using the provided parameters. * @@ -40,6 +52,18 @@ public class OAuth2RefreshToken extends AbstractOAuth2Token { * @param issuedAt the time at which the token was issued */ public OAuth2RefreshToken(String tokenValue, Instant issuedAt) { - super(tokenValue, issuedAt, null); + super(tokenValue, issuedAt != null ? Collections.singletonMap("iat", issuedAt) : Collections.emptyMap()); + } + + @Override + public Instant getIssuedAt() { + Object value = getAttributes().get("iat"); + return value instanceof Instant ? (Instant)value : null; + } + + @Override + public Instant getExpiresAt() { + Object value = getAttributes().get("exp"); + return value instanceof Instant ? (Instant)value : null; } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 55488926cb..7fa6e7ce37 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -23,6 +23,7 @@ import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -181,15 +182,15 @@ public Builder additionalParameters(Map additionalParameters) { * @return a {@link OAuth2AccessTokenResponse} */ public OAuth2AccessTokenResponse build() { - Instant issuedAt = getIssuedAt(); - - Instant expiresAt = getExpiresAt(); + final Map attributes = new HashMap<>(); + attributes.put("iat", getIssuedAt()); + attributes.put("exp", getExpiresAt()); OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse(); accessTokenResponse.accessToken = new OAuth2AccessToken( - this.tokenType, this.tokenValue, issuedAt, expiresAt, this.scopes); + this.tokenType, this.tokenValue, attributes, this.scopes); if (StringUtils.hasText(this.refreshToken)) { - accessTokenResponse.refreshToken = new OAuth2RefreshToken(this.refreshToken, issuedAt); + accessTokenResponse.refreshToken = new OAuth2RefreshToken(this.refreshToken, attributes); } accessTokenResponse.additionalParameters = Collections.unmodifiableMap( CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() : this.additionalParameters); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java index 6e1297862d..c23c8a9886 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java @@ -15,14 +15,12 @@ */ package org.springframework.security.oauth2.core.oidc; -import org.springframework.security.oauth2.core.AbstractOAuth2Token; -import org.springframework.util.Assert; - import java.time.Instant; -import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.Map; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; + /** * An implementation of an {@link AbstractOAuth2Token} representing an OpenID Connect Core 1.0 ID Token. * @@ -39,8 +37,17 @@ * @see Standard Claims */ public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAccessor { - private final Map claims; + /** + * Constructs a {@code OidcIdToken} using the provided parameters. + * + * @param tokenValue the ID Token value + * @param claims the claims about the authentication of the End-User + */ + public OidcIdToken(final String tokenValue, final Map claims) { + super(tokenValue, claims); + } + /** * Constructs a {@code OidcIdToken} using the provided parameters. * @@ -48,15 +55,32 @@ public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAcce * @param issuedAt the time at which the ID Token was issued {@code (iat)} * @param expiresAt the expiration time {@code (exp)} on or after which the ID Token MUST NOT be accepted * @param claims the claims about the authentication of the End-User + * @deprecated provide issue and expiration instants as claims. If non null "issuedAt" is provided and "iat" claim is there too, then first wins (claim is overridden). Same for expiration. */ + @Deprecated public OidcIdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map claims) { - super(tokenValue, issuedAt, expiresAt); - Assert.notEmpty(claims, "claims cannot be empty"); - this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + this(tokenValue, withInstants(claims, issuedAt, expiresAt)); + } + + private static Map withInstants(final Map claims, final Instant issuedAt, final Instant expiresAt) { + final Map attributes = new HashMap<>(claims); + if(issuedAt != null) attributes.put(IdTokenClaimNames.IAT, issuedAt); + if(expiresAt != null) attributes.put(IdTokenClaimNames.EXP, expiresAt); + return attributes; } @Override public Map getClaims() { - return this.claims; + return getAttributes(); + } + + @Override + public Instant getIssuedAt() { + return getClaimAsInstant(IdTokenClaimNames.IAT); + } + + @Override + public Instant getExpiresAt() { + return getClaimAsInstant(IdTokenClaimNames.EXP); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java index ccede95337..5ce19c03bc 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java @@ -15,15 +15,19 @@ */ package org.springframework.security.oauth2.core; -import org.junit.Test; -import org.springframework.util.SerializationUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.byLessThan; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; +import org.springframework.util.SerializationUtils; /** * Tests for {@link OAuth2AccessToken}. @@ -36,6 +40,17 @@ public class OAuth2AccessTokenTests { private static final Instant ISSUED_AT = Instant.now(); private static final Instant EXPIRES_AT = Instant.from(ISSUED_AT).plusSeconds(60); private static final Set SCOPES = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + + static Map attributes(final Instant iat, final Instant exp){ + final Map attributes = new HashMap<>(); + attributes.put("iat", iat.getEpochSecond()); + attributes.put("exp", exp.getEpochSecond()); + return attributes; + } + + static Map attributes(){ + return attributes(ISSUED_AT, EXPIRES_AT); + } @Test public void tokenTypeGetValueWhenTokenTypeBearerThenReturnBearer() { @@ -44,33 +59,33 @@ public void tokenTypeGetValueWhenTokenTypeBearerThenReturnBearer() { @Test(expected = IllegalArgumentException.class) public void constructorWhenTokenTypeIsNullThenThrowIllegalArgumentException() { - new OAuth2AccessToken(null, TOKEN_VALUE, ISSUED_AT, EXPIRES_AT); + new OAuth2AccessToken(null, TOKEN_VALUE, attributes()); } @Test(expected = IllegalArgumentException.class) public void constructorWhenTokenValueIsNullThenThrowIllegalArgumentException() { - new OAuth2AccessToken(TOKEN_TYPE, null, ISSUED_AT, EXPIRES_AT); + new OAuth2AccessToken(TOKEN_TYPE, null, attributes()); } @Test(expected = IllegalArgumentException.class) public void constructorWhenIssuedAtAfterExpiresAtThenThrowIllegalArgumentException() { - new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, Instant.from(EXPIRES_AT).plusSeconds(1), EXPIRES_AT); + new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, attributes(Instant.from(EXPIRES_AT).plusSeconds(1), EXPIRES_AT)); } @Test(expected = IllegalArgumentException.class) public void constructorWhenExpiresAtBeforeIssuedAtThenThrowIllegalArgumentException() { - new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, ISSUED_AT, Instant.from(ISSUED_AT).minusSeconds(1)); + new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, attributes(ISSUED_AT, Instant.from(ISSUED_AT).minusSeconds(1))); } @Test public void constructorWhenAllParametersProvidedAndValidThenCreated() { OAuth2AccessToken accessToken = new OAuth2AccessToken( - TOKEN_TYPE, TOKEN_VALUE, ISSUED_AT, EXPIRES_AT, SCOPES); + TOKEN_TYPE, TOKEN_VALUE, attributes(), SCOPES); assertThat(accessToken.getTokenType()).isEqualTo(TOKEN_TYPE); assertThat(accessToken.getTokenValue()).isEqualTo(TOKEN_VALUE); - assertThat(accessToken.getIssuedAt()).isEqualTo(ISSUED_AT); - assertThat(accessToken.getExpiresAt()).isEqualTo(EXPIRES_AT); + assertThat(accessToken.getIssuedAt()).isCloseTo(ISSUED_AT, byLessThan(1, ChronoUnit.SECONDS)); + assertThat(accessToken.getExpiresAt()).isCloseTo(EXPIRES_AT, byLessThan(1, ChronoUnit.SECONDS)); assertThat(accessToken.getScopes()).isEqualTo(SCOPES); } @@ -78,15 +93,15 @@ public void constructorWhenAllParametersProvidedAndValidThenCreated() { @Test public void constructorWhenCreatedThenIsSerializableAndDeserializable() { OAuth2AccessToken accessToken = new OAuth2AccessToken( - TOKEN_TYPE, TOKEN_VALUE, ISSUED_AT, EXPIRES_AT, SCOPES); + TOKEN_TYPE, TOKEN_VALUE, attributes(), SCOPES); byte[] serialized = SerializationUtils.serialize(accessToken); accessToken = (OAuth2AccessToken) SerializationUtils.deserialize(serialized); assertThat(serialized).isNotNull(); assertThat(accessToken.getTokenType()).isEqualTo(TOKEN_TYPE); assertThat(accessToken.getTokenValue()).isEqualTo(TOKEN_VALUE); - assertThat(accessToken.getIssuedAt()).isEqualTo(ISSUED_AT); - assertThat(accessToken.getExpiresAt()).isEqualTo(EXPIRES_AT); + assertThat(accessToken.getIssuedAt()).isCloseTo(ISSUED_AT, byLessThan(1, ChronoUnit.SECONDS)); + assertThat(accessToken.getExpiresAt()).isCloseTo(EXPIRES_AT, byLessThan(1, ChronoUnit.SECONDS)); assertThat(accessToken.getScopes()).isEqualTo(SCOPES); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AccessTokens.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AccessTokens.java index 62ca87ff48..7fc6afbe48 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AccessTokens.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AccessTokens.java @@ -19,25 +19,33 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; /** * @author Rob Winch * @since 5.1 */ public class TestOAuth2AccessTokens { + + static Map attributes(){ + final Map attributes = new HashMap<>(); + attributes.put("iat", Instant.now()); + attributes.put("exp", Instant.now().plus(Duration.ofDays(1))); + return attributes; + } + public static OAuth2AccessToken noScopes() { return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "no-scopes", - Instant.now(), - Instant.now().plus(Duration.ofDays(1))); + attributes()); } public static OAuth2AccessToken scopes(String... scopes) { return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", - Instant.now(), - Instant.now().plus(Duration.ofDays(1)), + attributes(), new HashSet<>(Arrays.asList(scopes))); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2RefreshTokens.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2RefreshTokens.java index 8face452f5..bb68e53f7b 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2RefreshTokens.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/TestOAuth2RefreshTokens.java @@ -17,13 +17,15 @@ package org.springframework.security.oauth2.core; import java.time.Instant; +import java.util.Collections; /** * @author Rob Winch * @since 5.1 */ public class TestOAuth2RefreshTokens { + public static OAuth2RefreshToken refreshToken() { - return new OAuth2RefreshToken("refresh-token", Instant.now()); + return new OAuth2RefreshToken("refresh-token", Collections.singletonMap("iat", Instant.now())); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenTests.java index 15a04e6b0a..959e0fb764 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenTests.java @@ -48,9 +48,9 @@ public class OidcIdTokenTests { private static final String ISS_VALUE = "https://provider.com"; private static final String SUB_VALUE = "subject1"; private static final List AUD_VALUE = Arrays.asList("aud1", "aud2"); - private static final long IAT_VALUE = Instant.now().toEpochMilli(); - private static final long EXP_VALUE = Instant.now().plusSeconds(60).toEpochMilli(); - private static final long AUTH_TIME_VALUE = Instant.now().minusSeconds(5).toEpochMilli(); + private static final long IAT_VALUE = Instant.now().getEpochSecond(); + private static final long EXP_VALUE = Instant.now().plusSeconds(60).getEpochSecond(); + private static final long AUTH_TIME_VALUE = Instant.now().minusSeconds(5).getEpochSecond(); private static final String NONCE_VALUE = "nonce"; private static final String ACR_VALUE = "acr"; private static final List AMR_VALUE = Arrays.asList("amr1", "amr2"); @@ -79,27 +79,25 @@ public class OidcIdTokenTests { @Test(expected = IllegalArgumentException.class) public void constructorWhenTokenValueIsNullThenThrowIllegalArgumentException() { - new OidcIdToken(null, Instant.ofEpochMilli(IAT_VALUE), Instant.ofEpochMilli(EXP_VALUE), CLAIMS); + new OidcIdToken(null, CLAIMS); } @Test(expected = IllegalArgumentException.class) public void constructorWhenClaimsIsEmptyThenThrowIllegalArgumentException() { - new OidcIdToken(ID_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), - Instant.ofEpochMilli(EXP_VALUE), Collections.emptyMap()); + new OidcIdToken(ID_TOKEN_VALUE, Collections.emptyMap()); } @Test public void constructorWhenParametersProvidedAndValidThenCreated() { - OidcIdToken idToken = new OidcIdToken(ID_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), - Instant.ofEpochMilli(EXP_VALUE), CLAIMS); + OidcIdToken idToken = new OidcIdToken(ID_TOKEN_VALUE, CLAIMS); assertThat(idToken.getClaims()).isEqualTo(CLAIMS); assertThat(idToken.getTokenValue()).isEqualTo(ID_TOKEN_VALUE); assertThat(idToken.getIssuer().toString()).isEqualTo(ISS_VALUE); assertThat(idToken.getSubject()).isEqualTo(SUB_VALUE); assertThat(idToken.getAudience()).isEqualTo(AUD_VALUE); - assertThat(idToken.getIssuedAt().toEpochMilli()).isEqualTo(IAT_VALUE); - assertThat(idToken.getExpiresAt().toEpochMilli()).isEqualTo(EXP_VALUE); + assertThat(idToken.getIssuedAt().getEpochSecond()).isEqualTo(IAT_VALUE); + assertThat(idToken.getExpiresAt().getEpochSecond()).isEqualTo(EXP_VALUE); assertThat(idToken.getAuthenticatedAt().getEpochSecond()).isEqualTo(AUTH_TIME_VALUE); assertThat(idToken.getNonce()).isEqualTo(NONCE_VALUE); assertThat(idToken.getAuthenticationContextClass()).isEqualTo(ACR_VALUE); diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUserTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUserTests.java index 2fad69684f..f909705e62 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUserTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUserTests.java @@ -50,11 +50,13 @@ public class DefaultOidcUserTests { static { ID_TOKEN_CLAIMS.put(IdTokenClaimNames.ISS, "https://example.com"); ID_TOKEN_CLAIMS.put(IdTokenClaimNames.SUB, SUBJECT); + ID_TOKEN_CLAIMS.put(IdTokenClaimNames.IAT, Instant.EPOCH); + ID_TOKEN_CLAIMS.put(IdTokenClaimNames.EXP, Instant.MAX); USER_INFO_CLAIMS.put(StandardClaimNames.NAME, NAME); USER_INFO_CLAIMS.put(StandardClaimNames.EMAIL, EMAIL); } - private static final OidcIdToken ID_TOKEN = new OidcIdToken("id-token-value", Instant.EPOCH, Instant.MAX, ID_TOKEN_CLAIMS); + private static final OidcIdToken ID_TOKEN = new OidcIdToken("id-token-value", ID_TOKEN_CLAIMS); private static final OidcUserInfo USER_INFO = new OidcUserInfo(USER_INFO_CLAIMS); @Test(expected = IllegalArgumentException.class) @@ -76,24 +78,24 @@ public void constructorWhenNameAttributeKeyInvalidThenThrowIllegalArgumentExcept public void constructorWhenAuthoritiesIdTokenProvidedThenCreated() { DefaultOidcUser user = new DefaultOidcUser(AUTHORITIES, ID_TOKEN); - assertThat(user.getClaims()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB); + assertThat(user.getClaims()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); assertThat(user.getIdToken()).isEqualTo(ID_TOKEN); assertThat(user.getName()).isEqualTo(SUBJECT); assertThat(user.getAuthorities()).hasSize(1); assertThat(user.getAuthorities().iterator().next()).isEqualTo(AUTHORITY); - assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB); + assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); } @Test public void constructorWhenAuthoritiesIdTokenNameAttributeKeyProvidedThenCreated() { DefaultOidcUser user = new DefaultOidcUser(AUTHORITIES, ID_TOKEN, IdTokenClaimNames.SUB); - assertThat(user.getClaims()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB); + assertThat(user.getClaims()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); assertThat(user.getIdToken()).isEqualTo(ID_TOKEN); assertThat(user.getName()).isEqualTo(SUBJECT); assertThat(user.getAuthorities()).hasSize(1); assertThat(user.getAuthorities().iterator().next()).isEqualTo(AUTHORITY); - assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB); + assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); } @Test @@ -101,14 +103,14 @@ public void constructorWhenAuthoritiesIdTokenUserInfoProvidedThenCreated() { DefaultOidcUser user = new DefaultOidcUser(AUTHORITIES, ID_TOKEN, USER_INFO); assertThat(user.getClaims()).containsOnlyKeys( - IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL); + IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); assertThat(user.getIdToken()).isEqualTo(ID_TOKEN); assertThat(user.getUserInfo()).isEqualTo(USER_INFO); assertThat(user.getName()).isEqualTo(SUBJECT); assertThat(user.getAuthorities()).hasSize(1); assertThat(user.getAuthorities().iterator().next()).isEqualTo(AUTHORITY); assertThat(user.getAttributes()).containsOnlyKeys( - IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL); + IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); } @Test @@ -116,13 +118,13 @@ public void constructorWhenAllParametersProvidedAndValidThenCreated() { DefaultOidcUser user = new DefaultOidcUser(AUTHORITIES, ID_TOKEN, USER_INFO, StandardClaimNames.EMAIL); assertThat(user.getClaims()).containsOnlyKeys( - IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL); + IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); assertThat(user.getIdToken()).isEqualTo(ID_TOKEN); assertThat(user.getUserInfo()).isEqualTo(USER_INFO); assertThat(user.getName()).isEqualTo(EMAIL); assertThat(user.getAuthorities()).hasSize(1); assertThat(user.getAuthorities().iterator().next()).isEqualTo(AUTHORITY); assertThat(user.getAttributes()).containsOnlyKeys( - IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL); + IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthorityTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthorityTests.java index d859b74d56..99ec2bea2e 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthorityTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthorityTests.java @@ -44,11 +44,13 @@ public class OidcUserAuthorityTests { static { ID_TOKEN_CLAIMS.put(IdTokenClaimNames.ISS, "https://example.com"); ID_TOKEN_CLAIMS.put(IdTokenClaimNames.SUB, SUBJECT); + ID_TOKEN_CLAIMS.put(IdTokenClaimNames.IAT, Instant.EPOCH); + ID_TOKEN_CLAIMS.put(IdTokenClaimNames.EXP, Instant.MAX); USER_INFO_CLAIMS.put(StandardClaimNames.NAME, NAME); USER_INFO_CLAIMS.put(StandardClaimNames.EMAIL, EMAIL); } - private static final OidcIdToken ID_TOKEN = new OidcIdToken("id-token-value", Instant.EPOCH, Instant.MAX, ID_TOKEN_CLAIMS); + private static final OidcIdToken ID_TOKEN = new OidcIdToken("id-token-value", ID_TOKEN_CLAIMS); private static final OidcUserInfo USER_INFO = new OidcUserInfo(USER_INFO_CLAIMS); @Test(expected = IllegalArgumentException.class) @@ -74,6 +76,6 @@ public void constructorWhenAllParametersProvidedAndValidThenCreated() { assertThat(userAuthority.getUserInfo()).isEqualTo(USER_INFO); assertThat(userAuthority.getAuthority()).isEqualTo(AUTHORITY); assertThat(userAuthority.getAttributes()).containsOnlyKeys( - IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL); + IdTokenClaimNames.ISS, IdTokenClaimNames.SUB, StandardClaimNames.NAME, StandardClaimNames.EMAIL, IdTokenClaimNames.IAT, IdTokenClaimNames.EXP); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java index bdfe6abe60..dca7119bdb 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java @@ -42,6 +42,8 @@ private static OidcIdToken idToken() { claims.put(IdTokenClaimNames.ISS, "http://localhost/issuer"); claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client")); claims.put(IdTokenClaimNames.AZP, "client"); - return new OidcIdToken("id-token", Instant.now(), Instant.now().plusSeconds(3600), claims); + claims.put(IdTokenClaimNames.IAT, Instant.now()); + claims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600)); + return new OidcIdToken("id-token", claims); } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java index 6e62372dea..8a41120288 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java @@ -15,14 +15,15 @@ */ package org.springframework.security.oauth2.jwt; -import org.springframework.security.oauth2.core.AbstractOAuth2Token; -import org.springframework.util.Assert; - import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.util.Assert; + /** * An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT). * @@ -42,24 +43,42 @@ */ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor { private final Map headers; - private final Map claims; /** * Constructs a {@code Jwt} using the provided parameters. * * @param tokenValue the token value - * @param issuedAt the time at which the JWT was issued - * @param expiresAt the expiration time on or after which the JWT MUST NOT be accepted * @param headers the JOSE header(s) * @param claims the JWT Claims Set */ - public Jwt(String tokenValue, Instant issuedAt, Instant expiresAt, - Map headers, Map claims) { - super(tokenValue, issuedAt, expiresAt); + public Jwt(String tokenValue, Map headers, Map claims) { + super(tokenValue, claims); Assert.notEmpty(headers, "headers cannot be empty"); Assert.notEmpty(claims, "claims cannot be empty"); this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); - this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + } + + /** + * Constructs a {@code Jwt} using the provided parameters. + * + * @param tokenValue the token value + * @param issuedAt the time at which the JWT was issued + * @param expiresAt the expiration time on or after which the JWT MUST NOT be accepted + * @param headers the JOSE header(s) + * @param claims the JWT Claims Set + * @deprecated since 5.2 provide issue and expiration instants as claims. If non null "issuedAt" is provided and "iat" claim is there too, then first wins (claim is overridden). Same for expiration. + */ + @Deprecated + public Jwt(final String tokenValue, final Instant issuedAt, final Instant expiresAt, + final Map headers, final Map claims) { + this(tokenValue, headers, withInstants(claims, issuedAt, expiresAt)); + } + + private static Map withInstants(final Map claims, final Instant issuedAt, final Instant expiresAt) { + final Map attributes = new HashMap<>(claims); + if(issuedAt != null) attributes.put(JwtClaimNames.IAT, issuedAt); + if(expiresAt != null) attributes.put(JwtClaimNames.EXP, expiresAt); + return attributes; } /** @@ -78,6 +97,16 @@ public Map getHeaders() { */ @Override public Map getClaims() { - return this.claims; + return getAttributes(); + } + + @Override + public Instant getIssuedAt() { + return this.getClaimAsInstant(JwtClaimNames.IAT); + } + + @Override + public Instant getExpiresAt() { + return this.getClaimAsInstant(JwtClaimNames.EXP); } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index 0ffcfe4bb8..7704c53ab4 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -16,6 +16,32 @@ package org.springframework.security.oauth2.jwt; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.util.Assert; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.RemoteKeySourceException; import com.nimbusds.jose.jwk.JWKSet; @@ -36,31 +62,6 @@ import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.jwt.proc.JWTProcessor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.util.Assert; -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; - -import javax.crypto.SecretKey; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.interfaces.RSAPublicKey; -import java.text.ParseException; -import java.time.Instant; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; /** * A low-level Nimbus implementation of {@link JwtDecoder} which takes a raw Nimbus configuration. @@ -144,9 +145,7 @@ private Jwt createJwt(String token, JWT parsedJwt) { Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); Map claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims()); - Instant expiresAt = (Instant) claims.get(JwtClaimNames.EXP); - Instant issuedAt = (Instant) claims.get(JwtClaimNames.IAT); - jwt = new Jwt(token, issuedAt, expiresAt, headers, claims); + jwt = new Jwt(token, headers, claims); } catch (RemoteKeySourceException ex) { if (ex.getCause() instanceof ParseException) { throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set")); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 5eb815a5e3..b302507d3b 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -15,6 +15,23 @@ */ package org.springframework.security.oauth2.jwt; +import java.security.interfaces.RSAPublicKey; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import javax.crypto.SecretKey; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.util.Assert; +import org.springframework.web.reactive.function.client.WebClient; + import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; @@ -38,25 +55,10 @@ import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.jwt.proc.JWTProcessor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.util.Assert; -import org.springframework.web.reactive.function.client.WebClient; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import javax.crypto.SecretKey; -import java.security.interfaces.RSAPublicKey; -import java.time.Instant; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.Function; - /** * An implementation of a {@link ReactiveJwtDecoder} that "decodes" a * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a @@ -162,9 +164,7 @@ private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); Map claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims()); - Instant expiresAt = (Instant) claims.get(JwtClaimNames.EXP); - Instant issuedAt = (Instant) claims.get(JwtClaimNames.IAT); - return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, claims); + return new Jwt(parsedJwt.getParsedString(), headers, claims); } private Jwt validateJwt(Jwt jwt) { diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java index 85f3ad30e5..9b10c28649 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java @@ -17,6 +17,7 @@ import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -46,10 +47,8 @@ public class JwtIssuerValidatorTests { public void validateWhenIssuerMatchesThenReturnsSuccess() { Jwt jwt = new Jwt( MOCK_TOKEN, - MOCK_ISSUED_AT, - MOCK_EXPIRES_AT, MOCK_HEADERS, - Collections.singletonMap("iss", ISSUER)); + addIssueInstants(Collections.singletonMap("iss", ISSUER))); assertThat(this.validator.validate(jwt)) .isEqualTo(OAuth2TokenValidatorResult.success()); @@ -59,10 +58,8 @@ public void validateWhenIssuerMatchesThenReturnsSuccess() { public void validateWhenIssuerMismatchesThenReturnsError() { Jwt jwt = new Jwt( MOCK_TOKEN, - MOCK_ISSUED_AT, - MOCK_EXPIRES_AT, MOCK_HEADERS, - Collections.singletonMap(JwtClaimNames.ISS, "https://other")); + addIssueInstants(Collections.singletonMap(JwtClaimNames.ISS, "https://other"))); OAuth2TokenValidatorResult result = this.validator.validate(jwt); @@ -73,10 +70,8 @@ public void validateWhenIssuerMismatchesThenReturnsError() { public void validateWhenJwtHasNoIssuerThenReturnsError() { Jwt jwt = new Jwt( MOCK_TOKEN, - MOCK_ISSUED_AT, - MOCK_EXPIRES_AT, MOCK_HEADERS, - Collections.singletonMap(JwtClaimNames.AUD, "https://aud")); + addIssueInstants(Collections.singletonMap(JwtClaimNames.AUD, "https://aud"))); OAuth2TokenValidatorResult result = this.validator.validate(jwt); assertThat(result.getErrors()).isNotEmpty(); @@ -87,10 +82,8 @@ public void validateWhenJwtHasNoIssuerThenReturnsError() { public void validateWhenIssuerMatchesAndIsNotAUriThenReturnsSuccess() { Jwt jwt = new Jwt( MOCK_TOKEN, - MOCK_ISSUED_AT, - MOCK_EXPIRES_AT, MOCK_HEADERS, - Collections.singletonMap(JwtClaimNames.ISS, "issuer")); + addIssueInstants(Collections.singletonMap(JwtClaimNames.ISS, "issuer"))); JwtIssuerValidator validator = new JwtIssuerValidator("issuer"); assertThat(validator.validate(jwt)) @@ -108,4 +101,11 @@ public void constructorWhenNullIssuerIsGivenThenThrowsIllegalArgumentException() assertThatCode(() -> new JwtIssuerValidator(null)) .isInstanceOf(IllegalArgumentException.class); } + + private Map addIssueInstants(final Map claims) { + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, MOCK_ISSUED_AT); + attributes.put(JwtClaimNames.EXP, MOCK_EXPIRES_AT); + return attributes; + } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTests.java index e59716da74..8ada32720e 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTests.java @@ -69,29 +69,26 @@ public class JwtTests { @Test(expected = IllegalArgumentException.class) public void constructorWhenTokenValueIsNullThenThrowIllegalArgumentException() { - new Jwt(null, Instant.ofEpochMilli(IAT_VALUE), Instant.ofEpochMilli(EXP_VALUE), HEADERS, CLAIMS); + new Jwt(null, HEADERS, addIssueInstants(CLAIMS)); } @Test(expected = IllegalArgumentException.class) public void constructorWhenHeadersIsEmptyThenThrowIllegalArgumentException() { - new Jwt(JWT_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), - Instant.ofEpochMilli(EXP_VALUE), Collections.emptyMap(), CLAIMS); + new Jwt(JWT_TOKEN_VALUE, Collections.emptyMap(), addIssueInstants(CLAIMS)); } @Test(expected = IllegalArgumentException.class) public void constructorWhenClaimsIsEmptyThenThrowIllegalArgumentException() { - new Jwt(JWT_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), - Instant.ofEpochMilli(EXP_VALUE), HEADERS, Collections.emptyMap()); + new Jwt(JWT_TOKEN_VALUE, HEADERS, Collections.emptyMap()); } @Test public void constructorWhenParametersProvidedAndValidThenCreated() { - Jwt jwt = new Jwt(JWT_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), - Instant.ofEpochMilli(EXP_VALUE), HEADERS, CLAIMS); + Jwt jwt = new Jwt(JWT_TOKEN_VALUE, HEADERS, addIssueInstants(CLAIMS)); assertThat(jwt.getTokenValue()).isEqualTo(JWT_TOKEN_VALUE); assertThat(jwt.getHeaders()).isEqualTo(HEADERS); - assertThat(jwt.getClaims()).isEqualTo(CLAIMS); + assertThat(jwt.getClaims()).isEqualTo(addIssueInstants(CLAIMS)); assertThat(jwt.getIssuer().toString()).isEqualTo(ISS_VALUE); assertThat(jwt.getSubject()).isEqualTo(SUB_VALUE); assertThat(jwt.getAudience()).isEqualTo(AUD_VALUE); @@ -100,4 +97,11 @@ public void constructorWhenParametersProvidedAndValidThenCreated() { assertThat(jwt.getIssuedAt().toEpochMilli()).isEqualTo(IAT_VALUE); assertThat(jwt.getId()).isEqualTo(JTI_VALUE); } + + private Map addIssueInstants(final Map claims) { + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.ofEpochMilli(IAT_VALUE)); + attributes.put(JwtClaimNames.EXP, Instant.ofEpochMilli(EXP_VALUE)); + return attributes; + } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java index 2101d22eca..335c631d94 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -21,6 +21,7 @@ import java.time.ZoneId; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @@ -54,10 +55,8 @@ public void validateWhenJwtIsExpiredThenErrorMessageIndicatesExpirationTime() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - oneHourAgo, MOCK_HEADER, - MOCK_CLAIM_SET); + addIssueInstants(MOCK_CLAIM_SET, oneHourAgo)); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); @@ -73,10 +72,8 @@ public void validateWhenJwtIsTooEarlyThenErrorMessageIndicatesNotBeforeTime() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - null, MOCK_HEADER, - Collections.singletonMap(JwtClaimNames.NBF, oneHourFromNow)); + addIssueInstants(Collections.singletonMap(JwtClaimNames.NBF, oneHourFromNow), null)); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); @@ -99,19 +96,15 @@ public void validateWhenConfiguredWithClockSkewThenValidatesUsingThatSkew() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - almostOneDayAgo, MOCK_HEADER, - Collections.singletonMap(JwtClaimNames.NBF, almostOneDayFromNow)); + addIssueInstants(Collections.singletonMap(JwtClaimNames.NBF, almostOneDayFromNow), almostOneDayAgo)); assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - justOverOneDayAgo, MOCK_HEADER, - MOCK_CLAIM_SET); + addIssueInstants(MOCK_CLAIM_SET, justOverOneDayAgo)); OAuth2TokenValidatorResult result = jwtValidator.validate(jwt); Collection messages = @@ -122,10 +115,8 @@ public void validateWhenConfiguredWithClockSkewThenValidatesUsingThatSkew() { jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - null, MOCK_HEADER, - Collections.singletonMap(JwtClaimNames.NBF, justOverOneDayFromNow)); + addIssueInstants(Collections.singletonMap(JwtClaimNames.NBF, justOverOneDayFromNow), null)); result = jwtValidator.validate(jwt); messages = @@ -140,10 +131,8 @@ public void validateWhenConfiguredWithClockSkewThenValidatesUsingThatSkew() { public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - Instant.now(MOCK_NOW), MOCK_HEADER, - Collections.singletonMap("some", "claim")); + addIssueInstants(Collections.singletonMap("some", "claim"), Instant.now(MOCK_NOW))); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0)); jwtValidator.setClock(MOCK_NOW); @@ -152,10 +141,8 @@ public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() { jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - null, MOCK_HEADER, - Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW))); + addIssueInstants(Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW)), null)); assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); } @@ -164,10 +151,8 @@ public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() { public void validateWhenNeitherExpiryNorNotBeforeIsSpecifiedThenReturnsSuccessfulResult() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - null, MOCK_HEADER, - MOCK_CLAIM_SET); + addIssueInstants(MOCK_CLAIM_SET, null)); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); @@ -177,10 +162,8 @@ public void validateWhenNeitherExpiryNorNotBeforeIsSpecifiedThenReturnsSuccessfu public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSuccessfulResult() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - null, MOCK_HEADER, - Collections.singletonMap(JwtClaimNames.NBF, Instant.MIN)); + addIssueInstants(Collections.singletonMap(JwtClaimNames.NBF, Instant.MIN), null)); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); @@ -190,10 +173,8 @@ public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSucces public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - Instant.MAX, MOCK_HEADER, - MOCK_CLAIM_SET); + addIssueInstants(MOCK_CLAIM_SET, Instant.MAX)); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); @@ -203,10 +184,8 @@ public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSucces public void validateWhenBothExpiryAndNotBeforeAreValidThenReturnsSuccessfulResult() { Jwt jwt = new Jwt( MOCK_TOKEN_VALUE, - MOCK_ISSUED_AT, - Instant.now(MOCK_NOW), MOCK_HEADER, - Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW))); + addIssueInstants(Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW)), Instant.now(MOCK_NOW))); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0)); jwtValidator.setClock(MOCK_NOW); @@ -227,4 +206,19 @@ public void constructorWhenInvokedWithNullDurationThenThrowsIllegalArgumentExcep assertThatCode(() -> new JwtTimestampValidator(null)) .isInstanceOf(IllegalArgumentException.class); } + + private Map addIssueInstants(final Map claims, final Instant iat, final Instant exp) { + Map attributes = new HashMap<>(claims); + if(iat != null) { + attributes.put(JwtClaimNames.IAT, iat); + } + if(exp != null) { + attributes.put(JwtClaimNames.EXP, exp); + } + return attributes; + } + + private Map addIssueInstants(final Map claims, final Instant exp) { + return addIssueInstants(claims, MOCK_ISSUED_AT, exp); + } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationProvider.java index eb7032f555..77a170aad7 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationProvider.java @@ -138,15 +138,13 @@ public Authentication authenticate(Authentication authentication) throws Authent BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; TokenIntrospectionSuccessResponse response = introspect(bearer.getToken()); Map claims = convertClaimsSet(response); - Instant iat = (Instant) claims.get(ISSUED_AT); - Instant exp = (Instant) claims.get(EXPIRES_AT); // construct token OAuth2AccessToken token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - bearer.getToken(), iat, exp); + bearer.getToken(), claims); Collection authorities = extractAuthorities(claims); AbstractAuthenticationToken result = - new OAuth2IntrospectionAuthenticationToken(token, claims, authorities); + new OAuth2IntrospectionAuthenticationToken(token, authorities); result.setDetails(bearer.getDetails()); return result; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationToken.java index 9bccd64997..8e559e72db 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationToken.java @@ -15,9 +15,9 @@ */ package org.springframework.security.oauth2.server.resource.authentication; +import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.SUBJECT; + import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; import java.util.Map; import org.springframework.security.core.GrantedAuthority; @@ -25,8 +25,6 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; -import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.SUBJECT; - /** * An {@link org.springframework.security.core.Authentication} token that represents a successful authentication as * obtained through an opaque token @@ -41,7 +39,6 @@ public class OAuth2IntrospectionAuthenticationToken private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - private Map attributes; private String name; /** @@ -51,9 +48,9 @@ public class OAuth2IntrospectionAuthenticationToken * @param authorities The authorities associated with the given token */ public OAuth2IntrospectionAuthenticationToken(OAuth2AccessToken token, - Map attributes, Collection authorities) { + Collection authorities) { - this(token, attributes, authorities, null); + this(token, authorities, null); } /** @@ -64,12 +61,11 @@ public OAuth2IntrospectionAuthenticationToken(OAuth2AccessToken token, * @param name The name associated with this token */ public OAuth2IntrospectionAuthenticationToken(OAuth2AccessToken token, - Map attributes, Collection authorities, String name) { - - super(token, attributes, token, authorities); - Assert.notEmpty(attributes, "attributes cannot be empty"); - this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); - this.name = name == null ? (String) attributes.get(SUBJECT) : name; + Collection authorities, String name) { + + super(token, token.getAttributes(), token, authorities); + Assert.notEmpty(token.getAttributes(), "attributes cannot be empty"); + this.name = name == null ? (String) token.getAttributes().get(SUBJECT) : name; setAuthenticated(true); } @@ -78,7 +74,7 @@ public OAuth2IntrospectionAuthenticationToken(OAuth2AccessToken token, */ @Override public Map getTokenAttributes() { - return this.attributes; + return this.getToken().getAttributes(); } /** diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionReactiveAuthenticationManager.java index 755989d6e4..285c7653f0 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionReactiveAuthenticationManager.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionReactiveAuthenticationManager.java @@ -144,14 +144,12 @@ private Mono authenticate(String token) return introspect(token) .map(response -> { Map claims = convertClaimsSet(response); - Instant iat = (Instant) claims.get(ISSUED_AT); - Instant exp = (Instant) claims.get(EXPIRES_AT); // construct token OAuth2AccessToken accessToken = - new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp); + new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, claims); Collection authorities = extractAuthorities(claims); - return new OAuth2IntrospectionAuthenticationToken(accessToken, claims, authorities); + return new OAuth2IntrospectionAuthenticationToken(accessToken, authorities); }); } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java index 7f72e8d21a..5b0e92f404 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java @@ -34,6 +34,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; /** * Tests for {@link JwtAuthenticationConverter} @@ -81,7 +82,11 @@ public void convertWithOverriddenGrantedAuthoritiesConverter() { private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java index 15a25a66e7..12a9969b8d 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java @@ -31,6 +31,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; @@ -77,7 +78,7 @@ public void authenticateWhenJwtDecodesThenAuthenticationHasAttributesContainedIn JwtAuthenticationToken authentication = (JwtAuthenticationToken) this.provider.authenticate(token); - assertThat(authentication.getTokenAttributes()).isEqualTo(claims); + assertThat(authentication.getTokenAttributes()).isNotEmpty(); } @Test @@ -133,8 +134,12 @@ private BearerTokenAuthenticationToken authentication() { private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } private Predicate errorCode(String errorCode) { diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java index 1a61b35082..25a1201cc5 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java @@ -30,6 +30,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -79,7 +80,7 @@ public void constructorWhenUsingCorrectParametersThenConstructedCorrectly() { assertThat(token.getPrincipal()).isEqualTo(jwt); assertThat(token.getCredentials()).isEqualTo(jwt); assertThat(token.getToken()).isEqualTo(jwt); - assertThat(token.getTokenAttributes()).isEqualTo(claims); + assertThat(token.getTokenAttributes()).isNotEmpty(); assertThat(token.isAuthenticated()).isTrue(); } @@ -94,14 +95,18 @@ public void constructorWhenUsingOnlyJwtThenConstructedCorrectly() { assertThat(token.getPrincipal()).isEqualTo(jwt); assertThat(token.getCredentials()).isEqualTo(jwt); assertThat(token.getToken()).isEqualTo(jwt); - assertThat(token.getTokenAttributes()).isEqualTo(claims); + assertThat(token.getTokenAttributes()).isNotEmpty(); assertThat(token.isAuthenticated()).isFalse(); } private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java index 385ac27883..fbaa577d7e 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java @@ -32,6 +32,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; /** * Tests for {@link JwtGrantedAuthoritiesConverter} @@ -111,7 +112,11 @@ public void convertWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTrans private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java index fff71f4b13..0fe6ce84c2 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java @@ -26,6 +26,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; @@ -59,9 +60,10 @@ public void setup() { Map claims = new HashMap<>(); claims.put("scope", "message:read message:write"); - Instant issuedAt = Instant.now(); - Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); - this.jwt = new Jwt("jwt", issuedAt, expiresAt, claims, claims); + claims.put(JwtClaimNames.IAT, Instant.now()); + claims.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); + + this.jwt = new Jwt("jwt", claims, claims); } @Test diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationTokenTests.java index 5374186f22..babfaed263 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationTokenTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionAuthenticationTokenTests.java @@ -16,6 +16,14 @@ package org.springframework.security.oauth2.server.resource.authentication; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.CLIENT_ID; +import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.EXPIRES_AT; +import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.ISSUED_AT; +import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.SUBJECT; +import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.USERNAME; + import java.time.Instant; import java.util.Collections; import java.util.HashMap; @@ -24,16 +32,10 @@ import org.junit.Before; import org.junit.Test; - import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.core.OAuth2AccessToken; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.CLIENT_ID; -import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.SUBJECT; -import static org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames.USERNAME; +import org.springframework.util.Assert; /** * Tests for {@link OAuth2IntrospectionAuthenticationToken} @@ -41,23 +43,29 @@ * @author Josh Cummings */ public class OAuth2IntrospectionAuthenticationTokenTests { - private final OAuth2AccessToken token = - new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "token", Instant.now(), Instant.now().plusSeconds(3600)); - private final Map attributes = new HashMap<>(); + private Map attributes; + + private OAuth2AccessToken token(final Map attributes) { + Assert.notEmpty(attributes, "attributes cannot be empty"); + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", attributes); + } + private final String name = "sub"; @Before public void setUp() { + this.attributes = new HashMap<>(5); this.attributes.put(SUBJECT, this.name); this.attributes.put(CLIENT_ID, "client_id"); this.attributes.put(USERNAME, "username"); + this.attributes.put(ISSUED_AT, Instant.now()); + this.attributes.put(EXPIRES_AT, Instant.now().plusSeconds(3600)); } @Test public void getNameWhenConfiguredInConstructorThenReturnsName() { OAuth2IntrospectionAuthenticationToken authenticated = - new OAuth2IntrospectionAuthenticationToken(this.token, this.attributes, + new OAuth2IntrospectionAuthenticationToken(token(attributes), AuthorityUtils.createAuthorityList("USER"), this.name); assertThat(authenticated.getName()).isEqualTo(this.name); } @@ -65,7 +73,7 @@ public void getNameWhenConfiguredInConstructorThenReturnsName() { @Test public void getNameWhenHasNoSubjectThenReturnsNull() { OAuth2IntrospectionAuthenticationToken authenticated = - new OAuth2IntrospectionAuthenticationToken(this.token, Collections.singletonMap("claim", "value"), + new OAuth2IntrospectionAuthenticationToken(token(Collections.singletonMap("claim", "value")), Collections.emptyList()); assertThat(authenticated.getName()).isNull(); } @@ -73,24 +81,23 @@ public void getNameWhenHasNoSubjectThenReturnsNull() { @Test public void getNameWhenTokenHasUsernameThenReturnsUsernameAttribute() { OAuth2IntrospectionAuthenticationToken authenticated = - new OAuth2IntrospectionAuthenticationToken(this.token, this.attributes, Collections.emptyList()); + new OAuth2IntrospectionAuthenticationToken(token(this.attributes), Collections.emptyList()); assertThat(authenticated.getName()).isEqualTo(this.attributes.get(SUBJECT)); } @Test public void constructorWhenTokenIsNullThenThrowsException() { - assertThatCode(() -> new OAuth2IntrospectionAuthenticationToken(null, null, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("token cannot be null"); + assertThatCode(() -> new OAuth2IntrospectionAuthenticationToken(null, Collections.emptyList(), null)) + .isInstanceOf(NullPointerException.class); } @Test public void constructorWhenAttributesAreNullOrEmptyThenThrowsException() { - assertThatCode(() -> new OAuth2IntrospectionAuthenticationToken(this.token, null, null)) + assertThatCode(() -> new OAuth2IntrospectionAuthenticationToken(token(null), null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("principal cannot be null"); + .hasMessageContaining("attributes cannot be empty"); - assertThatCode(() -> new OAuth2IntrospectionAuthenticationToken(this.token, Collections.emptyMap(), null)) + assertThatCode(() -> new OAuth2IntrospectionAuthenticationToken(token(Collections.emptyMap()), null)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("attributes cannot be empty"); } @@ -98,7 +105,7 @@ public void constructorWhenAttributesAreNullOrEmptyThenThrowsException() { @Test public void constructorWhenPassingAllAttributesThenTokenIsAuthenticated() { OAuth2IntrospectionAuthenticationToken authenticated = - new OAuth2IntrospectionAuthenticationToken(this.token, Collections.singletonMap("claim", "value"), + new OAuth2IntrospectionAuthenticationToken(token(Collections.singletonMap("claim", "value")), Collections.emptyList(), "harris"); assertThat(authenticated.isAuthenticated()).isTrue(); } @@ -106,7 +113,7 @@ public void constructorWhenPassingAllAttributesThenTokenIsAuthenticated() { @Test public void getTokenAttributesWhenHasTokenThenReturnsThem() { OAuth2IntrospectionAuthenticationToken authenticated = - new OAuth2IntrospectionAuthenticationToken(this.token, this.attributes, Collections.emptyList()); + new OAuth2IntrospectionAuthenticationToken(token(this.attributes), Collections.emptyList()); assertThat(authenticated.getTokenAttributes()).isEqualTo(this.attributes); } @@ -114,7 +121,7 @@ public void getTokenAttributesWhenHasTokenThenReturnsThem() { public void getAuthoritiesWhenHasAuthoritiesThenReturnsThem() { List authorities = AuthorityUtils.createAuthorityList("USER"); OAuth2IntrospectionAuthenticationToken authenticated = - new OAuth2IntrospectionAuthenticationToken(this.token, this.attributes, authorities); + new OAuth2IntrospectionAuthenticationToken(token(this.attributes), authorities); assertThat(authenticated.getAuthorities()).isEqualTo(authorities); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterAdapterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterAdapterTests.java index 66d9cc5db1..a18ebf8a9f 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterAdapterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterAdapterTests.java @@ -30,6 +30,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import static org.assertj.core.api.Assertions.assertThat; @@ -123,7 +124,11 @@ public void convertWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTrans private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java index 67bcc56d28..c79364abff 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java @@ -35,6 +35,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; /** * Tests for {@link ReactiveJwtAuthenticationConverter} @@ -83,7 +84,11 @@ public void convertWithOverriddenGrantedAuthoritiesConverter() { private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtGrantedAuthoritiesConverterAdapterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtGrantedAuthoritiesConverterAdapterTests.java index 378a2ce55a..bd12e8f2cc 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtGrantedAuthoritiesConverterAdapterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtGrantedAuthoritiesConverterAdapterTests.java @@ -34,6 +34,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; /** * Tests for {@link ReactiveJwtGrantedAuthoritiesConverterAdapter} @@ -69,7 +70,11 @@ public void whenConstructingWithInvalidConverter() { private Jwt jwt(Map claims) { Map headers = new HashMap<>(); headers.put("alg", JwsAlgorithms.RS256); + + Map attributes = new HashMap<>(claims); + attributes.put(JwtClaimNames.IAT, Instant.now()); + attributes.put(JwtClaimNames.EXP, Instant.now().plusSeconds(3600)); - return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + return new Jwt("token", headers, attributes); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java index 118267c2d8..b575d5c61b 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.resource.web.access; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -87,7 +88,7 @@ public void handleWhenTokenHasNoScopesThenInsufficientScopeError() MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); - Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap()); + Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.singletonMap("foo", "bar")); request.setUserPrincipal(token); this.accessDeniedHandler.handle(request, response, null); @@ -229,21 +230,28 @@ public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { static class TestingOAuth2TokenAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken { - private Map attributes; - - protected TestingOAuth2TokenAuthenticationToken(Map attributes) { - super(new TestingOAuth2Token("token")); - this.attributes = attributes; + protected TestingOAuth2TokenAuthenticationToken(final Map attributes) { + super(new TestingOAuth2Token("token", attributes)); } @Override public Map getTokenAttributes() { - return this.attributes; + return this.getToken().getAttributes(); } static class TestingOAuth2Token extends AbstractOAuth2Token { - public TestingOAuth2Token(String tokenValue) { - super(tokenValue); + public TestingOAuth2Token(final String tokenValue, final Map attributes) { + super(tokenValue, attributes); + } + + @Override + public Instant getIssuedAt() { + return null; + } + + @Override + public Instant getExpiresAt() { + return null; } } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java index 6a048b2c5a..e3d6557793 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.resource.web.access.server; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -80,7 +81,7 @@ public void handleWhenNotOAuth2AuthenticatedAndRealmSetThenStatus403AndAuthHeade @Test public void handleWhenTokenHasNoScopesThenInsufficientScopeError() { - Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap()); + Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.singletonMap("foo", "bar")); ServerWebExchange exchange = mock(ServerWebExchange.class); when(exchange.getPrincipal()).thenReturn(Mono.just(token)); when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); @@ -214,21 +215,28 @@ public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { static class TestingOAuth2TokenAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken { - private Map attributes; - - protected TestingOAuth2TokenAuthenticationToken(Map attributes) { - super(new TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token("token")); - this.attributes = attributes; + protected TestingOAuth2TokenAuthenticationToken(final Map attributes) { + super(new TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token("token", attributes)); } @Override public Map getTokenAttributes() { - return this.attributes; + return this.getToken().getAttributes(); } static class TestingOAuth2Token extends AbstractOAuth2Token { - public TestingOAuth2Token(String tokenValue) { - super(tokenValue); + public TestingOAuth2Token(final String tokenValue, final Map attributes) { + super(tokenValue, attributes); + } + + @Override + public Instant getIssuedAt() { + return null; + } + + @Override + public Instant getExpiresAt() { + return null; } } }