Skip to content

Commit

Permalink
Add support for nested username attribute in DefaultOAuth2User
Browse files Browse the repository at this point in the history
Closes spring-projectsgh-14186

Signed-off-by: ahmd-nabil <ahm3dnabil99@gmail.com>
  • Loading branch information
ahmd-nabil committed Dec 13, 2023
1 parent 63e726e commit 91e2203
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2023 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
*
* https://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.jackson2;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

/**
* A JsonDeserializer for {@link DefaultOAuth2User}.
*
* @author Ahmed Nabil
* @since 6.3
* @see DefaultOAuth2User
* @see DefaultOAuth2UserMixin
*/
public class DefaultOAuth2UserDeserializer extends JsonDeserializer<DefaultOAuth2User> {

@Override
public DefaultOAuth2User deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JacksonException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode defaultOAuth2UserNode = mapper.readTree(parser);
Collection<? extends GrantedAuthority> authorities = JsonNodeUtils.findValue(defaultOAuth2UserNode,
"authorities", JsonNodeUtils.GRANTED_AUTHORITY_COLLECTION, mapper);
Map<String, Object> attributes = JsonNodeUtils.findValue(defaultOAuth2UserNode, "attributes",
JsonNodeUtils.STRING_OBJECT_MAP, mapper);
String name = JsonNodeUtils.findStringValue(defaultOAuth2UserNode, "name");
if (name != null) {
return new DefaultOAuth2User(attributes, authorities, name);
}
String nameAttributeKey = JsonNodeUtils.findStringValue(defaultOAuth2UserNode, "nameAttributeKey");
return new DefaultOAuth2User(authorities, attributes, nameAttributeKey);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -24,6 +24,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
Expand All @@ -37,6 +38,7 @@
* @see OAuth2ClientJackson2Module
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonDeserialize(using = DefaultOAuth2UserDeserializer.class)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -48,4 +50,10 @@ abstract class DefaultOAuth2UserMixin {
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
}

@JsonCreator
DefaultOAuth2UserMixin(@JsonProperty("attributes") Map<String, Object> attributes,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("name") String name) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2002-2023 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
*
* https://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.jackson2;

import java.io.IOException;
import java.util.Collection;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;

/**
* A JsonDeserializer for {@link DefaultOidcUser}.
*
* @author Ahmed Nabil
* @since 6.3
* @see DefaultOidcUser
* @see DefaultOidcUserMixin
*/
public class DefaultOidcUserDeserializer extends JsonDeserializer<DefaultOidcUser> {

@Override
public DefaultOidcUser deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JacksonException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode defaultOidcUserNode = mapper.readTree(parser);
Collection<? extends GrantedAuthority> authorities = JsonNodeUtils.findValue(defaultOidcUserNode, "authorities",
JsonNodeUtils.GRANTED_AUTHORITY_COLLECTION, mapper);
OidcIdToken idToken = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "idToken", OidcIdToken.class, mapper);
OidcUserInfo userInfo = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "userInfo", OidcUserInfo.class,
mapper);
String nameAttributeKey = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "nameAttributeKey", String.class,
mapper);
String name = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "name", String.class, mapper);
return (name != null) ? new DefaultOidcUser(idToken, userInfo, authorities, name)
: new DefaultOidcUser(authorities, idToken, userInfo, nameAttributeKey);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -23,6 +23,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
Expand All @@ -38,6 +39,7 @@
* @see OAuth2ClientJackson2Module
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonDeserialize(using = DefaultOidcUserDeserializer.class)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true)
Expand All @@ -49,4 +51,10 @@ abstract class DefaultOidcUserMixin {
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
}

@JsonCreator
DefaultOidcUserMixin(@JsonProperty("idToken") OidcIdToken idToken, @JsonProperty("userInfo") OidcUserInfo userInfo,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("name") String name) {
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -16,13 +16,16 @@

package org.springframework.security.oauth2.client.jackson2;

import java.util.Collection;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.core.GrantedAuthority;

/**
* Utility class for {@code JsonNode}.
*
Expand All @@ -37,6 +40,9 @@ abstract class JsonNodeUtils {
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
};

static final TypeReference<Collection<? extends GrantedAuthority>> GRANTED_AUTHORITY_COLLECTION = new TypeReference<Collection<? extends GrantedAuthority>>() {
};

static String findStringValue(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
Expand All @@ -62,4 +68,12 @@ static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
return (value != null && value.isObject()) ? value : null;
}

static <T> T findValueByPath(JsonNode jsonNode, String path, Class<T> type, ObjectMapper mapper) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.path(path);
return (value != null && !value.isMissingNode()) ? mapper.convertValue(value, type) : null;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -16,12 +16,16 @@

package org.springframework.security.oauth2.client.userinfo;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.context.expression.MapAccessor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.GrantedAuthority;
Expand Down Expand Up @@ -76,6 +80,8 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq

private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();

private final SpelExpressionParser parser = new SpelExpressionParser();

private RestOperations restOperations;

public DefaultOAuth2UserService() {
Expand All @@ -87,35 +93,14 @@ public DefaultOAuth2UserService() {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
if (!StringUtils
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
+ userRequest.getClientRegistration().getRegistrationId(),
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
+ userRequest.getClientRegistration().getRegistrationId(),
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String userNameAttributeName = getUserNameAttributeName(userRequest);
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
Map<String, Object> userAttributes = response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
Map<String, Object> attributes = response.getBody();
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes);
String name = getName(attributes, userNameAttributeName);
return new DefaultOAuth2User(attributes, authorities, name);
}

private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
Expand Down Expand Up @@ -157,6 +142,48 @@ private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRe
}
}

private String getUserNameAttributeName(OAuth2UserRequest userRequest) {
if (!StringUtils
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
+ userRequest.getClientRegistration().getRegistrationId(),
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
+ userRequest.getClientRegistration().getRegistrationId(),
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return userNameAttributeName;
}

private Collection<GrantedAuthority> getAuthorities(OAuth2AccessToken token, Map<String, Object> attributes) {
Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(attributes));
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return authorities;
}

private String getName(Map<String, Object> attributes, String userNameAttributeName) {
Assert.notEmpty(attributes, "attributes cannot be empty");
Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty");
SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
.withRootObject(attributes)
.build();
Expression expression = this.parser.parseExpression(userNameAttributeName);
return expression.getValue(context, String.class);
}

/**
* Sets the {@link Converter} used for converting the {@link OAuth2UserRequest} to a
* {@link RequestEntity} representation of the UserInfo Request.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -36,7 +36,6 @@
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
Expand Down Expand Up @@ -194,7 +193,7 @@ private static String asJson(DefaultOAuth2User oauth2User) {
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
" \"username\": \"user\"\n" +
" },\n" +
" \"nameAttributeKey\": \"username\"\n" +
" \"name\": \"user\"\n" +
" }";
// @formatter:on
}
Expand All @@ -206,7 +205,7 @@ private static String asJson(DefaultOidcUser oidcUser) {
" \"authorities\": " + asJson(oidcUser.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" +
" \"idToken\": " + asJson(oidcUser.getIdToken()) + ",\n" +
" \"userInfo\": " + asJson(oidcUser.getUserInfo()) + ",\n" +
" \"nameAttributeKey\": \"" + IdTokenClaimNames.SUB + "\"\n" +
" \"name\": \"" + oidcUser.getName() + "\"\n" +
" }";
// @formatter:on
}
Expand Down

0 comments on commit 91e2203

Please sign in to comment.