From 5e2b9c4fde96c52bc4ffad2faacae1c2bd2a5859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Wacongne?= Date: Fri, 19 Apr 2019 03:38:17 +0200 Subject: [PATCH] AbstractOAuth2Token modifications: * remove issuedAt and expiresAt properties (redundancyand potential incoherence with claims) * promote claims (AKA attributes) as abstracted member --- .../client/OAuth2LoginConfigurerTests.java | 12 ++- .../OAuth2ResourceServerConfigurerTests.java | 11 ++- .../config/web/server/OAuth2LoginTests.java | 5 +- .../server/OAuth2ResourceServerSpecTests.java | 44 ++++++---- ...thorizationCodeAuthenticationProvider.java | 2 +- ...tionCodeReactiveAuthenticationManager.java | 2 +- ...iveOAuth2AuthorizedClientServiceTests.java | 14 ++- ...zationCodeAuthenticationProviderTests.java | 6 +- ...odeReactiveAuthenticationManagerTests.java | 49 ++++++++--- .../OidcIdTokenValidatorTests.java | 40 +++++---- .../OidcReactiveOAuth2UserServiceTests.java | 17 ++-- .../oidc/userinfo/OidcUserRequestTests.java | 17 +++- .../userinfo/OidcUserRequestUtilsTests.java | 23 +++-- .../oidc/userinfo/OidcUserServiceTests.java | 16 +++- ...DefaultReactiveOAuth2UserServiceTests.java | 12 ++- .../DelegatingOAuth2UserServiceTests.java | 2 +- ...OAuth2UserRequestEntityConverterTests.java | 27 +++--- .../userinfo/OAuth2UserRequestTests.java | 10 ++- ...izedClientExchangeFilterFunctionTests.java | 52 ++++++----- ...izedClientExchangeFilterFunctionTests.java | 31 ++++--- ...uth2LoginAuthenticationWebFilterTests.java | 9 +- .../oauth2/core/AbstractOAuth2Token.java | 87 ++++++++----------- .../oauth2/core/OAuth2AccessToken.java | 51 +++++++++-- .../oauth2/core/OAuth2RefreshToken.java | 26 +++++- .../endpoint/OAuth2AccessTokenResponse.java | 11 +-- .../oauth2/core/oidc/OidcIdToken.java | 44 +++++++--- .../oauth2/core/OAuth2AccessTokenTests.java | 41 ++++++--- .../oauth2/core/TestOAuth2AccessTokens.java | 16 +++- .../oauth2/core/TestOAuth2RefreshTokens.java | 4 +- .../oauth2/core/oidc/OidcIdTokenTests.java | 18 ++-- .../core/oidc/user/DefaultOidcUserTests.java | 20 +++-- .../oidc/user/OidcUserAuthorityTests.java | 6 +- .../oauth2/core/oidc/user/TestOidcUsers.java | 4 +- .../security/oauth2/jwt/Jwt.java | 51 ++++++++--- .../security/oauth2/jwt/NimbusJwtDecoder.java | 55 ++++++------ .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 38 ++++---- .../oauth2/jwt/JwtIssuerValidatorTests.java | 24 ++--- .../security/oauth2/jwt/JwtTests.java | 20 +++-- .../jwt/JwtTimestampValidatorTests.java | 60 ++++++------- ...h2IntrospectionAuthenticationProvider.java | 6 +- ...Auth2IntrospectionAuthenticationToken.java | 24 +++-- ...spectionReactiveAuthenticationManager.java | 6 +- .../JwtAuthenticationConverterTests.java | 7 +- .../JwtAuthenticationProviderTests.java | 9 +- .../JwtAuthenticationTokenTests.java | 11 ++- .../JwtGrantedAuthoritiesConverterTests.java | 7 +- ...JwtReactiveAuthenticationManagerTests.java | 8 +- ...IntrospectionAuthenticationTokenTests.java | 53 ++++++----- ...wtAuthenticationConverterAdapterTests.java | 7 +- ...activeJwtAuthenticationConverterTests.java | 7 +- ...antedAuthoritiesConverterAdapterTests.java | 7 +- .../BearerTokenAccessDeniedHandlerTests.java | 26 ++++-- ...erTokenServerAccessDeniedHandlerTests.java | 26 ++++-- 53 files changed, 752 insertions(+), 429 deletions(-) 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 3789561536e..301df20365a 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 f6734af2d28..f0defcde5b7 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 6857bd6ce53..34a58293e01 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 5edd88a0053..1d0b77a8a3d 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 73227b69893..4530c141598 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 8a215efdf8d..3aebccd4a31 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 52d85be817e..6c7c0851dac 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 77dba3d87a5..b436cbdeab3 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 937babdea2d..f38352e4da2 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 5ef5f7d25ff..e6432386499 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 15d9574aae4..70cd0fcbf37 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 770bb0e0289..487b591061f 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 53ca0a094a5..0b72578ebdd 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 15d4015a5e4..e3e464b0ad7 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 b856c80252d..b2187c5f36b 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 f43b18a0ae4..726c6d68077 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 4ad9d4e4aac..88b063bde2b 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 6e2222e5f19..ff50fa8b664 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 7b8ba1889f5..2c70f22b580 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 d99de2db281..fe61058549d 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 fd50f3f72a3..80b10a8cd5f 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 e1dc4d59030..696a7e37147 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 b2ff587f0d8..3742c25cf6b 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 e52f3643993..f19fd39a6a3 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 55488926cb1..7fa6e7ce37f 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 6e1297862db..c23c8a98865 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 ccede953375..5ce19c03bcb 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 62ca87ff48e..7fc6afbe485 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 8face452f5b..bb68e53f7b8 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 15a04e6b0a9..959e0fb764a 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 2fad69684f3..f909705e620 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 d859b74d564..99ec2bea2ed 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 bdfe6abe60e..dca7119bdb3 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 6e62372dea3..8a411202881 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 0ffcfe4bb8e..7704c53ab48 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 5eb815a5e3a..b302507d3b0 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 85f3ad30e55..9b10c286491 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 e59716da740..8ada32720eb 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 2101d22ecad..335c631d94a 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 eb7032f5558..77a170aad77 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 9bccd64997a..8e559e72db8 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 755989d6e46..285c7653f0f 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 7f72e8d21a2..5b0e92f4040 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 15a25a66e76..12a9969b8d7 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 1a61b350821..25a1201cc53 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 385ac278832..fbaa577d7e8 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 fff71f4b13a..0fe6ce84c2c 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 5374186f228..babfaed263e 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 66d9cc5db1d..a18ebf8a9fc 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 67bcc56d28f..c79364abff5 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 378a2ce55ac..bd12e8f2ccc 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 118267c2d89..b575d5c61bc 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 6a048b2c5a8..e3d65577932 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; } } }