From 5a792346776e3cbc7916e59a45ec5e1441516f11 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 10 Jun 2021 04:19:20 -0400 Subject: [PATCH] Provide extension for processing access token request Issue gh-319 --- .../web/OAuth2TokenEndpointFilter.java | 172 +++--------------- ...horizationCodeAuthenticationConverter.java | 90 +++++++++ ...entCredentialsAuthenticationConverter.java | 87 +++++++++ .../authentication/OAuth2EndpointUtils.java | 57 ++++++ ...h2RefreshTokenAuthenticationConverter.java | 95 ++++++++++ .../web/OAuth2TokenEndpointFilterTests.java | 42 +++++ 6 files changed, 393 insertions(+), 150 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeAuthenticationConverter.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ClientCredentialsAuthenticationConverter.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2RefreshTokenAuthenticationConverter.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java index bdb9bbe9..a88ee48e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java +++ b/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 accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); private final HttpMessageConverter 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 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 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 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 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 requestedScopes = null; - if (StringUtils.hasText(scope)) { - requestedScopes = new HashSet<>( - Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); - } - - // @formatter:off - Map 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 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 requestedScopes = null; - if (StringUtils.hasText(scope)) { - requestedScopes = new HashSet<>( - Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); - } - - // @formatter:off - Map 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); - } - } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeAuthenticationConverter.java new file mode 100644 index 00000000..03ae3dca --- /dev/null +++ b/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 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 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); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ClientCredentialsAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ClientCredentialsAuthenticationConverter.java new file mode 100644 index 00000000..9d1ade0f --- /dev/null +++ b/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 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 requestedScopes = null; + if (StringUtils.hasText(scope)) { + requestedScopes = new HashSet<>( + Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + + // @formatter:off + Map 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); + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java new file mode 100644 index 00000000..cb64eb1f --- /dev/null +++ b/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 getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap 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); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2RefreshTokenAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2RefreshTokenAuthenticationConverter.java new file mode 100644 index 00000000..28e1bbb3 --- /dev/null +++ b/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 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 requestedScopes = null; + if (StringUtils.hasText(scope)) { + requestedScopes = new HashSet<>( + Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + + // @formatter:off + Map 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); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java index ca4de67e..f5cbf151 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java +++ b/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);