From 10de63ce89859231d64b78309233d0e7c9b2f23c Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 18 May 2021 16:26:24 -0500 Subject: [PATCH] Access Token Response supports any data type Changed the converter used to convert a map into an OAuth2AccessTokenResponse to support any object as the value, including json numbers and nested objects. Also deprecated old classes/setters and added new classes/setters. Closes gh-9685 --- ...MapOAuth2AccessTokenResponseConverter.java | 119 ++++++++++++++++++ ...OAuth2AccessTokenResponseMapConverter.java | 66 ++++++++++ ...MapOAuth2AccessTokenResponseConverter.java | 63 +--------- ...OAuth2AccessTokenResponseMapConverter.java | 38 ++---- ...cessTokenResponseHttpMessageConverter.java | 79 ++++++++++-- ...th2AccessTokenResponseConverterTests.java} | 64 +++++++++- ...AccessTokenResponseMapConverterTests.java} | 54 ++++++-- ...okenResponseHttpMessageConverterTests.java | 13 +- 8 files changed, 377 insertions(+), 119 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverter.java rename oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/{MapOAuth2AccessTokenResponseConverterTests.java => DefaultMapOAuth2AccessTokenResponseConverterTests.java} (61%) rename oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/{OAuth2AccessTokenResponseMapConverterTests.java => DefaultOAuth2AccessTokenResponseMapConverterTests.java} (54%) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java new file mode 100644 index 0000000000..b7fa948551 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core.endpoint; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.StringUtils; + +/** + * A {@link Converter} that converts the provided OAuth 2.0 Access Token Response + * parameters to an {@link OAuth2AccessTokenResponse}. + * + * @author Steve Riesenberg + * @since 5.6 + */ +public final class DefaultMapOAuth2AccessTokenResponseConverter + implements Converter, OAuth2AccessTokenResponse> { + + private static final Set TOKEN_RESPONSE_PARAMETER_NAMES = new HashSet<>( + Arrays.asList(OAuth2ParameterNames.ACCESS_TOKEN, OAuth2ParameterNames.EXPIRES_IN, + OAuth2ParameterNames.REFRESH_TOKEN, OAuth2ParameterNames.SCOPE, OAuth2ParameterNames.TOKEN_TYPE)); + + @Override + public OAuth2AccessTokenResponse convert(Map source) { + String accessToken = getParameterValue(source, OAuth2ParameterNames.ACCESS_TOKEN); + OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(source); + long expiresIn = getExpiresIn(source); + Set scopes = getScopes(source); + String refreshToken = getParameterValue(source, OAuth2ParameterNames.REFRESH_TOKEN); + Map additionalParameters = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + if (!TOKEN_RESPONSE_PARAMETER_NAMES.contains(entry.getKey())) { + additionalParameters.put(entry.getKey(), entry.getValue()); + } + } + // @formatter:off + return OAuth2AccessTokenResponse.withToken(accessToken) + .tokenType(accessTokenType) + .expiresIn(expiresIn) + .scopes(scopes) + .refreshToken(refreshToken) + .additionalParameters(additionalParameters) + .build(); + // @formatter:on + } + + private static OAuth2AccessToken.TokenType getAccessTokenType(Map tokenResponseParameters) { + if (OAuth2AccessToken.TokenType.BEARER.getValue() + .equalsIgnoreCase(getParameterValue(tokenResponseParameters, OAuth2ParameterNames.TOKEN_TYPE))) { + return OAuth2AccessToken.TokenType.BEARER; + } + return null; + } + + private static long getExpiresIn(Map tokenResponseParameters) { + return getParameterValue(tokenResponseParameters, OAuth2ParameterNames.EXPIRES_IN, 0L); + } + + private static Set getScopes(Map tokenResponseParameters) { + if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) { + String scope = getParameterValue(tokenResponseParameters, OAuth2ParameterNames.SCOPE); + return new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + return Collections.emptySet(); + } + + private static String getParameterValue(Map tokenResponseParameters, String parameterName) { + Object obj = tokenResponseParameters.get(parameterName); + return (obj != null) ? obj.toString() : null; + } + + private static long getParameterValue(Map tokenResponseParameters, String parameterName, + long defaultValue) { + long parameterValue = defaultValue; + + Object obj = tokenResponseParameters.get(parameterName); + if (obj != null) { + // Final classes Long and Integer do not need to be coerced + if (obj.getClass() == Long.class) { + parameterValue = (Long) obj; + } + else if (obj.getClass() == Integer.class) { + parameterValue = (Integer) obj; + } + else { + // Attempt to coerce to a long (typically from a String) + try { + parameterValue = Long.parseLong(obj.toString()); + } + catch (NumberFormatException ignored) { + } + } + } + + return parameterValue; + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverter.java new file mode 100644 index 0000000000..870d89e5ee --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core.endpoint; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * A {@link Converter} that converts the provided {@link OAuth2AccessTokenResponse} to a + * {@code Map} representation of the OAuth 2.0 Access Token Response parameters. + * + * @author Steve Riesenberg + * @since 5.6 + */ +public final class DefaultOAuth2AccessTokenResponseMapConverter + implements Converter> { + + @Override + public Map convert(OAuth2AccessTokenResponse tokenResponse) { + Map parameters = new HashMap<>(); + parameters.put(OAuth2ParameterNames.ACCESS_TOKEN, tokenResponse.getAccessToken().getTokenValue()); + parameters.put(OAuth2ParameterNames.TOKEN_TYPE, tokenResponse.getAccessToken().getTokenType().getValue()); + parameters.put(OAuth2ParameterNames.EXPIRES_IN, getExpiresIn(tokenResponse)); + if (!CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { + parameters.put(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(tokenResponse.getAccessToken().getScopes(), " ")); + } + if (tokenResponse.getRefreshToken() != null) { + parameters.put(OAuth2ParameterNames.REFRESH_TOKEN, tokenResponse.getRefreshToken().getTokenValue()); + } + if (!CollectionUtils.isEmpty(tokenResponse.getAdditionalParameters())) { + for (Map.Entry entry : tokenResponse.getAdditionalParameters().entrySet()) { + parameters.put(entry.getKey(), entry.getValue()); + } + } + return parameters; + } + + private static long getExpiresIn(OAuth2AccessTokenResponse tokenResponse) { + if (tokenResponse.getAccessToken().getExpiresAt() != null) { + return ChronoUnit.SECONDS.between(Instant.now(), tokenResponse.getAccessToken().getExpiresAt()); + } + return -1; + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverter.java index ef5e138232..5ca008b2fd 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,81 +16,28 @@ package org.springframework.security.oauth2.core.endpoint; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.Map; -import java.util.Set; import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.util.StringUtils; /** * A {@link Converter} that converts the provided OAuth 2.0 Access Token Response * parameters to an {@link OAuth2AccessTokenResponse}. * + * @deprecated Use {@link DefaultMapOAuth2AccessTokenResponseConverter} instead * @author Joe Grandja * @author Nikita Konev * @since 5.3 */ +@Deprecated public final class MapOAuth2AccessTokenResponseConverter implements Converter, OAuth2AccessTokenResponse> { - private static final Set TOKEN_RESPONSE_PARAMETER_NAMES = new HashSet<>( - Arrays.asList(OAuth2ParameterNames.ACCESS_TOKEN, OAuth2ParameterNames.EXPIRES_IN, - OAuth2ParameterNames.REFRESH_TOKEN, OAuth2ParameterNames.SCOPE, OAuth2ParameterNames.TOKEN_TYPE)); + private final Converter, OAuth2AccessTokenResponse> delegate = new DefaultMapOAuth2AccessTokenResponseConverter(); @Override public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) { - String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN); - OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(tokenResponseParameters); - long expiresIn = getExpiresIn(tokenResponseParameters); - Set scopes = getScopes(tokenResponseParameters); - String refreshToken = tokenResponseParameters.get(OAuth2ParameterNames.REFRESH_TOKEN); - Map additionalParameters = new LinkedHashMap<>(); - for (Map.Entry entry : tokenResponseParameters.entrySet()) { - if (!TOKEN_RESPONSE_PARAMETER_NAMES.contains(entry.getKey())) { - additionalParameters.put(entry.getKey(), entry.getValue()); - } - } - // @formatter:off - return OAuth2AccessTokenResponse.withToken(accessToken) - .tokenType(accessTokenType) - .expiresIn(expiresIn) - .scopes(scopes) - .refreshToken(refreshToken) - .additionalParameters(additionalParameters) - .build(); - // @formatter:on - } - - private OAuth2AccessToken.TokenType getAccessTokenType(Map tokenResponseParameters) { - if (OAuth2AccessToken.TokenType.BEARER.getValue() - .equalsIgnoreCase(tokenResponseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) { - return OAuth2AccessToken.TokenType.BEARER; - } - return null; - } - - private long getExpiresIn(Map tokenResponseParameters) { - if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) { - try { - return Long.parseLong(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN)); - } - catch (NumberFormatException ex) { - } - } - return 0; - } - - private Set getScopes(Map tokenResponseParameters) { - if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) { - String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE); - return new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); - } - return Collections.emptySet(); + return this.delegate.convert(tokenResponseParameters); } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverter.java index 443f03ccee..5219e2ec4d 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,52 +16,32 @@ package org.springframework.security.oauth2.core.endpoint; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; import org.springframework.core.convert.converter.Converter; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; /** * A {@link Converter} that converts the provided {@link OAuth2AccessTokenResponse} to a * {@code Map} representation of the OAuth 2.0 Access Token Response parameters. * + * @deprecated Use {@link DefaultOAuth2AccessTokenResponseMapConverter} instead * @author Joe Grandja * @author Nikita Konev * @since 5.3 */ +@Deprecated public final class OAuth2AccessTokenResponseMapConverter implements Converter> { + private final Converter> delegate = new DefaultOAuth2AccessTokenResponseMapConverter(); + @Override public Map convert(OAuth2AccessTokenResponse tokenResponse) { - Map parameters = new HashMap<>(); - parameters.put(OAuth2ParameterNames.ACCESS_TOKEN, tokenResponse.getAccessToken().getTokenValue()); - parameters.put(OAuth2ParameterNames.TOKEN_TYPE, tokenResponse.getAccessToken().getTokenType().getValue()); - parameters.put(OAuth2ParameterNames.EXPIRES_IN, String.valueOf(getExpiresIn(tokenResponse))); - if (!CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - parameters.put(OAuth2ParameterNames.SCOPE, - StringUtils.collectionToDelimitedString(tokenResponse.getAccessToken().getScopes(), " ")); - } - if (tokenResponse.getRefreshToken() != null) { - parameters.put(OAuth2ParameterNames.REFRESH_TOKEN, tokenResponse.getRefreshToken().getTokenValue()); - } - if (!CollectionUtils.isEmpty(tokenResponse.getAdditionalParameters())) { - for (Map.Entry entry : tokenResponse.getAdditionalParameters().entrySet()) { - parameters.put(entry.getKey(), entry.getValue().toString()); - } - } - return parameters; - } - - private long getExpiresIn(OAuth2AccessTokenResponse tokenResponse) { - if (tokenResponse.getAccessToken().getExpiresAt() != null) { - return ChronoUnit.SECONDS.between(Instant.now(), tokenResponse.getAccessToken().getExpiresAt()); - } - return -1; + Map stringTokenResponseParameters = new HashMap<>(); + this.delegate.convert(tokenResponse) + .forEach((key, value) -> stringTokenResponseParameters.put(key, String.valueOf(value))); + return stringTokenResponseParameters; } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java index 513a14fc82..2d0e8da76a 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,9 @@ package org.springframework.security.oauth2.core.http.converter; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -import java.util.stream.Collectors; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; @@ -31,6 +32,8 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.security.oauth2.core.endpoint.DefaultMapOAuth2AccessTokenResponseConverter; +import org.springframework.security.oauth2.core.endpoint.DefaultOAuth2AccessTokenResponseMapConverter; import org.springframework.security.oauth2.core.endpoint.MapOAuth2AccessTokenResponseConverter; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponseMapConverter; @@ -55,10 +58,22 @@ public class OAuth2AccessTokenResponseHttpMessageConverter private GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); + /** + * @deprecated This field should no longer be used + */ + @Deprecated protected Converter, OAuth2AccessTokenResponse> tokenResponseConverter = new MapOAuth2AccessTokenResponseConverter(); + private Converter, OAuth2AccessTokenResponse> accessTokenResponseConverter = new DefaultMapOAuth2AccessTokenResponseConverter(); + + /** + * @deprecated This field should no longer be used + */ + @Deprecated protected Converter> tokenResponseParametersConverter = new OAuth2AccessTokenResponseMapConverter(); + private Converter> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter(); + public OAuth2AccessTokenResponseHttpMessageConverter() { super(DEFAULT_CHARSET, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); } @@ -73,16 +88,18 @@ public class OAuth2AccessTokenResponseHttpMessageConverter protected OAuth2AccessTokenResponse readInternal(Class clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException { try { - // gh-6463: Parse parameter values as Object in order to handle potential JSON - // Object and then convert values to String Map tokenResponseParameters = (Map) this.jsonMessageConverter .read(STRING_OBJECT_MAP.getType(), null, inputMessage); - // @formatter:off - return this.tokenResponseConverter.convert(tokenResponseParameters - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, (entry) -> String.valueOf(entry.getValue())))); - // @formatter:on + // Only use deprecated converter if it has been set directly + if (this.tokenResponseConverter.getClass() != MapOAuth2AccessTokenResponseConverter.class) { + // gh-6463: Parse parameter values as Object in order to handle potential + // JSON Object and then convert values to String + Map stringTokenResponseParameters = new HashMap<>(); + tokenResponseParameters + .forEach((key, value) -> stringTokenResponseParameters.put(key, String.valueOf(value))); + return this.tokenResponseConverter.convert(stringTokenResponseParameters); + } + return this.accessTokenResponseConverter.convert(tokenResponseParameters); } catch (Exception ex) { throw new HttpMessageNotReadableException( @@ -95,7 +112,15 @@ public class OAuth2AccessTokenResponseHttpMessageConverter protected void writeInternal(OAuth2AccessTokenResponse tokenResponse, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException { try { - Map tokenResponseParameters = this.tokenResponseParametersConverter.convert(tokenResponse); + Map tokenResponseParameters; + // Only use deprecated converter if it has been set directly + if (this.tokenResponseParametersConverter.getClass() != OAuth2AccessTokenResponseMapConverter.class) { + tokenResponseParameters = new LinkedHashMap<>( + this.tokenResponseParametersConverter.convert(tokenResponse)); + } + else { + tokenResponseParameters = this.accessTokenResponseParametersConverter.convert(tokenResponse); + } this.jsonMessageConverter.write(tokenResponseParameters, STRING_OBJECT_MAP.getType(), MediaType.APPLICATION_JSON, outputMessage); } @@ -108,26 +133,58 @@ public class OAuth2AccessTokenResponseHttpMessageConverter /** * Sets the {@link Converter} used for converting the OAuth 2.0 Access Token Response * parameters to an {@link OAuth2AccessTokenResponse}. + * @deprecated Use {@link #setAccessTokenResponseConverter(Converter)} instead * @param tokenResponseConverter the {@link Converter} used for converting to an * {@link OAuth2AccessTokenResponse} */ + @Deprecated public final void setTokenResponseConverter( Converter, OAuth2AccessTokenResponse> tokenResponseConverter) { Assert.notNull(tokenResponseConverter, "tokenResponseConverter cannot be null"); this.tokenResponseConverter = tokenResponseConverter; } + /** + * Sets the {@link Converter} used for converting the OAuth 2.0 Access Token Response + * parameters to an {@link OAuth2AccessTokenResponse}. + * @param accessTokenResponseConverter the {@link Converter} used for converting to an + * {@link OAuth2AccessTokenResponse} + * @since 5.6 + */ + public final void setAccessTokenResponseConverter( + Converter, OAuth2AccessTokenResponse> accessTokenResponseConverter) { + Assert.notNull(accessTokenResponseConverter, "accessTokenResponseConverter cannot be null"); + this.accessTokenResponseConverter = accessTokenResponseConverter; + } + /** * Sets the {@link Converter} used for converting the * {@link OAuth2AccessTokenResponse} to a {@code Map} representation of the OAuth 2.0 * Access Token Response parameters. + * @deprecated Use {@link #setAccessTokenResponseParametersConverter(Converter)} + * instead * @param tokenResponseParametersConverter the {@link Converter} used for converting * to a {@code Map} representation of the Access Token Response parameters */ + @Deprecated public final void setTokenResponseParametersConverter( Converter> tokenResponseParametersConverter) { Assert.notNull(tokenResponseParametersConverter, "tokenResponseParametersConverter cannot be null"); this.tokenResponseParametersConverter = tokenResponseParametersConverter; } + /** + * Sets the {@link Converter} used for converting the + * {@link OAuth2AccessTokenResponse} to a {@code Map} representation of the OAuth 2.0 + * Access Token Response parameters. + * @param accessTokenResponseParametersConverter the {@link Converter} used for + * converting to a {@code Map} representation of the Access Token Response parameters + * @since 5.6 + */ + public final void setAccessTokenResponseParametersConverter( + Converter> accessTokenResponseParametersConverter) { + Assert.notNull(accessTokenResponseParametersConverter, "accessTokenResponseParametersConverter cannot be null"); + this.accessTokenResponseParametersConverter = accessTokenResponseParametersConverter; + } + } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java similarity index 61% rename from oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverterTests.java rename to oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java index 715f4efa77..c4064c6efb 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/MapOAuth2AccessTokenResponseConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.core.endpoint; import java.time.Duration; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -25,21 +26,22 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; /** - * Tests for {@link MapOAuth2AccessTokenResponseConverter}. + * Tests for {@link DefaultMapOAuth2AccessTokenResponseConverter}. * - * @author Nikita Konev + * @author Steve Riesenberg */ -public class MapOAuth2AccessTokenResponseConverterTests { +public class DefaultMapOAuth2AccessTokenResponseConverterTests { - private MapOAuth2AccessTokenResponseConverter messageConverter; + private Converter, OAuth2AccessTokenResponse> messageConverter; @Before public void setup() { - this.messageConverter = new MapOAuth2AccessTokenResponseConverter(); + this.messageConverter = new DefaultMapOAuth2AccessTokenResponseConverter(); } @Test @@ -116,4 +118,54 @@ public class MapOAuth2AccessTokenResponseConverterTests { Assert.assertEquals(0, additionalParameters.size()); } + // gh-9685 + @Test + public void shouldConvertWithNumericExpiresIn() { + Map map = new HashMap<>(); + map.put("access_token", "access-token-1234"); + map.put("token_type", "bearer"); + map.put("expires_in", 3600); + OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); + OAuth2AccessToken accessToken = converted.getAccessToken(); + Assert.assertNotNull(accessToken); + Assert.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assert.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); + Assert.assertEquals(3600, Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); + } + + // gh-9685 + @Test + public void shouldConvertWithObjectAdditionalParameter() { + Map map = new HashMap<>(); + map.put("access_token", "access-token-1234"); + map.put("token_type", "bearer"); + map.put("expires_in", "3600"); + map.put("scope", "read write"); + map.put("refresh_token", "refresh-token-1234"); + Map nestedObject = new LinkedHashMap<>(); + nestedObject.put("a", "first value"); + nestedObject.put("b", "second value"); + map.put("custom_parameter_1", nestedObject); + map.put("custom_parameter_2", "custom-value-2"); + OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); + OAuth2AccessToken accessToken = converted.getAccessToken(); + Assert.assertNotNull(accessToken); + Assert.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assert.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); + Set scopes = accessToken.getScopes(); + Assert.assertNotNull(scopes); + Assert.assertEquals(2, scopes.size()); + Assert.assertTrue(scopes.contains("read")); + Assert.assertTrue(scopes.contains("write")); + Assert.assertEquals(3600, Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); + OAuth2RefreshToken refreshToken = converted.getRefreshToken(); + Assert.assertNotNull(refreshToken); + Assert.assertEquals("refresh-token-1234", refreshToken.getTokenValue()); + Map additionalParameters = converted.getAdditionalParameters(); + Assert.assertNotNull(additionalParameters); + Assert.assertEquals(2, additionalParameters.size()); + Assert.assertEquals(nestedObject, additionalParameters.get("custom_parameter_1")); + Assert.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2")); + } + } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java similarity index 54% rename from oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverterTests.java rename to oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java index ae4f4117b4..0446e73aac 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseMapConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.core.endpoint; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -25,24 +26,25 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2AccessToken; /** - * Tests for {@link OAuth2AccessTokenResponseMapConverter}. + * Tests for {@link DefaultOAuth2AccessTokenResponseMapConverter}. * - * @author Nikita Konev + * @author Steve Riesenberg */ -public class OAuth2AccessTokenResponseMapConverterTests { +public class DefaultOAuth2AccessTokenResponseMapConverterTests { - private OAuth2AccessTokenResponseMapConverter messageConverter; + private Converter> messageConverter; @Before public void setup() { - this.messageConverter = new OAuth2AccessTokenResponseMapConverter(); + this.messageConverter = new DefaultOAuth2AccessTokenResponseMapConverter(); } @Test - public void convertFull() { + public void shouldConvertFull() { Map additionalParameters = new HashMap<>(); additionalParameters.put("custom_parameter_1", "custom-value-1"); additionalParameters.put("custom_parameter_2", "custom-value-2"); @@ -58,7 +60,7 @@ public class OAuth2AccessTokenResponseMapConverterTests { .tokenType(OAuth2AccessToken.TokenType.BEARER) .build(); // @formatter:on - Map result = this.messageConverter.convert(build); + Map result = this.messageConverter.convert(build); Assert.assertEquals(7, result.size()); Assert.assertEquals("access-token-value-1234", result.get("access_token")); Assert.assertEquals("refresh-token-value-1234", result.get("refresh_token")); @@ -70,17 +72,49 @@ public class OAuth2AccessTokenResponseMapConverterTests { } @Test - public void convertMinimal() { + public void shouldConvertMinimal() { // @formatter:off OAuth2AccessTokenResponse build = OAuth2AccessTokenResponse.withToken("access-token-value-1234") .tokenType(OAuth2AccessToken.TokenType.BEARER) .build(); // @formatter:on - Map result = this.messageConverter.convert(build); + Map result = this.messageConverter.convert(build); Assert.assertEquals(3, result.size()); Assert.assertEquals("access-token-value-1234", result.get("access_token")); Assert.assertEquals("Bearer", result.get("token_type")); Assert.assertNotNull(result.get("expires_in")); } + // gh-9685 + @Test + public void shouldConvertWithObjectAdditionalParameter() { + Map nestedObject = new LinkedHashMap<>(); + nestedObject.put("a", "first value"); + nestedObject.put("b", "second value"); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("custom_parameter_1", nestedObject); + additionalParameters.put("custom_parameter_2", "custom-value-2"); + Set scopes = new HashSet<>(); + scopes.add("read"); + scopes.add("write"); + // @formatter:off + OAuth2AccessTokenResponse build = OAuth2AccessTokenResponse.withToken("access-token-value-1234") + .expiresIn(3699) + .additionalParameters(additionalParameters) + .refreshToken("refresh-token-value-1234") + .scopes(scopes) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build(); + // @formatter:on + Map result = this.messageConverter.convert(build); + Assert.assertEquals(7, result.size()); + Assert.assertEquals("access-token-value-1234", result.get("access_token")); + Assert.assertEquals("refresh-token-value-1234", result.get("refresh_token")); + Assert.assertEquals("read write", result.get("scope")); + Assert.assertEquals("Bearer", result.get("token_type")); + Assert.assertNotNull(result.get("expires_in")); + Assert.assertEquals(nestedObject, result.get("custom_parameter_1")); + Assert.assertEquals("custom-value-2", result.get("custom_parameter_2")); + } + } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverterTests.java index 438f221169..f51327cc04 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; @@ -124,9 +125,11 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests { .isBeforeOrEqualTo(Instant.now().plusSeconds(3600)); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234"); - assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly( - entry("custom_object_1", "{name1=value1}"), entry("custom_object_2", "[value1, value2]"), - entry("custom_parameter_1", "custom-value-1"), entry("custom_parameter_2", "custom-value-2")); + Map additionalParameters = accessTokenResponse.getAdditionalParameters().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, (entry) -> String.valueOf(entry.getValue()))); + assertThat(additionalParameters).containsExactly(entry("custom_object_1", "{name1=value1}"), + entry("custom_object_2", "[value1, value2]"), entry("custom_parameter_1", "custom-value-1"), + entry("custom_parameter_2", "custom-value-2")); } // gh-8108 @@ -148,7 +151,7 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests { assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); assertThat(accessTokenResponse.getAccessToken().getExpiresAt()) .isBeforeOrEqualTo(Instant.now().plusSeconds(3600)); - assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("null"); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234"); }