From f8a9077d5ae84d3d4494f7e38ecd8c6e065556d7 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 4 Oct 2017 17:32:02 -0400 Subject: [PATCH] Generalize AuthorizationCodeAuthenticationProvider The AuthorizationCodeAuthenticationProvider implements part of the Authorization Code Grant flow as defined in OAuth 2.0 Authorization Framework and OpenID Connect Core 1.0. The implementation needs to be de-coupled to allow for better re-use and readability. This commit introduces the AuthorizationGrantAuthenticator and extracts logic from AuthorizationCodeAuthenticationProvider and provides different implementations for OAuth 2.0 and OpenID Connect 1.0. This re-factor is part of the work required for Issue gh-4513 --- ...ionCodeAuthenticationFilterConfigurer.java | 19 +++- ...thorizationCodeAuthenticationProvider.java | 95 ++++++------------- .../AuthorizationCodeAuthenticationToken.java | 12 ++- .../AuthorizationCodeAuthenticator.java | 66 +++++++++++++ .../AuthorizationGrantAuthenticator.java | 29 ++++++ ...gatingAuthorizationGrantAuthenticator.java | 59 ++++++++++++ ...ionCodeAuthenticationProcessingFilter.java | 46 ++------- .../OidcAuthorizationCodeAuthenticator.java | 95 +++++++++++++++++++ 8 files changed, 311 insertions(+), 110 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticator.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticator.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DelegatingAuthorizationGrantAuthenticator.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/authentication/OidcAuthorizationCodeAuthenticator.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java index 19ca2d78bc..ce0e4f670a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java @@ -20,6 +20,9 @@ import org.springframework.security.config.annotation.web.configurers.AbstractAu import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticator; +import org.springframework.security.oauth2.client.authentication.AuthorizationGrantAuthenticator; +import org.springframework.security.oauth2.client.authentication.DelegatingAuthorizationGrantAuthenticator; import org.springframework.security.oauth2.client.authentication.jwt.JwtDecoderRegistry; import org.springframework.security.oauth2.client.authentication.jwt.nimbus.NimbusJwtDecoderRegistry; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -34,6 +37,7 @@ import org.springframework.security.oauth2.client.web.AuthorizationGrantTokenExc import org.springframework.security.oauth2.client.web.nimbus.NimbusAuthorizationCodeTokenExchanger; import org.springframework.security.oauth2.core.AccessToken; import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.oidc.client.authentication.OidcAuthorizationCodeAuthenticator; import org.springframework.security.oauth2.oidc.client.user.OidcUserService; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -51,6 +55,7 @@ final class AuthorizationCodeAuthenticationFilterConfigurer, AuthorizationCodeAuthenticationProcessingFilter> { private R authorizationResponseMatcher; + private AuthorizationGrantAuthenticator authorizationCodeAuthenticator; private AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger; private SecurityTokenRepository accessTokenRepository; private JwtDecoderRegistry jwtDecoderRegistry; @@ -124,8 +129,7 @@ final class AuthorizationCodeAuthenticationFilterConfigurer getAuthorizationCodeAuthenticator() { + if (this.authorizationCodeAuthenticator == null) { + List> authenticators = new ArrayList<>(); + authenticators.add(new AuthorizationCodeAuthenticator(this.getAuthorizationCodeTokenExchanger())); + authenticators.add(new OidcAuthorizationCodeAuthenticator( + this.getAuthorizationCodeTokenExchanger(), this.getJwtDecoderRegistry())); + this.authorizationCodeAuthenticator = new DelegatingAuthorizationGrantAuthenticator<>(authenticators);; + } + return this.authorizationCodeAuthenticator; + } + private AuthorizationGrantTokenExchanger getAuthorizationCodeTokenExchanger() { if (this.authorizationCodeTokenExchanger == null) { this.authorizationCodeTokenExchanger = new NimbusAuthorizationCodeTokenExchanger(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java index 7ccadf245a..9ff709373b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java @@ -21,21 +21,12 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; -import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtDecoder; -import org.springframework.security.oauth2.client.authentication.jwt.JwtDecoderRegistry; -import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.token.SecurityTokenRepository; import org.springframework.security.oauth2.client.user.OAuth2UserService; -import org.springframework.security.oauth2.client.web.AuthorizationGrantTokenExchanger; import org.springframework.security.oauth2.core.AccessToken; -import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.oidc.client.authentication.OidcClientAuthenticationToken; import org.springframework.security.oauth2.oidc.client.authentication.OidcUserAuthenticationToken; -import org.springframework.security.oauth2.oidc.core.IdToken; -import org.springframework.security.oauth2.oidc.core.endpoint.OidcParameter; import org.springframework.security.oauth2.oidc.core.user.OidcUser; import org.springframework.util.Assert; @@ -51,16 +42,13 @@ import java.util.Collection; * associating it with the returned {@link OAuth2UserAuthenticationToken}. * *

- * The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantTokenExchanger} - * to make a request to the authorization server's Token Endpoint - * to verify the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()}. - * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}. + * The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantAuthenticator} + * to authenticate the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()} and ultimately + * return an "Authorized Client" as an {@link OAuth2ClientAuthenticationToken}. * *

- * It will then create an {@link OAuth2ClientAuthenticationToken} associating the {@link AccessToken} and optionally - * the {@link IdToken} from the {@link TokenResponseAttributes} and pass it to - * {@link OAuth2UserService#loadUser(OAuth2ClientAuthenticationToken)} to obtain the end-user's (resource owner) attributes - * in the form of an {@link OAuth2User}. + * It will then call {@link OAuth2UserService#loadUser(OAuth2ClientAuthenticationToken)} + * to obtain the end-user's (resource owner) attributes in the form of an {@link OAuth2User}. * *

* Finally, it will create an {@link OAuth2UserAuthenticationToken}, associating the {@link OAuth2User} @@ -73,10 +61,8 @@ import java.util.Collection; * @see OAuth2ClientAuthenticationToken * @see OidcClientAuthenticationToken * @see OAuth2UserAuthenticationToken - * @see AuthorizationGrantTokenExchanger - * @see TokenResponseAttributes - * @see AccessToken - * @see IdToken + * @see OidcUserAuthenticationToken + * @see AuthorizationGrantAuthenticator * @see OAuth2UserService * @see OAuth2User * @see OidcUser @@ -87,76 +73,51 @@ import java.util.Collection; * @see Section 3.1.3.3 OpenID Connect Token Response */ public class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { - private final AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger; + private final AuthorizationGrantAuthenticator authorizationCodeAuthenticator; private final SecurityTokenRepository accessTokenRepository; - private final JwtDecoderRegistry jwtDecoderRegistry; - private final OAuth2UserService userInfoService; - private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + private final OAuth2UserService userService; + private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); public AuthorizationCodeAuthenticationProvider( - AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger, + AuthorizationGrantAuthenticator authorizationCodeAuthenticator, SecurityTokenRepository accessTokenRepository, - JwtDecoderRegistry jwtDecoderRegistry, - OAuth2UserService userInfoService) { + OAuth2UserService userService) { - Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null"); + Assert.notNull(authorizationCodeAuthenticator, "authorizationCodeAuthenticator cannot be null"); Assert.notNull(accessTokenRepository, "accessTokenRepository cannot be null"); - Assert.notNull(jwtDecoderRegistry, "jwtDecoderRegistry cannot be null"); - Assert.notNull(userInfoService, "userInfoService cannot be null"); - this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger; + Assert.notNull(userService, "userService cannot be null"); + this.authorizationCodeAuthenticator = authorizationCodeAuthenticator; this.accessTokenRepository = accessTokenRepository; - this.jwtDecoderRegistry = jwtDecoderRegistry; - this.userInfoService = userInfoService; + this.userService = userService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (AuthorizationCodeAuthenticationToken) authentication; - ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); - TokenResponseAttributes tokenResponse = - this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication); + OAuth2ClientAuthenticationToken oauth2ClientAuthentication = + this.authorizationCodeAuthenticator.authenticate(authorizationCodeAuthentication); - AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(), - tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(), - tokenResponse.getExpiresAt(), tokenResponse.getScope()); + this.accessTokenRepository.saveSecurityToken( + oauth2ClientAuthentication.getAccessToken(), + oauth2ClientAuthentication.getClientRegistration()); - IdToken idToken = null; - if (tokenResponse.getAdditionalParameters().containsKey(OidcParameter.ID_TOKEN)) { - JwtDecoder jwtDecoder = this.jwtDecoderRegistry.getJwtDecoder(clientRegistration); - if (jwtDecoder == null) { - throw new IllegalArgumentException("Unable to find a registered JwtDecoder for Client Registration: '" + clientRegistration.getRegistrationId() + - "'. Check to ensure you have configured the JwkSet URI property."); - } - Jwt jwt = jwtDecoder.decode((String)tokenResponse.getAdditionalParameters().get(OidcParameter.ID_TOKEN)); - idToken = new IdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); - } + OAuth2User oauth2User = this.userService.loadUser(oauth2ClientAuthentication); - OAuth2ClientAuthenticationToken oauth2ClientAuthentication; - if (idToken != null) { - oauth2ClientAuthentication = new OidcClientAuthenticationToken(clientRegistration, accessToken, idToken); - } else { - oauth2ClientAuthentication = new OAuth2ClientAuthenticationToken(clientRegistration, accessToken); - } - oauth2ClientAuthentication.setDetails(authorizationCodeAuthentication.getDetails()); - - OAuth2User user = this.userInfoService.loadUser(oauth2ClientAuthentication); - - Collection authorities = - this.authoritiesMapper.mapAuthorities(user.getAuthorities()); + Collection mappedAuthorities = + this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); OAuth2UserAuthenticationToken oauth2UserAuthentication; - if (OidcUser.class.isAssignableFrom(user.getClass())) { + if (OidcUser.class.isAssignableFrom(oauth2User.getClass())) { oauth2UserAuthentication = new OidcUserAuthenticationToken( - (OidcUser)user, authorities, (OidcClientAuthenticationToken)oauth2ClientAuthentication); + (OidcUser)oauth2User, mappedAuthorities, (OidcClientAuthenticationToken)oauth2ClientAuthentication); } else { - oauth2UserAuthentication = new OAuth2UserAuthenticationToken(user, authorities, oauth2ClientAuthentication); + oauth2UserAuthentication = new OAuth2UserAuthenticationToken( + oauth2User, mappedAuthorities, oauth2ClientAuthentication); } oauth2UserAuthentication.setDetails(oauth2ClientAuthentication.getDetails()); - this.accessTokenRepository.saveSecurityToken(accessToken, clientRegistration); - return oauth2UserAuthentication; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java index 6d241c92c7..8f1e8640d1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes; import org.springframework.util.Assert; /** @@ -33,13 +34,18 @@ import org.springframework.util.Assert; public class AuthorizationCodeAuthenticationToken extends AuthorizationGrantAuthenticationToken { private final String authorizationCode; private final ClientRegistration clientRegistration; + private final AuthorizationRequestAttributes authorizationRequest; - public AuthorizationCodeAuthenticationToken(String authorizationCode, ClientRegistration clientRegistration) { + public AuthorizationCodeAuthenticationToken(String authorizationCode, + ClientRegistration clientRegistration, + AuthorizationRequestAttributes authorizationRequest) { super(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorityUtils.NO_AUTHORITIES); Assert.hasText(authorizationCode, "authorizationCode cannot be empty"); Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.notNull(authorizationRequest, "authorizationRequest cannot be null"); this.authorizationCode = authorizationCode; this.clientRegistration = clientRegistration; + this.authorizationRequest = authorizationRequest; this.setAuthenticated(false); } @@ -60,4 +66,8 @@ public class AuthorizationCodeAuthenticationToken extends AuthorizationGrantAuth public ClientRegistration getClientRegistration() { return this.clientRegistration; } + + public AuthorizationRequestAttributes getAuthorizationRequest() { + return this.authorizationRequest; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticator.java new file mode 100644 index 0000000000..51aa8f2706 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticator.java @@ -0,0 +1,66 @@ +/* + * 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.oauth2.client.web.AuthorizationGrantTokenExchanger; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; +import org.springframework.util.Assert; + +/** + * An implementation of an {@link AuthorizationGrantAuthenticator} that + * "authenticates" an authorization code grant credential + * against an OAuth 2.0 Provider's Token Endpoint. + * + * @author Joe Grandja + * @since 5.0 + */ +public class AuthorizationCodeAuthenticator implements AuthorizationGrantAuthenticator { + private final AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger; + + public AuthorizationCodeAuthenticator(AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger) { + Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null"); + this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger; + } + + @Override + public OAuth2ClientAuthenticationToken authenticate( + AuthorizationCodeAuthenticationToken authorizationCodeAuthentication) throws OAuth2AuthenticationException { + + // Section 3.1.2.1 Authentication Request - http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope + // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + // If the openid scope value is not present, the behavior is entirely unspecified. + if (authorizationCodeAuthentication.getAuthorizationRequest().getScope().contains("openid")) { + // The OpenID Connect implementation of AuthorizationGrantAuthenticator + // must handle OpenID Connect Authentication Requests + return null; + } + + TokenResponseAttributes tokenResponse = + this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication); + + AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(), + tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(), + tokenResponse.getExpiresAt(), tokenResponse.getScope()); + + OAuth2ClientAuthenticationToken oauth2ClientAuthentication = + new OAuth2ClientAuthenticationToken(authorizationCodeAuthentication.getClientRegistration(), accessToken); + oauth2ClientAuthentication.setDetails(authorizationCodeAuthentication.getDetails()); + + return oauth2ClientAuthentication; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticator.java new file mode 100644 index 0000000000..dac55f077a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticator.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * A strategy used for "authenticating" an authorization grant credential + * with the authorization server's Token Endpoint. + * + * @author Joe Grandja + * @since 5.0 + */ +public interface AuthorizationGrantAuthenticator { + + OAuth2ClientAuthenticationToken authenticate(T authorizationGrantAuthentication) throws OAuth2AuthenticationException; + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DelegatingAuthorizationGrantAuthenticator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DelegatingAuthorizationGrantAuthenticator.java new file mode 100644 index 0000000000..c866bba17d --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DelegatingAuthorizationGrantAuthenticator.java @@ -0,0 +1,59 @@ +/* + * 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.core.ResolvableType; +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * An implementation of an {@link AuthorizationGrantAuthenticator} that + * simply delegates to one of the {@link AuthorizationGrantAuthenticator}'s that it composes. + * + * @author Joe Grandja + * @since 5.0 + */ +public class DelegatingAuthorizationGrantAuthenticator implements AuthorizationGrantAuthenticator { + private final Map, List>> authenticators = new HashMap<>(); + + public DelegatingAuthorizationGrantAuthenticator(List> authenticators) { + Assert.notEmpty(authenticators, "authenticators cannot be empty"); + authenticators.forEach(authenticator -> { + Class authenticationType = + ResolvableType.forInstance(authenticator).as(AuthorizationGrantAuthenticator.class) + .resolveGeneric(0).asSubclass(AuthorizationGrantAuthenticationToken.class); + this.authenticators + .computeIfAbsent(authenticationType, k -> new LinkedList<>()) + .add(authenticator); + }); + } + + @Override + public OAuth2ClientAuthenticationToken authenticate(T authorizationGrantAuthentication) throws OAuth2AuthenticationException { + return this.authenticators.getOrDefault(authorizationGrantAuthentication.getClass(), Collections.emptyList()) + .stream() + .map(authenticator -> authenticator.authenticate(authorizationGrantAuthentication)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationCodeAuthenticationProcessingFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationCodeAuthenticationProcessingFilter.java index 90ca191bed..78d8de0478 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationCodeAuthenticationProcessingFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationCodeAuthenticationProcessingFilter.java @@ -21,21 +21,15 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException; -import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken; -import org.springframework.security.oauth2.client.authentication.OAuth2UserAuthenticationToken; 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.AuthorizationCodeAuthorizationResponseAttributes; import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes; import org.springframework.security.oauth2.core.endpoint.ErrorResponseAttributes; import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter; -import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -51,9 +45,9 @@ import java.io.IOException; * the processing of an OAuth 2.0 Authorization Response for the authorization code grant flow. * *

- * This Filter processes the Authorization Response in the following step sequence: + * This Filter processes the Authorization Response as follows: * - *

    + *
      *
    • * 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 Authorization Request) parameters @@ -62,47 +56,19 @@ import java.io.IOException; *
    • *
    • * This Filter will then create an {@link AuthorizationCodeAuthenticationToken} with - * the {@link OAuth2Parameter#CODE} received in the previous step and pass it to + * the {@link OAuth2Parameter#CODE} received in the previous step and delegate 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 Token Endpoint for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}. *
    • - *
    • - * Upon receiving the Access Token Request, the authorization server will authenticate the client, - * verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI} - * received matches the URI originally provided in the Authorization Request. - * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}. - *
    • - *
    • - * The {@link AuthorizationCodeAuthenticationProvider} will then create a new {@link OAuth2ClientAuthenticationToken} - * associating the {@link AccessToken} from the {@link TokenResponseAttributes} and pass it to - * {@link OAuth2UserService#loadUser(OAuth2ClientAuthenticationToken)}. The {@link OAuth2UserService} will make a request - * to the authorization server's UserInfo Endpoint (using the {@link AccessToken}) - * to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}. - *
    • - *
    • - * The {@link AuthorizationCodeAuthenticationProvider} will then create a {@link OAuth2UserAuthenticationToken} - * associating the {@link OAuth2ClientAuthenticationToken} and {@link OAuth2User} returned from the {@link OAuth2UserService}. - * Finally, the {@link OAuth2UserAuthenticationToken} is returned to the {@link AuthenticationManager} - * and then back to this Filter at which point the session is considered "authenticated". - *
    • - *
- * - *

- * NOTE: Steps 4-5 are not part of the authorization code grant flow and instead are - * "authentication flow" 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 AuthorizationCodeRequestRedirectFilter * @see AuthorizationRequestAttributes * @see AuthorizationRequestRepository - * @see AuthorizationCodeRequestRedirectFilter - * @see ClientRegistration * @see ClientRegistrationRepository * @see Section 4.1 Authorization Code Grant Flow * @see Section 4.1.2 Authorization Response @@ -154,7 +120,7 @@ public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAut this.authorizationCodeResponseConverter.apply(request); AuthorizationCodeAuthenticationToken authRequest = new AuthorizationCodeAuthenticationToken( - authorizationCodeResponseAttributes.getCode(), clientRegistration); + authorizationCodeResponseAttributes.getCode(), clientRegistration, matchingAuthorizationRequest); authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/authentication/OidcAuthorizationCodeAuthenticator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/authentication/OidcAuthorizationCodeAuthenticator.java new file mode 100644 index 0000000000..082dd192f2 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/authentication/OidcAuthorizationCodeAuthenticator.java @@ -0,0 +1,95 @@ +/* + * 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.oidc.client.authentication; + +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtDecoder; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.AuthorizationGrantAuthenticator; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException; +import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.jwt.JwtDecoderRegistry; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.AuthorizationGrantTokenExchanger; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; +import org.springframework.security.oauth2.oidc.core.IdToken; +import org.springframework.security.oauth2.oidc.core.endpoint.OidcParameter; +import org.springframework.util.Assert; + +/** + * An implementation of an {@link AuthorizationGrantAuthenticator} that + * "authenticates" an authorization code grant credential + * against an OpenID Connect 1.0 Provider's Token Endpoint. + * + * @author Joe Grandja + * @since 5.0 + */ +public class OidcAuthorizationCodeAuthenticator implements AuthorizationGrantAuthenticator { + private final AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger; + private final JwtDecoderRegistry jwtDecoderRegistry; + + public OidcAuthorizationCodeAuthenticator( + AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger, + JwtDecoderRegistry jwtDecoderRegistry) { + + Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null"); + Assert.notNull(jwtDecoderRegistry, "jwtDecoderRegistry cannot be null"); + this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger; + this.jwtDecoderRegistry = jwtDecoderRegistry; + } + + @Override + public OAuth2ClientAuthenticationToken authenticate( + AuthorizationCodeAuthenticationToken authorizationCodeAuthentication) throws OAuth2AuthenticationException { + + // Section 3.1.2.1 Authentication Request - http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope + // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + // If the openid scope value is not present, the behavior is entirely unspecified. + if (!authorizationCodeAuthentication.getAuthorizationRequest().getScope().contains("openid")) { + return null; + } + + ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); + + TokenResponseAttributes tokenResponse = + this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication); + + AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(), + tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(), + tokenResponse.getExpiresAt(), tokenResponse.getScope()); + + if (!tokenResponse.getAdditionalParameters().containsKey(OidcParameter.ID_TOKEN)) { + throw new IllegalArgumentException( + "Missing (required) ID Token in Token Response for Client Registration: '" + clientRegistration.getRegistrationId() + "'"); + } + + JwtDecoder jwtDecoder = this.jwtDecoderRegistry.getJwtDecoder(clientRegistration); + if (jwtDecoder == null) { + throw new IllegalArgumentException("Unable to find a registered JwtDecoder for Client Registration: '" + clientRegistration.getRegistrationId() + + "'. Check to ensure you have configured the JwkSet URI."); + } + Jwt jwt = jwtDecoder.decode((String)tokenResponse.getAdditionalParameters().get(OidcParameter.ID_TOKEN)); + IdToken idToken = new IdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + + OidcClientAuthenticationToken oidcClientAuthentication = + new OidcClientAuthenticationToken(clientRegistration, accessToken, idToken); + oidcClientAuthentication.setDetails(authorizationCodeAuthentication.getDetails()); + + return oidcClientAuthentication; + } +}