From 65a7e1d724d91411a3c3699e1143ae2788edb152 Mon Sep 17 00:00:00 2001 From: Amanda Anganes Date: Thu, 26 Sep 2013 11:56:57 -0400 Subject: [PATCH] Added UserInfo.toJson method; added ScopeClaimTranslationService; rewrote UserInfoSerializer to use both --- .../openid/connect/model/DefaultUserInfo.java | 43 ++++ .../mitre/openid/connect/model/UserInfo.java | 10 + .../service/ScopeClaimTranslationService.java | 10 + .../connect/view/UserInfoSerializer.java | 196 +++++------------- .../connect/web/UserInfoInterceptor.java | 5 +- .../openid/connect/view/UserInfoView.java | 151 +------------- 6 files changed, 122 insertions(+), 293 deletions(-) diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java index 0c5aa5db4..a07e299b3 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java @@ -400,7 +400,50 @@ public class DefaultUserInfo implements UserInfo { public void setBirthdate(String birthdate) { this.birthdate = birthdate; } + + @Override + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("sub", this.getSub()); + + obj.addProperty("name", this.getName()); + obj.addProperty("preferred_username", this.getPreferredUsername()); + obj.addProperty("given_name", this.getGivenName()); + obj.addProperty("family_name", this.getFamilyName()); + obj.addProperty("middle_name", this.getMiddleName()); + obj.addProperty("nickname", this.getNickname()); + obj.addProperty("profile", this.getProfile()); + obj.addProperty("picture", this.getPicture()); + obj.addProperty("website", this.getWebsite()); + obj.addProperty("gender", this.getGender()); + obj.addProperty("zone_info", this.getZoneinfo()); + obj.addProperty("locale", this.getLocale()); + obj.addProperty("updated_time", this.getUpdatedTime()); + obj.addProperty("birthdate", this.getBirthdate()); + + obj.addProperty("email", this.getEmail()); + obj.addProperty("email_verified", this.getEmailVerified()); + + obj.addProperty("phone_number", this.getPhoneNumber()); + obj.addProperty("phone_number_verified", this.getPhoneNumberVerified()); + + if (this.getAddress() != null) { + + JsonObject addr = new JsonObject(); + addr.addProperty("formatted", this.getAddress().getFormatted()); + addr.addProperty("street_address", this.getAddress().getStreetAddress()); + addr.addProperty("locality", this.getAddress().getLocality()); + addr.addProperty("region", this.getAddress().getRegion()); + addr.addProperty("postal_code", this.getAddress().getPostalCode()); + addr.addProperty("country", this.getAddress().getCountry()); + + obj.add("address", addr); + } + + return obj; + } + /** * Parse a JsonObject into a UserInfo. * @param o diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java index 341a4f90d..0e97dcc94 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java @@ -16,6 +16,8 @@ ******************************************************************************/ package org.mitre.openid.connect.model; +import com.google.gson.JsonObject; + public interface UserInfo { @@ -222,4 +224,12 @@ public interface UserInfo { * @param birthdate */ public abstract void setBirthdate(String birthdate); + + /** + * Serialize this UserInfo object to JSON + * + * @return + */ + public abstract JsonObject toJson(); + } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java index 18ff37542..e33473525 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java @@ -2,8 +2,10 @@ package org.mitre.openid.connect.service; import java.util.List; import java.util.Map; +import java.util.Set; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** @@ -77,6 +79,14 @@ public class ScopeClaimTranslationService { public List getClaimsForScope(String scope) { return scopesToClaims.get(scope); } + + public List getClaimsForScopeSet(Set scopes) { + List result = Lists.newArrayList(); + for (String scope : scopes) { + result.addAll(getClaimsForScope(scope)); + } + return result; + } public String getFieldNameForClaim(String claim) { return claimsToFields.get(claim); diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/view/UserInfoSerializer.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/view/UserInfoSerializer.java index b16b8929a..19401a82c 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/view/UserInfoSerializer.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/view/UserInfoSerializer.java @@ -1,174 +1,82 @@ package org.mitre.openid.connect.view; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.List; import java.util.Map.Entry; import java.util.Set; -import org.mitre.openid.connect.model.DefaultUserInfo; import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.ScopeClaimTranslationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.ReflectionUtils; -import com.google.common.base.CaseFormat; -import com.google.common.collect.Sets; import com.google.gson.JsonElement; import com.google.gson.JsonObject; public class UserInfoSerializer { - private static Logger logger = LoggerFactory.getLogger(UserInfoSerializer.class); - private static ScopeClaimTranslationService translator = new ScopeClaimTranslationService(); + /** + * Filter the UserInfo object by scope, using our ScopeClaimTranslationService to determine + * which claims are allowed for each given scope. + * + * @param ui the UserInfo to filter + * @param scope the allowed scopes to filter by + * @return the filtered JsonObject result + */ + public static JsonObject filterByScope(UserInfo ui, Set scope) { + + JsonObject uiJson = ui.toJson(); + List filteredClaims = translator.getClaimsForScopeSet(scope); + JsonObject result = new JsonObject(); + + for (String claim : filteredClaims) { + if (uiJson.has(claim)) { + result.add(claim, uiJson.get(claim)); + } + } + + return result; + } + /** * Build a JSON response according to the request object received. * * Claims requested in requestObj.userinfo.claims are added to any * claims corresponding to requested scopes, if any. * - * @param ui - * @param scope - * @param requestObj - * @param claimsRequest the claims request parameter object. - * @return + * @param ui the UserInfo to filter + * @param scope the allowed scopes to filter by + * @param authorizedClaims the claims authorized by the client or user + * @param requestedClaims the claims requested in the RequestObject + * @return the filtered JsonObject result */ - public static JsonObject toJsonFromRequestObj(UserInfo ui, Set scope, JsonObject requestObj, JsonObject claimsRequest) { + public static JsonObject toJsonFromRequestObj(UserInfo ui, Set scope, JsonObject authorizedClaims, JsonObject requestedClaims) { - JsonObject obj = toJson(ui, scope); - - //Process list of requested claims out of the request object - JsonElement claims = requestObj.get("claims"); - if (claims == null || !claims.isJsonObject()) { + // Only proceed if we have both requested claims and authorized claims list. Otherwise just return + // the scope-filtered claim set. + if (requestedClaims == null || authorizedClaims == null) { + return filterByScope(ui, scope); + } + + // get the base object + JsonObject obj = ui.toJson(); + + List allowedByScope = translator.getClaimsForScopeSet(scope); + JsonObject userinfoAuthorized = authorizedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); + JsonObject userinfoRequested = requestedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); + + if (userinfoAuthorized == null || !userinfoAuthorized.isJsonObject()) { return obj; } - JsonElement userinfo = claims.getAsJsonObject().get("userinfo"); - if (userinfo == null || !userinfo.isJsonObject()) { - return obj; - } - - // Filter claims from the request object with the claims from the claims request parameter, if it exists - - // Doing the set intersection manually because the claim entries may be referring to - // the same claim but have different 'individual claim values', causing the Entry<> to be unequal, - // which doesn't allow the use of the more compact Sets.intersection() type method. - Set> requestClaimsSet = Sets.newHashSet(); - if (claimsRequest != null) { - - for (Entry entry : userinfo.getAsJsonObject().entrySet()) { - if (claimsRequest.has(entry.getKey())) { - requestClaimsSet.add(entry); - } - } - - } - - //TODO: is there a way to use bean processors to do bean.getfield(name)? - //Method reflection is OK, but need a service to translate scopes into claim names => field names - - - - // TODO: this method is likely to be fragile if the data model changes at all - - //For each claim found, add it if not already present - for (Entry i : requestClaimsSet) { - String claimName = i.getKey(); - if (!obj.has(claimName)) { - String value = ""; - - String fieldName = translator.getFieldNameForClaim(claimName); - Field field = ReflectionUtils.findField(DefaultUserInfo.class, fieldName); - - Object val = ReflectionUtils.getField(field, userinfo); - - //TODO:how to convert val to a String? Most claims can be converted directly; address is compound - - - //Process claim names to go from "claim_name" to "ClaimName" - String camelClaimName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, claimName); - //Now we have "getClaimName" - String methodName = "get" + camelClaimName; - Method getter = null; - try { - getter = ui.getClass().getMethod(methodName); - value = (String) getter.invoke(ui); - obj.addProperty(claimName, value); - } catch (SecurityException e) { - logger.error("SecurityException in UserInfoView.java: ", e); - } catch (NoSuchMethodException e) { - logger.error("NoSuchMethodException in UserInfoView.java: ", e); - } catch (IllegalArgumentException e) { - logger.error("IllegalArgumentException in UserInfoView.java: ", e); - } catch (IllegalAccessException e) { - logger.error("IllegalAccessException in UserInfoView.java: ", e); - } catch (InvocationTargetException e) { - logger.error("InvocationTargetException in UserInfoView.java: ", e); - } + // Filter claims by performing a manual intersection of claims that are allowed by the given scope, requested, and authorized. + // We cannot use Sets.intersection() or similar because Entry<> objects will evaluate to being unequal if their values are + // different, whereas we are only interested in matching the Entry<>'s key values. + JsonObject result = new JsonObject(); + for (Entry entry : userinfoAuthorized.getAsJsonObject().entrySet()) { + if (userinfoRequested.has(entry.getKey()) && allowedByScope.contains(entry.getKey())) { + result.add(entry.getKey(), entry.getValue()); } } - return obj; + return result; } - - public static JsonObject toJson(UserInfo ui, Set scope) { - - JsonObject obj = new JsonObject(); - - //TODO: This is a hack: the UserInfoInterceptor should use a serializer from this class, but it doesn't - //have access to a scope set. It wants to just serialize whatever fields are present? - if (scope == null) { - Set allScopes = Sets.newHashSet("openid", "profile", "email", "phone", "address"); - scope = allScopes; - } - - if (scope.contains("openid")) { - obj.addProperty("sub", ui.getSub()); - } - - if (scope.contains("profile")) { - obj.addProperty("name", ui.getName()); - obj.addProperty("preferred_username", ui.getPreferredUsername()); - obj.addProperty("given_name", ui.getGivenName()); - obj.addProperty("family_name", ui.getFamilyName()); - obj.addProperty("middle_name", ui.getMiddleName()); - obj.addProperty("nickname", ui.getNickname()); - obj.addProperty("profile", ui.getProfile()); - obj.addProperty("picture", ui.getPicture()); - obj.addProperty("website", ui.getWebsite()); - obj.addProperty("gender", ui.getGender()); - obj.addProperty("zone_info", ui.getZoneinfo()); - obj.addProperty("locale", ui.getLocale()); - obj.addProperty("updated_time", ui.getUpdatedTime()); - obj.addProperty("birthdate", ui.getBirthdate()); - } - - if (scope.contains("email")) { - obj.addProperty("email", ui.getEmail()); - obj.addProperty("email_verified", ui.getEmailVerified()); - } - - if (scope.contains("phone")) { - obj.addProperty("phone_number", ui.getPhoneNumber()); - obj.addProperty("phone_number_verified", ui.getPhoneNumberVerified()); - } - - if (scope.contains("address") && ui.getAddress() != null) { - - JsonObject addr = new JsonObject(); - addr.addProperty("formatted", ui.getAddress().getFormatted()); - addr.addProperty("street_address", ui.getAddress().getStreetAddress()); - addr.addProperty("locality", ui.getAddress().getLocality()); - addr.addProperty("region", ui.getAddress().getRegion()); - addr.addProperty("postal_code", ui.getAddress().getPostalCode()); - addr.addProperty("country", ui.getAddress().getCountry()); - - obj.add("address", addr); - } - - return obj; - } - } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java index 1cc885516..70492bc71 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java @@ -28,7 +28,6 @@ import javax.servlet.http.HttpServletResponse; import org.mitre.openid.connect.model.OIDCAuthenticationToken; import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.UserInfoService; -import org.mitre.openid.connect.view.UserInfoSerializer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -84,7 +83,7 @@ public class UserInfoInterceptor extends HandlerInterceptorAdapter { // if they're logging into this server from a remote OIDC server, pass through their user info OIDCAuthenticationToken oidc = (OIDCAuthenticationToken) p; modelAndView.addObject("userInfo", oidc.getUserInfo()); - modelAndView.addObject("userInfoJson", UserInfoSerializer.toJson(oidc.getUserInfo(), null)); + modelAndView.addObject("userInfoJson", oidc.getUserInfo().toJson()); } else { if (p != null && p.getName() != null) { // don't bother checking if we don't have a principal @@ -94,7 +93,7 @@ public class UserInfoInterceptor extends HandlerInterceptorAdapter { // if we have one, inject it so views can use it if (user != null) { modelAndView.addObject("userInfo", user); - modelAndView.addObject("userInfoJson", UserInfoSerializer.toJson(user, null)); + modelAndView.addObject("userInfoJson", user.toJson()); } } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java index 47f7b0ae9..a05a2a9b6 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoView.java @@ -18,18 +18,15 @@ package org.mitre.openid.connect.view; import java.io.IOException; import java.io.Writer; -import java.text.ParseException; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.mitre.jwt.encryption.service.JwtEncryptionAndDecryptionService; import org.mitre.openid.connect.model.UserInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.web.servlet.view.AbstractView; @@ -40,13 +37,9 @@ import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; -import com.google.gson.JsonIOException; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import com.nimbusds.jwt.EncryptedJWT; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; + @Component("userInfoView") public class UserInfoView extends AbstractView { @@ -58,13 +51,13 @@ public class UserInfoView extends AbstractView { private Gson gson = new GsonBuilder() .setExclusionStrategies(new ExclusionStrategy() { - @Override + //@Override public boolean shouldSkipField(FieldAttributes f) { return false; } - @Override + //@Override public boolean shouldSkipClass(Class clazz) { // skip the JPA binding wrapper if (clazz.equals(BeanPropertyBindingResult.class)) { @@ -116,12 +109,9 @@ public class UserInfoView extends AbstractView { requestedClaims = jsonParser.parse((String) model.get("requestedClaims")).getAsJsonObject(); } if (authorizedClaims != null || requestedClaims != null) { - - gson.toJson(toJsonFromRequestObj(userInfo, scope, authorizedClaims, requestedClaims), out); + gson.toJson(UserInfoSerializer.toJsonFromRequestObj(userInfo, scope, authorizedClaims, requestedClaims), out); } else { - - gson.toJson(UserInfoSerializer.toJson(userInfo, scope), out); - + gson.toJson(UserInfoSerializer.filterByScope(userInfo, scope), out); } } catch (IOException e) { @@ -132,135 +122,4 @@ public class UserInfoView extends AbstractView { } - private JsonObject toJson(UserInfo ui, Set scope) { - - JsonObject obj = new JsonObject(); - - if (scope.contains("openid")) { - obj.addProperty("sub", ui.getSub()); - } - - if (scope.contains("profile")) { - obj.addProperty("name", ui.getName()); - obj.addProperty("preferred_username", ui.getPreferredUsername()); - obj.addProperty("given_name", ui.getGivenName()); - obj.addProperty("family_name", ui.getFamilyName()); - obj.addProperty("middle_name", ui.getMiddleName()); - obj.addProperty("nickname", ui.getNickname()); - obj.addProperty("profile", ui.getProfile()); - obj.addProperty("picture", ui.getPicture()); - obj.addProperty("website", ui.getWebsite()); - obj.addProperty("gender", ui.getGender()); - obj.addProperty("zone_info", ui.getZoneinfo()); - obj.addProperty("locale", ui.getLocale()); - obj.addProperty("updated_time", ui.getUpdatedTime()); - obj.addProperty("birthdate", ui.getBirthdate()); - } - - if (scope.contains("email")) { - obj.addProperty("email", ui.getEmail()); - obj.addProperty("email_verified", ui.getEmailVerified()); - } - - if (scope.contains("phone")) { - obj.addProperty("phone_number", ui.getPhoneNumber()); - obj.addProperty("phone_number_verified", ui.getPhoneNumberVerified()); - } - - if (scope.contains("address") && ui.getAddress() != null) { - - JsonObject addr = new JsonObject(); - addr.addProperty("formatted", ui.getAddress().getFormatted()); - addr.addProperty("street_address", ui.getAddress().getStreetAddress()); - addr.addProperty("locality", ui.getAddress().getLocality()); - addr.addProperty("region", ui.getAddress().getRegion()); - addr.addProperty("postal_code", ui.getAddress().getPostalCode()); - addr.addProperty("country", ui.getAddress().getCountry()); - - obj.add("address", addr); - } - - - return obj; - } - - /** - * Build a JSON response according to the request object received. - * - * Claims requested in requestObj.userinfo.claims are added to any - * claims corresponding to requested scopes, if any. - * - * @param ui - * @param scope - * @param authorizedClaims - * @param requestedClaims the claims request parameter object. - * @return - */ - private JsonObject toJsonFromRequestObj(UserInfo ui, Set scope, JsonObject authorizedClaims, JsonObject requestedClaims) { - - // get the base object - JsonObject obj = toJson(ui, scope); - - JsonObject userinfoAuthorized = authorizedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); - JsonObject userinfoRequested = requestedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); - - if (userinfoAuthorized == null || !userinfoAuthorized.isJsonObject()) { - return obj; - } - - - // Filter claims from the request object with the claims from the claims request parameter, if it exists - - // Doing the set intersection manually because the claim entries may be referring to - // the same claim but have different 'individual claim values', causing the Entry<> to be unequal, - // which doesn't allow the use of the more compact Sets.intersection() type method. - Set> requestClaimsSet = Sets.newHashSet(); - if (requestedClaims != null) { - - for (Entry entry : userinfoAuthorized.getAsJsonObject().entrySet()) { - if (userinfoRequested.has(entry.getKey())) { - requestClaimsSet.add(entry); - } - } - - } - - // TODO: this method is likely to be fragile if the data model changes at all - - //For each claim found, add it if not already present - for (Entry i : requestClaimsSet) { - String claimName = i.getKey(); - if (!obj.has(claimName)) { - String value = ""; - - - //Process claim names to go from "claim_name" to "ClaimName" - String camelClaimName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, claimName); - //Now we have "getClaimName" - String methodName = "get" + camelClaimName; - Method getter = null; - try { - getter = ui.getClass().getMethod(methodName); - value = (String) getter.invoke(ui); - obj.addProperty(claimName, value); - } catch (SecurityException e) { - logger.error("SecurityException in UserInfoView.java: ", e); - } catch (NoSuchMethodException e) { - logger.error("NoSuchMethodException in UserInfoView.java: ", e); - } catch (IllegalArgumentException e) { - logger.error("IllegalArgumentException in UserInfoView.java: ", e); - } catch (IllegalAccessException e) { - logger.error("IllegalAccessException in UserInfoView.java: ", e); - } catch (InvocationTargetException e) { - logger.error("InvocationTargetException in UserInfoView.java: ", e); - } - } - } - - - - return obj; - - } - }