|
@@ -17,11 +17,8 @@ package org.springframework.security.oauth2.server.authorization.web;
|
|
|
|
|
|
import java.io.IOException;
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
-import java.util.Arrays;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.HashSet;
|
|
-import java.util.Map;
|
|
|
|
import java.util.Set;
|
|
import java.util.Set;
|
|
-import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
import javax.servlet.FilterChain;
|
|
import javax.servlet.FilterChain;
|
|
import javax.servlet.ServletException;
|
|
import javax.servlet.ServletException;
|
|
@@ -31,24 +28,20 @@ import javax.servlet.http.HttpServletResponse;
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.MediaType;
|
|
-import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
|
|
import org.springframework.security.authentication.AuthenticationManager;
|
|
import org.springframework.security.authentication.AuthenticationManager;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.AuthenticationException;
|
|
import org.springframework.security.core.AuthenticationException;
|
|
-import org.springframework.security.core.authority.AuthorityUtils;
|
|
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
|
-import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
|
|
|
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
-import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
|
|
|
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
|
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException;
|
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException;
|
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
|
|
import org.springframework.security.web.DefaultRedirectStrategy;
|
|
import org.springframework.security.web.DefaultRedirectStrategy;
|
|
import org.springframework.security.web.RedirectStrategy;
|
|
import org.springframework.security.web.RedirectStrategy;
|
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
|
@@ -60,7 +53,6 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
|
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
import org.springframework.util.Assert;
|
|
import org.springframework.util.Assert;
|
|
-import org.springframework.util.MultiValueMap;
|
|
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
@@ -88,8 +80,8 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|
|
|
|
|
private final AuthenticationManager authenticationManager;
|
|
private final AuthenticationManager authenticationManager;
|
|
private final RequestMatcher authorizationEndpointMatcher;
|
|
private final RequestMatcher authorizationEndpointMatcher;
|
|
- private final AuthenticationConverter authenticationConverter;
|
|
|
|
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
|
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
|
|
|
+ private AuthenticationConverter authenticationConverter;
|
|
private String consentPage;
|
|
private String consentPage;
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -164,17 +156,6 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|
return new OrRequestMatcher(authorizationRequestMatcher, authorizationConsentMatcher);
|
|
return new OrRequestMatcher(authorizationRequestMatcher, authorizationConsentMatcher);
|
|
}
|
|
}
|
|
|
|
|
|
- /**
|
|
|
|
- * Specify the URI to redirect Resource Owners to if consent is required. A default consent
|
|
|
|
- * page will be generated when this attribute is not specified.
|
|
|
|
- *
|
|
|
|
- * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
|
|
|
|
- * @see org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer#consentPage(String)
|
|
|
|
- */
|
|
|
|
- public final void setConsentPage(String consentPage) {
|
|
|
|
- this.consentPage = consentPage;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
@Override
|
|
@Override
|
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
throws ServletException, IOException {
|
|
throws ServletException, IOException {
|
|
@@ -212,6 +193,28 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
|
|
|
+ * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
|
|
|
+ *
|
|
|
|
+ * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
|
|
|
+ */
|
|
|
|
+ public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
|
|
|
+ Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
|
|
|
+ this.authenticationConverter = authenticationConverter;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Specify the URI to redirect Resource Owners to if consent is required. A default consent
|
|
|
|
+ * page will be generated when this attribute is not specified.
|
|
|
|
+ *
|
|
|
|
+ * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
|
|
|
|
+ * @see org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer#consentPage(String)
|
|
|
|
+ */
|
|
|
|
+ public final void setConsentPage(String consentPage) {
|
|
|
|
+ this.consentPage = consentPage;
|
|
|
|
+ }
|
|
|
|
+
|
|
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
|
|
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
|
|
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
|
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
|
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException {
|
|
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException {
|
|
@@ -294,136 +297,6 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
|
|
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
|
|
}
|
|
}
|
|
|
|
|
|
- private static class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter {
|
|
|
|
- private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
|
|
|
|
- "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
|
|
|
- private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
|
|
|
- private final RequestMatcher oidcAuthenticationRequestMatcher;
|
|
|
|
-
|
|
|
|
- private OAuth2AuthorizationCodeRequestAuthenticationConverter() {
|
|
|
|
- RequestMatcher postMethodMatcher = request -> "POST".equals(request.getMethod());
|
|
|
|
- RequestMatcher responseTypeParameterMatcher = request ->
|
|
|
|
- request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null;
|
|
|
|
- RequestMatcher openidScopeMatcher = request -> {
|
|
|
|
- String scope = request.getParameter(OAuth2ParameterNames.SCOPE);
|
|
|
|
- return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID);
|
|
|
|
- };
|
|
|
|
- this.oidcAuthenticationRequestMatcher = new AndRequestMatcher(
|
|
|
|
- postMethodMatcher, responseTypeParameterMatcher, openidScopeMatcher);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- @Override
|
|
|
|
- public Authentication convert(HttpServletRequest request) {
|
|
|
|
- MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
|
|
|
-
|
|
|
|
- boolean authorizationRequest = false;
|
|
|
|
- if ("GET".equals(request.getMethod()) || this.oidcAuthenticationRequestMatcher.matches(request)) {
|
|
|
|
- authorizationRequest = true;
|
|
|
|
-
|
|
|
|
- // response_type (REQUIRED)
|
|
|
|
- String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE);
|
|
|
|
- if (!StringUtils.hasText(responseType) ||
|
|
|
|
- parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
|
|
|
- } else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
|
|
|
- throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- String authorizationUri = request.getRequestURL().toString();
|
|
|
|
-
|
|
|
|
- // client_id (REQUIRED)
|
|
|
|
- String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
|
|
|
- if (!StringUtils.hasText(clientId) ||
|
|
|
|
- parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
|
|
|
- if (principal == null) {
|
|
|
|
- principal = ANONYMOUS_AUTHENTICATION;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // redirect_uri (OPTIONAL)
|
|
|
|
- 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);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // scope (OPTIONAL)
|
|
|
|
- Set<String> scopes = null;
|
|
|
|
- if (authorizationRequest) {
|
|
|
|
- String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
|
|
|
- if (StringUtils.hasText(scope) &&
|
|
|
|
- parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
|
|
|
- }
|
|
|
|
- if (StringUtils.hasText(scope)) {
|
|
|
|
- scopes = new HashSet<>(
|
|
|
|
- Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
|
|
|
- }
|
|
|
|
- } else {
|
|
|
|
- // Consent request
|
|
|
|
- if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
|
|
|
|
- scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // state (RECOMMENDED)
|
|
|
|
- String state = parameters.getFirst(OAuth2ParameterNames.STATE);
|
|
|
|
- if (StringUtils.hasText(state) &&
|
|
|
|
- parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
|
|
|
- String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE);
|
|
|
|
- if (StringUtils.hasText(codeChallenge) &&
|
|
|
|
- parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
|
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE)
|
|
|
|
- String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
|
|
|
- if (StringUtils.hasText(codeChallengeMethod) &&
|
|
|
|
- parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) {
|
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // @formatter:off
|
|
|
|
- Map<String, Object> additionalParameters = parameters
|
|
|
|
- .entrySet()
|
|
|
|
- .stream()
|
|
|
|
- .filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
|
|
|
|
- !e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
|
|
|
|
- !e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) &&
|
|
|
|
- !e.getKey().equals(OAuth2ParameterNames.SCOPE) &&
|
|
|
|
- !e.getKey().equals(OAuth2ParameterNames.STATE))
|
|
|
|
- .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
|
|
|
- // @formatter:on
|
|
|
|
-
|
|
|
|
- return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal)
|
|
|
|
- .authorizationUri(authorizationUri)
|
|
|
|
- .redirectUri(redirectUri)
|
|
|
|
- .scopes(scopes)
|
|
|
|
- .state(state)
|
|
|
|
- .additionalParameters(additionalParameters)
|
|
|
|
- .consent(!authorizationRequest)
|
|
|
|
- .build();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private static void throwError(String errorCode, String parameterName) {
|
|
|
|
- throwError(errorCode, parameterName, "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private static void throwError(String errorCode, String parameterName, String errorUri) {
|
|
|
|
- OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
|
|
|
|
- throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
/**
|
|
/**
|
|
* For internal use only.
|
|
* For internal use only.
|
|
*/
|
|
*/
|