Add OidcReactiveOAuth2UserService

Issue: gh-5330
This commit is contained in:
Rob Winch 2018-06-06 13:43:48 -05:00
parent 5ed319b11a
commit f7a2a41241
2 changed files with 252 additions and 0 deletions

View File

@ -0,0 +1,95 @@
/*
* 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.userinfo;
import java.util.HashSet;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
/**
* An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect 1.0 Provider's.
*
* @author Rob Winch
* @since 5.1
* @see ReactiveOAuth2UserService
* @see OidcUserRequest
* @see OidcUser
* @see DefaultOidcUser
* @see OidcUserInfo
*/
public class OidcReactiveOAuth2UserService implements
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultReactiveOAuth2UserService();
@Override
public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
return getUserInfo(userRequest)
.map(userInfo -> new OidcUserAuthority(userRequest.getIdToken(), userInfo))
.defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), null))
.map(authority -> {
OidcUserInfo userInfo = authority.getUserInfo();
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
if (StringUtils.hasText(userNameAttributeName)) {
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
} else {
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
}
});
}
private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) {
if (!OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest)) {
return Mono.empty();
}
return this.oauth2UserService.loadUser(userRequest)
.map(OAuth2User::getAttributes)
.map(OidcUserInfo::new)
.doOnNext(userInfo -> {
String subject = userInfo.getSubject();
if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
});
}
public void setOauth2UserService(ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
Assert.notNull(oauth2UserService, "oauth2UserService cannot be null");
this.oauth2UserService = oauth2UserService;
}
}

View File

@ -0,0 +1,157 @@
/*
* 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.userinfo;
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.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
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.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
* @since 5.1
*/
@RunWith(MockitoJUnitRunner.class)
public class OidcReactiveOAuth2UserServiceTests {
@Mock
private ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService;
private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("id")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationUri("https://example.com/oauth2/authorize")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.userInfoUri("https://example.com/users/me")
.clientId("client-id")
.clientName("client-name")
.clientSecret("client-secret")
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("user")
.tokenUri("https://example.com/oauth/access_token");
private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(),
Instant.now().plusSeconds(3600), Collections
.singletonMap(IdTokenClaimNames.SUB, "sub123"));
private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"token",
Instant.now(),
Instant.now().plus(Duration.ofDays(1)),
Collections.singleton("user"));
private OidcReactiveOAuth2UserService userService = new OidcReactiveOAuth2UserService();
@Before
public void setup() {
this.userService.setOauth2UserService(this.oauth2UserService);
}
@Test
public void loadUserWhenUserInfoUriNullThenUserInfoNotRetrieved() {
this.registration.userInfoUri(null);
OidcUser user = this.userService.loadUser(userRequest()).block();
assertThat(user.getUserInfo()).isNull();
}
@Test
public void loadUserWhenOAuth2UserEmptyThenNullUserInfo() {
when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.empty());
OidcUser user = this.userService.loadUser(userRequest()).block();
assertThat(user.getUserInfo()).isNull();
}
@Test
public void loadUserWhenOAuth2UserSubjectNullThenOAuth2AuthenticationException() {
OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections.singletonMap("user", "rob"), "user");
when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User));
assertThatCode(() -> this.userService.loadUser(userRequest()).block())
.isInstanceOf(OAuth2AuthenticationException.class);
}
@Test
public void loadUserWhenOAuth2UserSubjectNotEqualThenOAuth2AuthenticationException() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(StandardClaimNames.SUB, "not-equal");
attributes.put("user", "rob");
OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"),
attributes, "user");
when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User));
assertThatCode(() -> this.userService.loadUser(userRequest()).block())
.isInstanceOf(OAuth2AuthenticationException.class);
}
@Test
public void loadUserWhenOAuth2UserThenUserInfoNotNull() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(StandardClaimNames.SUB, "sub123");
attributes.put("user", "rob");
OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"),
attributes, "user");
when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User));
assertThat(this.userService.loadUser(userRequest()).block().getUserInfo()).isNotNull();
}
@Test
public void loadUserWhenOAuth2UserAndUser() {
this.registration.userNameAttributeName("user");
Map<String, Object> attributes = new HashMap<>();
attributes.put(StandardClaimNames.SUB, "sub123");
attributes.put("user", "rob");
OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"),
attributes, "user");
when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User));
assertThat(this.userService.loadUser(userRequest()).block().getName()).isEqualTo("rob");
}
private OidcUserRequest userRequest() {
return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken);
}
}