Prefer `sub` claim as OIDC principal name
This commit removes preference for `name` claim as principal name in `DefaultOidcUser` so that the default is now `sub` claim. In addition to that, `DefaultOidcUser` now also provides constructors to explicitly define the claim to be preferred as principal name. Fixes gh-4515
This commit is contained in:
parent
1fe414379c
commit
549decf00a
|
@ -51,7 +51,7 @@ public class DefaultOAuth2User implements OAuth2User {
|
|||
throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey);
|
||||
}
|
||||
this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities));
|
||||
this.setAttributes(attributes);
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
this.nameAttributeKey = nameAttributeKey;
|
||||
}
|
||||
|
||||
|
@ -70,11 +70,6 @@ public class DefaultOAuth2User implements OAuth2User {
|
|||
return this.attributes;
|
||||
}
|
||||
|
||||
protected final void setAttributes(Map<String, Object> attributes) {
|
||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
}
|
||||
|
||||
private Set<GrantedAuthority> sortAuthorities(Set<GrantedAuthority> authorities) {
|
||||
SortedSet<GrantedAuthority> sortedAuthorities =
|
||||
new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority()));
|
||||
|
|
|
@ -13,21 +13,19 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.oidc.core.user;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
import org.springframework.security.oauth2.oidc.core.IdToken;
|
||||
import org.springframework.security.oauth2.oidc.core.IdTokenClaim;
|
||||
import org.springframework.security.oauth2.oidc.core.StandardClaim;
|
||||
import org.springframework.security.oauth2.oidc.core.UserInfo;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.security.oauth2.oidc.core.StandardClaim.NAME;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The default implementation of an {@link OidcUser}.
|
||||
|
@ -35,10 +33,10 @@ import static org.springframework.security.oauth2.oidc.core.StandardClaim.NAME;
|
|||
* <p>
|
||||
* The claim used for accessing the "name" of the
|
||||
* user <code>Principal</code> via {@link #getClaims()}
|
||||
* is {@link StandardClaim#NAME} or if not available
|
||||
* will default to {@link IdTokenClaim#SUB}.
|
||||
* is {@link IdTokenClaim#SUB}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Vedran Pavic
|
||||
* @since 5.0
|
||||
* @see OidcUser
|
||||
* @see DefaultOAuth2User
|
||||
|
@ -50,20 +48,22 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
private final UserInfo userInfo;
|
||||
|
||||
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken) {
|
||||
this(authorities, idToken, null);
|
||||
this(authorities, idToken, IdTokenClaim.SUB);
|
||||
}
|
||||
|
||||
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken, String nameAttributeKey) {
|
||||
this(authorities, idToken, null, nameAttributeKey);
|
||||
}
|
||||
|
||||
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken, UserInfo userInfo) {
|
||||
super(authorities, idToken.getClaims(), IdTokenClaim.SUB);
|
||||
this(authorities, idToken, userInfo, IdTokenClaim.SUB);
|
||||
}
|
||||
|
||||
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken, UserInfo userInfo,
|
||||
String nameAttributeKey) {
|
||||
super(authorities, resolveAttributes(idToken, userInfo), nameAttributeKey);
|
||||
this.idToken = idToken;
|
||||
this.userInfo = userInfo;
|
||||
if (userInfo != null) {
|
||||
this.setAttributes(
|
||||
Stream.of(this.getAttributes(), userInfo.getClaims())
|
||||
.flatMap(m -> m.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (k1, k2) -> k1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -71,9 +71,21 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
return this.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
String name = this.getClaimAsString(NAME);
|
||||
return (name != null ? name : super.getName());
|
||||
public IdToken getIdToken() {
|
||||
return this.idToken;
|
||||
}
|
||||
|
||||
public UserInfo getUserInfo() {
|
||||
return this.userInfo;
|
||||
}
|
||||
|
||||
private static Map<String, Object> resolveAttributes(IdToken idToken, UserInfo userInfo) {
|
||||
Assert.notNull(idToken, "idToken cannot be null");
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.putAll(idToken.getClaims());
|
||||
if (userInfo != null) {
|
||||
attributes.putAll(userInfo.getClaims());
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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.core.user;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultOAuth2User}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public class DefaultOAuth2UserTests {
|
||||
|
||||
private static final SimpleGrantedAuthority TEST_AUTHORITY = new SimpleGrantedAuthority("ROLE_USER");
|
||||
|
||||
private static final Set<GrantedAuthority> TEST_AUTHORITIES = Collections.singleton(TEST_AUTHORITY);
|
||||
|
||||
private static final String TEST_ATTRIBUTE_NAME_KEY = "username";
|
||||
|
||||
private static final String TEST_USERNAME = "test";
|
||||
|
||||
private static final Map<String, Object> TEST_ATTRIBUTES = Collections.singletonMap(
|
||||
TEST_ATTRIBUTE_NAME_KEY, TEST_USERNAME);
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void constructorWhenParametersAreValidThenIsCreated() {
|
||||
DefaultOAuth2User user = new DefaultOAuth2User(TEST_AUTHORITIES, TEST_ATTRIBUTES, TEST_ATTRIBUTE_NAME_KEY);
|
||||
|
||||
assertThat(user.getName()).isEqualTo(TEST_USERNAME);
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isEqualTo(TEST_AUTHORITY);
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(TEST_ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesAreNullThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("authorities cannot be empty");
|
||||
|
||||
new DefaultOAuth2User(null, TEST_ATTRIBUTES, TEST_ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesAreEmptyThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("authorities cannot be empty");
|
||||
|
||||
new DefaultOAuth2User(Collections.emptySet(), TEST_ATTRIBUTES, TEST_ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAttributesAreNullThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("attributes cannot be empty");
|
||||
|
||||
new DefaultOAuth2User(TEST_AUTHORITIES, null, TEST_ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAttributesAreEmptyThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("attributes cannot be empty");
|
||||
|
||||
new DefaultOAuth2User(TEST_AUTHORITIES, Collections.emptyMap(), TEST_ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNameAttributeKeyIsNullThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("nameAttributeKey cannot be empty");
|
||||
|
||||
new DefaultOAuth2User(TEST_AUTHORITIES, TEST_ATTRIBUTES, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNameAttributeKeyIsInvalidThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Invalid nameAttributeKey: invalid");
|
||||
|
||||
new DefaultOAuth2User(TEST_AUTHORITIES, TEST_ATTRIBUTES, "invalid");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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.core.user;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.oidc.core.IdToken;
|
||||
import org.springframework.security.oauth2.oidc.core.IdTokenClaim;
|
||||
import org.springframework.security.oauth2.oidc.core.StandardClaim;
|
||||
import org.springframework.security.oauth2.oidc.core.UserInfo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultOidcUser}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public class DefaultOidcUserTests {
|
||||
|
||||
private static final SimpleGrantedAuthority TEST_AUTHORITY = new SimpleGrantedAuthority("ROLE_USER");
|
||||
|
||||
private static final Set<GrantedAuthority> TEST_AUTHORITIES = Collections.singleton(TEST_AUTHORITY);
|
||||
|
||||
private static final String TEST_SUBJECT = "test";
|
||||
|
||||
private static final String TEST_EMAIL = "test@example.com";
|
||||
|
||||
private static final Map<String, Object> TEST_ID_TOKEN_CLAIMS = new HashMap<>();
|
||||
|
||||
static {
|
||||
TEST_ID_TOKEN_CLAIMS.put(IdTokenClaim.ISS, "https://example.com");
|
||||
TEST_ID_TOKEN_CLAIMS.put(IdTokenClaim.SUB, TEST_SUBJECT);
|
||||
}
|
||||
|
||||
private static final IdToken TEST_ID_TOKEN = new IdToken("value", Instant.EPOCH, Instant.MAX, TEST_ID_TOKEN_CLAIMS);
|
||||
|
||||
private static final UserInfo TEST_USER_INFO = new UserInfo(Collections.singletonMap(StandardClaim.EMAIL, TEST_EMAIL));
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesAndIdTokenThenIsCreated() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(TEST_AUTHORITIES, TEST_ID_TOKEN);
|
||||
|
||||
assertThat(user.getName()).isEqualTo(TEST_SUBJECT);
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isEqualTo(TEST_AUTHORITY);
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaim.ISS, IdTokenClaim.SUB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesAndIdTokenAndNameAttributeKeyThenIsCreated() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(TEST_AUTHORITIES, TEST_ID_TOKEN, IdTokenClaim.SUB);
|
||||
|
||||
assertThat(user.getName()).isEqualTo(TEST_SUBJECT);
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isEqualTo(TEST_AUTHORITY);
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaim.ISS, IdTokenClaim.SUB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesAndIdTokenAndUserInfoThenIsCreated() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(TEST_AUTHORITIES, TEST_ID_TOKEN, TEST_USER_INFO);
|
||||
|
||||
assertThat(user.getName()).isEqualTo(TEST_SUBJECT);
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isEqualTo(TEST_AUTHORITY);
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaim.ISS, IdTokenClaim.SUB, StandardClaim.EMAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesAndIdTokenAndUserInfoAndNameAttributeKeyThenIsCreated() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(TEST_AUTHORITIES, TEST_ID_TOKEN, TEST_USER_INFO, StandardClaim.EMAIL);
|
||||
|
||||
assertThat(user.getName()).isEqualTo(TEST_EMAIL);
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isEqualTo(TEST_AUTHORITY);
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaim.ISS, IdTokenClaim.SUB, StandardClaim.EMAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenIdTokenIsNullThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("idToken cannot be null");
|
||||
|
||||
new DefaultOidcUser(TEST_AUTHORITIES, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNameAttributeKeyClaimIsNotPresentThenThrowsException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Invalid nameAttributeKey: " + StandardClaim.NAME);
|
||||
|
||||
new DefaultOidcUser(TEST_AUTHORITIES, TEST_ID_TOKEN, TEST_USER_INFO, StandardClaim.NAME);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue