/* * Copyright 2012-2017 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 * * http://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.client.authentication; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.user.OAuth2UserService; import org.springframework.security.oauth2.client.web.converter.AuthorizationCodeAuthorizationResponseAttributesConverter; import org.springframework.security.oauth2.client.web.converter.ErrorResponseAttributesConverter; import org.springframework.security.oauth2.core.AccessToken; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.*; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 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 javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter.isDefaultRedirectUri; /** * An implementation of an {@link AbstractAuthenticationProcessingFilter} that handles * the processing of an <i>OAuth 2.0 Authorization Response</i> for the authorization code grant flow. * * <p> * This <code>Filter</code> processes the <i>Authorization Response</i> in the following step sequence: * * <ol> * <li> * Assuming the resource owner (end-user) has granted access to the client, the authorization server will append the * {@link OAuth2Parameter#CODE} and {@link OAuth2Parameter#STATE} (if provided in the <i>Authorization Request</i>) parameters * to the {@link OAuth2Parameter#REDIRECT_URI} (provided in the <i>Authorization Request</i>) * and redirect the end-user's user-agent back to this <code>Filter</code> (the client). * </li> * <li> * This <code>Filter</code> will then create an {@link AuthorizationCodeAuthenticationToken} with * the {@link OAuth2Parameter#CODE} received in the previous step and pass it to * {@link AuthorizationCodeAuthenticationProvider#authenticate(Authentication)} (indirectly via {@link AuthenticationManager}). * The {@link AuthorizationCodeAuthenticationProvider} will use an {@link AuthorizationGrantTokenExchanger} to make a request * to the authorization server's <i>Token Endpoint</i> for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}. * </li> * <li> * Upon receiving the <i>Access Token Request</i>, the authorization server will authenticate the client, * verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI} * received matches the <code>URI</code> originally provided in the <i>Authorization Request</i>. * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}. * </li> * <li> * The {@link AuthorizationCodeAuthenticationProvider} will then create a new {@link OAuth2AuthenticationToken} * associating the {@link AccessToken} from the {@link TokenResponseAttributes} and pass it to * {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}. The {@link OAuth2UserService} will make a request * to the authorization server's <i>UserInfo Endpoint</i> (using the {@link AccessToken}) * to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}. * </li> * <li> * The {@link AuthorizationCodeAuthenticationProvider} will create another new {@link OAuth2AuthenticationToken} * but this time associating the {@link AccessToken} and {@link OAuth2User} returned from the {@link OAuth2UserService}. * Finally, the {@link OAuth2AuthenticationToken} is returned to the {@link AuthenticationManager} * and then back to this <code>Filter</code> at which point the session is considered <i>"authenticated"</i>. * </li> * </ol> * * <p> * <b>NOTE:</b> Steps 4-5 are <b>not</b> part of the authorization code grant flow and instead are * <i>"authentication flow"</i> steps that are required in order to authenticate the end-user with the system. * * @author Joe Grandja * @since 5.0 * @see AbstractAuthenticationProcessingFilter * @see AuthorizationCodeAuthenticationToken * @see AuthorizationCodeAuthenticationProvider * @see AuthorizationGrantTokenExchanger * @see AuthorizationCodeAuthorizationResponseAttributes * @see AuthorizationRequestAttributes * @see AuthorizationRequestRepository * @see AuthorizationCodeRequestRedirectFilter * @see ClientRegistration * @see ClientRegistrationRepository * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a> * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a> */ public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code"; private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias"; private static final String AUTHORIZE_URI = AUTHORIZE_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}"; private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; private final ErrorResponseAttributesConverter errorResponseConverter = new ErrorResponseAttributesConverter(); private final AuthorizationCodeAuthorizationResponseAttributesConverter authorizationCodeResponseConverter = new AuthorizationCodeAuthorizationResponseAttributesConverter(); private final RequestMatcher authorizeRequestMatcher = new AntPathRequestMatcher(AUTHORIZE_URI); private ClientRegistrationRepository clientRegistrationRepository; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); public AuthorizationCodeAuthenticationProcessingFilter() { super(AUTHORIZE_URI); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { ErrorResponseAttributes authorizationError = this.errorResponseConverter.convert(request); if (authorizationError != null) { OAuth2Error oauth2Error = new OAuth2Error(authorizationError.getErrorCode(), authorizationError.getDescription(), authorizationError.getUri()); this.getAuthorizationRequestRepository().removeAuthorizationRequest(request); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } AuthorizationRequestAttributes matchingAuthorizationRequest = this.resolveAuthorizationRequest(request); ClientRegistration clientRegistration = this.getClientRegistrationRepository().getRegistrationByClientId( matchingAuthorizationRequest.getClientId()); // If clientRegistration.redirectUri is the default one (with Uri template variables) // then use matchingAuthorizationRequest.redirectUri instead if (isDefaultRedirectUri(clientRegistration)) { clientRegistration = new ClientRegistrationBuilderWithUriOverrides( clientRegistration, matchingAuthorizationRequest.getRedirectUri()).build(); } AuthorizationCodeAuthorizationResponseAttributes authorizationCodeResponseAttributes = this.authorizationCodeResponseConverter.convert(request); AuthorizationCodeAuthenticationToken authRequest = new AuthorizationCodeAuthenticationToken( authorizationCodeResponseAttributes.getCode(), clientRegistration); authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); Authentication authenticated = this.getAuthenticationManager().authenticate(authRequest); return authenticated; } public RequestMatcher getAuthorizeRequestMatcher() { return this.authorizeRequestMatcher; } protected ClientRegistrationRepository getClientRegistrationRepository() { return this.clientRegistrationRepository; } public final void setClientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty"); this.clientRegistrationRepository = clientRegistrationRepository; } protected AuthorizationRequestRepository getAuthorizationRequestRepository() { return this.authorizationRequestRepository; } public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) { Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null"); this.authorizationRequestRepository = authorizationRequestRepository; } private AuthorizationRequestAttributes resolveAuthorizationRequest(HttpServletRequest request) { AuthorizationRequestAttributes authorizationRequest = this.getAuthorizationRequestRepository().loadAuthorizationRequest(request); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } this.getAuthorizationRequestRepository().removeAuthorizationRequest(request); this.assertMatchingAuthorizationRequest(request, authorizationRequest); return authorizationRequest; } private void assertMatchingAuthorizationRequest(HttpServletRequest request, AuthorizationRequestAttributes authorizationRequest) { String state = request.getParameter(OAuth2Parameter.STATE); if (!authorizationRequest.getState().equals(state)) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } if (!request.getRequestURL().toString().equals(authorizationRequest.getRedirectUri())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } } private static class ClientRegistrationBuilderWithUriOverrides extends ClientRegistration.Builder { private ClientRegistrationBuilderWithUriOverrides(ClientRegistration clientRegistration, String redirectUri) { super(clientRegistration.getClientId()); this.clientSecret(clientRegistration.getClientSecret()); this.clientAuthenticationMethod(clientRegistration.getClientAuthenticationMethod()); this.authorizedGrantType(clientRegistration.getAuthorizedGrantType()); this.redirectUri(redirectUri); if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { this.scopes(clientRegistration.getScopes().stream().toArray(String[]::new)); } this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()); this.tokenUri(clientRegistration.getProviderDetails().getTokenUri()); this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri()); this.clientName(clientRegistration.getClientName()); this.clientAlias(clientRegistration.getClientAlias()); } } }