From 8b67154e779b629fa61bf9ebd837d1ba057b166d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 2 Aug 2018 15:37:29 -0500 Subject: [PATCH] Add OAuth2AuthorizationCodeReactiveAuthenticationManager Issue: gh-5620 --- ...tionCodeReactiveAuthenticationManager.java | 93 +++++++++++++ ...odeReactiveAuthenticationManagerTests.java | 123 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java new file mode 100644 index 0000000000..76c8861a0d --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2018 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.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +/** + * An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login, + * which leverages the OAuth 2.0 Authorization Code Grant Flow. + * + * This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating + * an Authorization Code credential with the Authorization Server's Token Endpoint + * and if valid, exchanging it for an Access Token credential. + *

+ * It will also obtain the user attributes of the End-User (Resource Owner) + * from the UserInfo Endpoint using an {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService}, + * which will create a {@code Principal} in the form of an {@link OAuth2User}. + * The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken} + * to complete the authentication. + * + * @author Rob Winch + * @since 5.1 + * @see OAuth2LoginAuthenticationToken + * @see ReactiveOAuth2AccessTokenResponseClient + * @see ReactiveOAuth2UserService + * @see OAuth2User + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.3 Access Token Request + * @see Section 4.1.4 Access Token Response + */ +public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements + ReactiveAuthenticationManager { + private final ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + public OAuth2AuthorizationCodeReactiveAuthenticationManager( + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient) { + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + this.accessTokenResponseClient = accessTokenResponseClient; + } + + @Override + public Mono authenticate(Authentication authentication) { + return Mono.defer(() -> { + OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication; + + OAuth2AuthorizationExchangeValidator.validate(token.getAuthorizationExchange()); + + OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest( + token.getClientRegistration(), + token.getAuthorizationExchange()); + + return this.accessTokenResponseClient.getTokenResponse(authzRequest) + .map(onSuccess(token)); + }); + } + + private Function onSuccess(OAuth2AuthorizationCodeAuthenticationToken token) { + return accessTokenResponse -> { + ClientRegistration registration = token.getClientRegistration(); + OAuth2AuthorizationExchange exchange = token.getAuthorizationExchange(); + OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + OAuth2RefreshToken refreshToken = accessTokenResponse.getRefreshToken(); + return new OAuth2AuthorizationCodeAuthenticationToken(registration, exchange, accessToken, refreshToken); + }; + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java new file mode 100644 index 0000000000..97fd5210f3 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 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.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests { + @Mock + private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + private OAuth2AuthorizationCodeReactiveAuthenticationManager manager; + + private ClientRegistration.Builder registration = TestClientRegistrations.clientRegistration(); + + private OAuth2AuthorizationRequest.Builder authorizationRequest = TestOAuth2AuthorizationRequests.request(); + + private OAuth2AuthorizationResponse.Builder authorizationResponse = TestOAuth2AuthorizationResponses.success(); + + private OAuth2AccessTokenResponse.Builder tokenResponse = TestOAuth2AccessTokenResponses + .accessTokenResponse(); + + @Before + public void setup() { + this.manager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient); + } + + @Test + public void authenticateWhenErrorThenOAuth2AuthenticationException() { + this.authorizationResponse = TestOAuth2AuthorizationResponses.error(); + assertThatCode(() -> authenticate()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticateWhenStateNotEqualThenOAuth2AuthenticationException() { + this.authorizationRequest.state("notequal"); + assertThatCode(() -> authenticate()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticateWhenRedirectUriNotEqualThenOAuth2AuthenticationException() { + this.authorizationRequest.redirectUri("https://example.org/notequal"); + assertThatCode(() -> authenticate()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticateWhenValidThenSuccess() { + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build())); + + OAuth2AuthorizationCodeAuthenticationToken result = authenticate(); + + assertThat(result).isNotNull(); + } + + @Test + public void authenticateWhenEmptyThenEmpty() { + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.empty()); + + OAuth2AuthorizationCodeAuthenticationToken result = authenticate(); + + assertThat(result).isNull(); + } + + @Test + public void authenticateWhenOAuth2AuthenticationExceptionThenOAuth2AuthenticationException() { + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.error(() -> new OAuth2AuthenticationException(new OAuth2Error("error")))); + + assertThatCode(() -> authenticate()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + private OAuth2AuthorizationCodeAuthenticationToken authenticate() { + OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange( + this.authorizationRequest.build(), this.authorizationResponse.build()); + OAuth2AuthorizationCodeAuthenticationToken token = new OAuth2AuthorizationCodeAuthenticationToken( + this.registration.build(), exchange); + return (OAuth2AuthorizationCodeAuthenticationToken) this.manager.authenticate(token).block(); + } +}