parent
							
								
									f7a2a41241
								
							
						
					
					
						commit
						d521d5e066
					
				| 
						 | 
				
			
			@ -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.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * 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 <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.3">Section 4.1.3 Access Token Request</a>
 | 
			
		||||
 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
 | 
			
		||||
 */
 | 
			
		||||
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<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
 | 
			
		||||
 | 
			
		||||
	private final ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService;
 | 
			
		||||
 | 
			
		||||
	private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
 | 
			
		||||
 | 
			
		||||
	private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
 | 
			
		||||
 | 
			
		||||
	private Function<ClientRegistration, ReactiveJwtDecoder> decoderFactory = new DefaultDecoderFactory();
 | 
			
		||||
 | 
			
		||||
	public OidcReactiveAuthenticationManager(
 | 
			
		||||
			ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
 | 
			
		||||
			ReactiveOAuth2UserService<OidcUserRequest, OidcUser> 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<Authentication> 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<ClientRegistration, ReactiveJwtDecoder> decoderFactory) {
 | 
			
		||||
		Assert.notNull(decoderFactory, "decoderFactory cannot be null");
 | 
			
		||||
		this.decoderFactory = decoderFactory;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Mono<OAuth2AuthenticationToken> 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<? extends GrantedAuthority> 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<OidcIdToken> 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<ClientRegistration, ReactiveJwtDecoder> {
 | 
			
		||||
		private final Map<String, ReactiveJwtDecoder> 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<OidcUserRequest, OidcUser> userService;
 | 
			
		||||
 | 
			
		||||
	@Mock
 | 
			
		||||
	private ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> 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<String, Object> 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<String, Object> 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);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue