From d521d5e06635a843fc3fdec9efe5d7ce335bf6a6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 25 May 2018 14:54:15 -0500 Subject: [PATCH] Add OidcReactiveAuthenticationManager Fixes: gh-5330 --- .../OidcReactiveAuthenticationManager.java | 229 +++++++++++++++++ ...idcReactiveAuthenticationManagerTests.java | 237 ++++++++++++++++++ 2 files changed, 466 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java new file mode 100644 index 0000000000..dfb30ca3c0 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManager.java @@ -0,0 +1,229 @@ +/* + * 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.oidc.authentication; + +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +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.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.NimbusJwkReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +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 OidcReactiveAuthenticationManager implements + ReactiveAuthenticationManager { + + 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 static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier"; + + private final ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + private final ReactiveOAuth2UserService userService; + + private final ReactiveOAuth2AuthorizedClientService authorizedClientService; + + private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); + + private Function decoderFactory = new DefaultDecoderFactory(); + + public OidcReactiveAuthenticationManager( + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient, + ReactiveOAuth2UserService userService, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + Assert.notNull(userService, "userService cannot be null"); + Assert.notNull(authorizedClientService, "authorizedClientService"); + this.accessTokenResponseClient = accessTokenResponseClient; + this.userService = userService; + this.authorizedClientService = authorizedClientService; + } + + @Override + public Mono authenticate(Authentication authentication) { + return Mono.defer(() -> { + OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; + + // 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 (!authorizationCodeAuthentication.getAuthorizationExchange() + .getAuthorizationRequest().getScopes().contains("openid")) { + // This is an OpenID Connect Authentication Request so return empty + // and let OAuth2LoginReactiveAuthenticationManager handle it instead + return Mono.empty(); + } + + + OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationRequest(); + OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationResponse(); + + if (authorizationResponse.statusError()) { + throw new OAuth2AuthenticationException( + authorizationResponse.getError(), authorizationResponse.getError().toString()); + } + + if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange()); + + return this.accessTokenResponseClient.getTokenResponse(authzRequest) + .flatMap(accessTokenResponse -> authenticationResult(authorizationCodeAuthentication, accessTokenResponse)); + }); + } + + /** + * Provides a way to customize the {@link ReactiveJwtDecoder} given a {@link ClientRegistration} + * @param decoderFactory the {@link Function} used to create {@link ReactiveJwtDecoder} instance. Cannot be null. + */ + void setDecoderFactory( + Function decoderFactory) { + Assert.notNull(decoderFactory, "decoderFactory cannot be null"); + this.decoderFactory = decoderFactory; + } + + private Mono authenticationResult(OAuth2LoginAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { + OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + + ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); + + if (!accessTokenResponse.getAdditionalParameters().containsKey(OidcParameterNames.ID_TOKEN)) { + OAuth2Error invalidIdTokenError = new OAuth2Error( + INVALID_ID_TOKEN_ERROR_CODE, + "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), + null); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + } + + return createOidcToken(clientRegistration, accessTokenResponse) + .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken)) + .flatMap(this.userService::loadUser) + .flatMap(oauth2User -> { + Collection mappedAuthorities = + this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); + + OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange(), + oauth2User, + mappedAuthorities, + accessToken); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authenticationResult.getClientRegistration(), + authenticationResult.getName(), + authenticationResult.getAccessToken()); + OAuth2AuthenticationToken result = new OAuth2AuthenticationToken( + authenticationResult.getPrincipal(), + authenticationResult.getAuthorities(), + authenticationResult.getClientRegistration().getRegistrationId()); + return this.authorizedClientService.saveAuthorizedClient(authorizedClient, authenticationResult) + .thenReturn(result); + }); + } + + private Mono createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { + ReactiveJwtDecoder jwtDecoder = this.decoderFactory.apply(clientRegistration); + String rawIdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); + return jwtDecoder.decode(rawIdToken) + .map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims())) + .doOnNext(idToken -> OidcTokenValidator.validateIdToken(idToken, clientRegistration)); + } + + private static class DefaultDecoderFactory implements Function { + private final Map jwtDecoders = new ConcurrentHashMap<>(); + + @Override + public ReactiveJwtDecoder apply(ClientRegistration clientRegistration) { + ReactiveJwtDecoder jwtDecoder = this.jwtDecoders.get(clientRegistration.getRegistrationId()); + if (jwtDecoder == null) { + if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) { + OAuth2Error oauth2Error = new OAuth2Error( + MISSING_SIGNATURE_VERIFIER_ERROR_CODE, + "Failed to find a Signature Verifier for Client Registration: '" + + clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", + null + ); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + jwtDecoder = new NimbusJwkReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri()); + this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder); + } + return jwtDecoder; + } + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java new file mode 100644 index 0000000000..44120c712a --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcReactiveAuthenticationManagerTests.java @@ -0,0 +1,237 @@ +/* + * 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.oidc.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.authentication.TestingAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +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.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OidcReactiveAuthenticationManagerTests { + @Mock + private ReactiveOAuth2UserService userService; + + @Mock + private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + @Mock + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @Mock + private ReactiveJwtDecoder jwtDecoder; + + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("openid") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .jwkSetUri("https://example.com/oauth2/jwk") + .clientSecret("clientSecret"); + + private OAuth2AuthorizationResponse.Builder authorizationResponseBldr = OAuth2AuthorizationResponse + .success("code") + .state("state"); + + private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections.singletonMap(IdTokenClaimNames.SUB, "sub123")); + + private OidcReactiveAuthenticationManager manager; + + @Before + public void setup() { + this.manager = new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService); + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn( + Mono.empty()); + } + + @Test + public void constructorWhenNullAccessTokenResponseClientThenIllegalArgumentException() { + this.accessTokenResponseClient = null; + assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullUserServiceThenIllegalArgumentException() { + this.userService = null; + assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullAuthorizedClientServiceThenIllegalArgumentException() { + this.authorizedClientService = null; + assertThatThrownBy(() -> new OidcReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authenticateWhenNoSubscriptionThenDoesNothing() { + // we didn't do anything because it should cause a ClassCastException (as verified below) + TestingAuthenticationToken token = new TestingAuthenticationToken("a", "b"); + + assertThatCode(()-> this.manager.authenticate(token)) + .doesNotThrowAnyException(); + + assertThatThrownBy(() -> this.manager.authenticate(token).block()) + .isInstanceOf(Throwable.class); + } + + @Test + public void authenticationWhenNotOidcThenEmpty() { + this.registration.scope("notopenid"); + assertThat(this.manager.authenticate(loginToken()).block()).isNull(); + } + + @Test + public void authenticationWhenErrorThenOAuth2AuthenticationException() { + this.authorizationResponseBldr = OAuth2AuthorizationResponse + .error("error") + .state("state"); + assertThatThrownBy(() -> this.manager.authenticate(loginToken()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticationWhenStateDoesNotMatchThenOAuth2AuthenticationException() { + this.authorizationResponseBldr.state("notmatch"); + assertThatThrownBy(() -> this.manager.authenticate(loginToken()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticationWhenOAuth2UserNotFoundThenEmpty() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.")) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + when(this.userService.loadUser(any())).thenReturn(Mono.empty()); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + assertThat(this.manager.authenticate(loginToken()).block()).isNull(); + } + + @Test + public void authenticationWhenOAuth2UserFoundThenSuccess() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue())) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); + when(this.userService.loadUser(any())).thenReturn(Mono.just(user)); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + + OAuth2AuthenticationToken result = (OAuth2AuthenticationToken) this.manager.authenticate(loginToken()).block(); + + assertThat(result.getPrincipal()).isEqualTo(user); + assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities()); + assertThat(result.isAuthenticated()).isTrue(); + } + + private OAuth2LoginAuthenticationToken loginToken() { + ClientRegistration clientRegistration = this.registration.build(); + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("state") + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .scopes(clientRegistration.getScopes()) + .build(); + OAuth2AuthorizationResponse authorizationResponse = this.authorizationResponseBldr + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .build(); + OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(authorizationRequest, + authorizationResponse); + return new OAuth2LoginAuthenticationToken(clientRegistration, authorizationExchange); + } +}