Browse Source

Provide extension for processing access token request

Issue gh-319
Joe Grandja 4 năm trước cách đây
mục cha
commit
5a79234677

+ 22 - 150
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java

@@ -17,13 +17,8 @@ package org.springframework.security.oauth2.server.authorization.web;
 
 import java.io.IOException;
 import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
@@ -38,7 +33,6 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -50,11 +44,12 @@ import org.springframework.security.oauth2.core.http.converter.OAuth2AccessToken
 import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -62,8 +57,6 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
-import org.springframework.util.MultiValueMap;
-import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 /**
@@ -103,11 +96,11 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 
 	private final AuthenticationManager authenticationManager;
 	private final RequestMatcher tokenEndpointMatcher;
-	private final AuthenticationConverter authorizationGrantAuthenticationConverter;
 	private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
 			new OAuth2AccessTokenResponseHttpMessageConverter();
 	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
 			new OAuth2ErrorHttpMessageConverter();
+	private AuthenticationConverter authenticationConverter;
 	private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAccessTokenResponse;
 	private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
 
@@ -131,11 +124,11 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 		Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty");
 		this.authenticationManager = authenticationManager;
 		this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
-		List<AuthenticationConverter> converters = new ArrayList<>();
-		converters.add(new AuthorizationCodeAuthenticationConverter());
-		converters.add(new RefreshTokenAuthenticationConverter());
-		converters.add(new ClientCredentialsAuthenticationConverter());
-		this.authorizationGrantAuthenticationConverter = new DelegatingAuthenticationConverter(converters);
+		this.authenticationConverter = new DelegatingAuthenticationConverter(
+				Arrays.asList(
+						new OAuth2AuthorizationCodeAuthenticationConverter(),
+						new OAuth2RefreshTokenAuthenticationConverter(),
+						new OAuth2ClientCredentialsAuthenticationConverter()));
 	}
 
 	@Override
@@ -153,7 +146,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.GRANT_TYPE);
 			}
 
-			Authentication authorizationGrantAuthentication = this.authorizationGrantAuthenticationConverter.convert(request);
+			Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);
 			if (authorizationGrantAuthentication == null) {
 				throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE);
 			}
@@ -167,6 +160,17 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 		}
 	}
 
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
+	 * to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the authorization grant.
+	 *
+	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
+	 */
+	public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
+	}
+
 	/**
 	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
 	 * and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
@@ -232,136 +236,4 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 		throw new OAuth2AuthenticationException(error);
 	}
 
-	private static class AuthorizationCodeAuthenticationConverter implements AuthenticationConverter {
-
-		@Override
-		public Authentication convert(HttpServletRequest request) {
-			// grant_type (REQUIRED)
-			String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
-			if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) {
-				return null;
-			}
-
-			Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
-
-			MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
-
-			// code (REQUIRED)
-			String code = parameters.getFirst(OAuth2ParameterNames.CODE);
-			if (!StringUtils.hasText(code) ||
-					parameters.get(OAuth2ParameterNames.CODE).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CODE);
-			}
-
-			// redirect_uri (REQUIRED)
-			// Required only if the "redirect_uri" parameter was included in the authorization request
-			String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
-			if (StringUtils.hasText(redirectUri) &&
-					parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
-			}
-
-			// @formatter:off
-			Map<String, Object> additionalParameters = parameters
-					.entrySet()
-					.stream()
-					.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
-							!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
-							!e.getKey().equals(OAuth2ParameterNames.CODE) &&
-							!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
-					.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
-			// @formatter:on
-
-			return new OAuth2AuthorizationCodeAuthenticationToken(
-					code, clientPrincipal, redirectUri, additionalParameters);
-		}
-	}
-
-	private static class RefreshTokenAuthenticationConverter implements AuthenticationConverter {
-
-		@Override
-		public Authentication convert(HttpServletRequest request) {
-			// grant_type (REQUIRED)
-			String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
-			if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
-				return null;
-			}
-
-			Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
-
-			MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
-
-			// refresh_token (REQUIRED)
-			String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
-			if (!StringUtils.hasText(refreshToken) ||
-					parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REFRESH_TOKEN);
-			}
-
-			// scope (OPTIONAL)
-			String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
-			if (StringUtils.hasText(scope) &&
-					parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
-			}
-			Set<String> requestedScopes = null;
-			if (StringUtils.hasText(scope)) {
-				requestedScopes = new HashSet<>(
-						Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
-			}
-
-			// @formatter:off
-			Map<String, Object> additionalParameters = parameters
-					.entrySet()
-					.stream()
-					.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
-							!e.getKey().equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
-							!e.getKey().equals(OAuth2ParameterNames.SCOPE))
-					.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
-			// @formatter:on
-
-			return new OAuth2RefreshTokenAuthenticationToken(
-					refreshToken, clientPrincipal, requestedScopes, additionalParameters);
-		}
-	}
-
-	private static class ClientCredentialsAuthenticationConverter implements AuthenticationConverter {
-
-		@Override
-		public Authentication convert(HttpServletRequest request) {
-			// grant_type (REQUIRED)
-			String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
-			if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) {
-				return null;
-			}
-
-			Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
-
-			MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
-
-			// scope (OPTIONAL)
-			String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
-			if (StringUtils.hasText(scope) &&
-					parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
-			}
-			Set<String> requestedScopes = null;
-			if (StringUtils.hasText(scope)) {
-				requestedScopes = new HashSet<>(
-						Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
-			}
-
-			// @formatter:off
-			Map<String, Object> additionalParameters = parameters
-					.entrySet()
-					.stream()
-					.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
-							!e.getKey().equals(OAuth2ParameterNames.SCOPE))
-					.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
-			// @formatter:on
-
-			return new OAuth2ClientCredentialsAuthenticationToken(
-					clientPrincipal, requestedScopes, additionalParameters);
-		}
-	}
 }

+ 90 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeAuthenticationConverter.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020-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.server.authorization.web.authentication;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Authorization Code Grant
+ * and then converts it to an {@link OAuth2AuthorizationCodeAuthenticationToken} used for authenticating the authorization grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see AuthenticationConverter
+ * @see OAuth2AuthorizationCodeAuthenticationToken
+ * @see OAuth2TokenEndpointFilter
+ */
+public final class OAuth2AuthorizationCodeAuthenticationConverter implements AuthenticationConverter {
+
+	@Nullable
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		// grant_type (REQUIRED)
+		String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
+		if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) {
+			return null;
+		}
+
+		Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
+
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
+
+		// code (REQUIRED)
+		String code = parameters.getFirst(OAuth2ParameterNames.CODE);
+		if (!StringUtils.hasText(code) ||
+				parameters.get(OAuth2ParameterNames.CODE).size() != 1) {
+			OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CODE);
+		}
+
+		// redirect_uri (REQUIRED)
+		// Required only if the "redirect_uri" parameter was included in the authorization request
+		String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
+		if (StringUtils.hasText(redirectUri) &&
+				parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
+			OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
+		}
+
+		// @formatter:off
+		Map<String, Object> additionalParameters = parameters
+				.entrySet()
+				.stream()
+				.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
+						!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
+						!e.getKey().equals(OAuth2ParameterNames.CODE) &&
+						!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
+				.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
+        // @formatter:on
+
+		return new OAuth2AuthorizationCodeAuthenticationToken(
+				code, clientPrincipal, redirectUri, additionalParameters);
+	}
+
+}

+ 87 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ClientCredentialsAuthenticationConverter.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020-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.server.authorization.web.authentication;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Client Credentials Grant
+ * and then converts it to an {@link OAuth2ClientCredentialsAuthenticationToken} used for authenticating the authorization grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see AuthenticationConverter
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2TokenEndpointFilter
+ */
+public final class OAuth2ClientCredentialsAuthenticationConverter implements AuthenticationConverter {
+
+	@Nullable
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		// grant_type (REQUIRED)
+		String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
+		if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) {
+			return null;
+		}
+
+		Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
+
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
+
+		// scope (OPTIONAL)
+		String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
+		if (StringUtils.hasText(scope) &&
+				parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
+			OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
+		}
+		Set<String> requestedScopes = null;
+		if (StringUtils.hasText(scope)) {
+			requestedScopes = new HashSet<>(
+					Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
+		}
+
+		// @formatter:off
+		Map<String, Object> additionalParameters = parameters
+				.entrySet()
+				.stream()
+				.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
+						!e.getKey().equals(OAuth2ParameterNames.SCOPE))
+				.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
+        // @formatter:on
+
+		return new OAuth2ClientCredentialsAuthenticationToken(
+				clientPrincipal, requestedScopes, additionalParameters);
+	}
+}

+ 57 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020-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.server.authorization.web.authentication;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * Utility methods for the OAuth 2.0 Protocol Endpoints.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ */
+final class OAuth2EndpointUtils {
+
+	private OAuth2EndpointUtils() {
+	}
+
+	static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
+		Map<String, String[]> parameterMap = request.getParameterMap();
+		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
+		parameterMap.forEach((key, values) -> {
+			if (values.length > 0) {
+				for (String value : values) {
+					parameters.add(key, value);
+				}
+			}
+		});
+		return parameters;
+	}
+
+	static void throwError(String errorCode, String parameterName) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName,
+				"https://tools.ietf.org/html/rfc6749#section-5.2");
+		throw new OAuth2AuthenticationException(error);
+	}
+
+}

+ 95 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2RefreshTokenAuthenticationConverter.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020-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.server.authorization.web.authentication;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Refresh Token Grant
+ * and then converts it to an {@link OAuth2RefreshTokenAuthenticationToken} used for authenticating the authorization grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see AuthenticationConverter
+ * @see OAuth2RefreshTokenAuthenticationToken
+ * @see OAuth2TokenEndpointFilter
+ */
+public final class OAuth2RefreshTokenAuthenticationConverter implements AuthenticationConverter {
+
+	@Nullable
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		// grant_type (REQUIRED)
+		String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
+		if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
+			return null;
+		}
+
+		Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
+
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
+
+		// refresh_token (REQUIRED)
+		String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
+		if (!StringUtils.hasText(refreshToken) ||
+				parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
+			OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REFRESH_TOKEN);
+		}
+
+		// scope (OPTIONAL)
+		String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
+		if (StringUtils.hasText(scope) &&
+				parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
+			OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
+		}
+		Set<String> requestedScopes = null;
+		if (StringUtils.hasText(scope)) {
+			requestedScopes = new HashSet<>(
+					Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
+		}
+
+		// @formatter:off
+		Map<String, Object> additionalParameters = parameters
+				.entrySet()
+				.stream()
+				.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
+						!e.getKey().equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
+						!e.getKey().equals(OAuth2ParameterNames.SCOPE))
+				.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
+        // @formatter:on
+
+		return new OAuth2RefreshTokenAuthenticationToken(
+				refreshToken, clientPrincipal, requestedScopes, additionalParameters);
+	}
+}

+ 42 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java

@@ -57,6 +57,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.util.StringUtils;
@@ -110,6 +111,13 @@ public class OAuth2TokenEndpointFilterTests {
 				.hasMessage("tokenEndpointUri cannot be empty");
 	}
 
+	@Test
+	public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authenticationConverter cannot be null");
+	}
+
 	@Test
 	public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
@@ -413,6 +421,40 @@ public class OAuth2TokenEndpointFilterTests {
 		assertThat(refreshTokenResult.getTokenValue()).isEqualTo(refreshToken.getTokenValue());
 	}
 
+	@Test
+	public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
+
+		OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
+				new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null, null);
+
+		AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
+		when(authenticationConverter.convert(any())).thenReturn(authorizationCodeAuthentication);
+		this.filter.setAuthenticationConverter(authenticationConverter);
+
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "token",
+				Instant.now(), Instant.now().plus(Duration.ofHours(1)),
+				new HashSet<>(Arrays.asList("scope1", "scope2")));
+		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
+				new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
+
+		when(this.authenticationManager.authenticate(any())).thenReturn(accessTokenAuthentication);
+
+		SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+		securityContext.setAuthentication(clientPrincipal);
+		SecurityContextHolder.setContext(securityContext);
+
+		MockHttpServletRequest request = createAuthorizationCodeTokenRequest(registeredClient);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(authenticationConverter).convert(any());
+	}
+
 	@Test
 	public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
 		AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);