Skip to content

Commit

Permalink
Add support for a more complex access token response
Browse files Browse the repository at this point in the history
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 setters/constructors and added new setters/factory methods.

Closes spring-projectsgh-9685
  • Loading branch information
sjohnr committed May 26, 2021
1 parent 65ecaa0 commit 1ab5069
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 199 deletions.
@@ -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<Map<String, ?>, OAuth2AccessTokenResponse> {

private static final Set<String> 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<String, ?> source) {
String accessToken = getParameterValue(source, OAuth2ParameterNames.ACCESS_TOKEN);
OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(source);
long expiresIn = getExpiresIn(source);
Set<String> scopes = getScopes(source);
String refreshToken = getParameterValue(source, OAuth2ParameterNames.REFRESH_TOKEN);
Map<String, Object> additionalParameters = new LinkedHashMap<>();
for (Map.Entry<String, ?> 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<String, ?> tokenResponseParameters) {
if (OAuth2AccessToken.TokenType.BEARER.getValue()
.equalsIgnoreCase(getParameterValue(tokenResponseParameters, OAuth2ParameterNames.TOKEN_TYPE))) {
return OAuth2AccessToken.TokenType.BEARER;
}
return null;
}

private static long getExpiresIn(Map<String, ?> tokenResponseParameters) {
return getParameterValue(tokenResponseParameters, OAuth2ParameterNames.EXPIRES_IN, 0L);
}

private static Set<String> getScopes(Map<String, ?> 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<String, ?> tokenResponseParameters, String parameterName) {
Object obj = tokenResponseParameters.get(parameterName);
return (obj != null) ? obj.toString() : null;
}

private static long getParameterValue(Map<String, ?> 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;
}

}
@@ -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<OAuth2AccessTokenResponse, Map<String, Object>> {

@Override
public Map<String, Object> convert(OAuth2AccessTokenResponse tokenResponse) {
Map<String, Object> 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<String, Object> 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;
}

}
@@ -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.
Expand All @@ -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<Map<String, String>, OAuth2AccessTokenResponse> {

private static final Set<String> 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<Map<String, ?>, OAuth2AccessTokenResponse> delegate = new DefaultMapOAuth2AccessTokenResponseConverter();

@Override
public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(tokenResponseParameters);
long expiresIn = getExpiresIn(tokenResponseParameters);
Set<String> scopes = getScopes(tokenResponseParameters);
String refreshToken = tokenResponseParameters.get(OAuth2ParameterNames.REFRESH_TOKEN);
Map<String, Object> additionalParameters = new LinkedHashMap<>();
for (Map.Entry<String, String> 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<String, String> tokenResponseParameters) {
if (OAuth2AccessToken.TokenType.BEARER.getValue()
.equalsIgnoreCase(tokenResponseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) {
return OAuth2AccessToken.TokenType.BEARER;
}
return null;
}

private long getExpiresIn(Map<String, String> tokenResponseParameters) {
if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) {
try {
return Long.parseLong(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
}
catch (NumberFormatException ex) {
}
}
return 0;
}

private Set<String> getScopes(Map<String, String> 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);
}

}
@@ -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.
Expand All @@ -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<OAuth2AccessTokenResponse, Map<String, String>> {

private final Converter<OAuth2AccessTokenResponse, Map<String, Object>> delegate = new DefaultOAuth2AccessTokenResponseMapConverter();

@Override
public Map<String, String> convert(OAuth2AccessTokenResponse tokenResponse) {
Map<String, String> 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<String, Object> 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<String, String> stringTokenResponseParameters = new HashMap<>();
this.delegate.convert(tokenResponse)
.forEach((key, value) -> stringTokenResponseParameters.put(key, String.valueOf(value)));
return stringTokenResponseParameters;
}

}

0 comments on commit 1ab5069

Please sign in to comment.