|
1 | 1 | package org.mitre.openid.connect.view;
|
2 | 2 |
|
3 |
| -import java.lang.reflect.Field; |
4 |
| -import java.lang.reflect.InvocationTargetException; |
5 |
| -import java.lang.reflect.Method; |
| 3 | +import java.util.List; |
6 | 4 | import java.util.Map.Entry;
|
7 | 5 | import java.util.Set;
|
8 | 6 |
|
9 |
| -import org.mitre.openid.connect.model.DefaultUserInfo; |
10 | 7 | import org.mitre.openid.connect.model.UserInfo;
|
11 | 8 | import org.mitre.openid.connect.service.ScopeClaimTranslationService;
|
12 |
| -import org.slf4j.Logger; |
13 |
| -import org.slf4j.LoggerFactory; |
14 |
| -import org.springframework.util.ReflectionUtils; |
15 | 9 |
|
16 |
| -import com.google.common.base.CaseFormat; |
17 |
| -import com.google.common.collect.Sets; |
18 | 10 | import com.google.gson.JsonElement;
|
19 | 11 | import com.google.gson.JsonObject;
|
20 | 12 |
|
21 | 13 | public class UserInfoSerializer {
|
22 | 14 |
|
23 |
| -private static Logger logger = LoggerFactory.getLogger(UserInfoSerializer.class); |
24 |
| - |
25 | 15 | private static ScopeClaimTranslationService translator = new ScopeClaimTranslationService();
|
26 | 16 |
|
27 | 17 | /**
|
28 |
| - * Build a JSON response according to the request object received. |
29 |
| - * |
30 |
| - * Claims requested in requestObj.userinfo.claims are added to any |
31 |
| - * claims corresponding to requested scopes, if any. |
| 18 | + * Filter the UserInfo object by scope, using our ScopeClaimTranslationService to determine |
| 19 | + * which claims are allowed for each given scope. |
32 | 20 | *
|
33 |
| - * @param ui |
34 |
| - * @param scope |
35 |
| - * @param requestObj |
36 |
| - * @param claimsRequest the claims request parameter object. |
37 |
| - * @return |
| 21 | + * @param ui the UserInfo to filter |
| 22 | + * @param scope the allowed scopes to filter by |
| 23 | + * @return the filtered JsonObject result |
38 | 24 | */
|
39 |
| -public static JsonObject toJsonFromRequestObj(UserInfo ui, Set<String> scope, JsonObject requestObj, JsonObject claimsRequest) { |
40 |
| - |
41 |
| -JsonObject obj = toJson(ui, scope); |
42 |
| - |
43 |
| -//Process list of requested claims out of the request object |
44 |
| -JsonElement claims = requestObj.get("claims"); |
45 |
| -if (claims == null || !claims.isJsonObject()) { |
46 |
| -return obj; |
47 |
| -} |
48 |
| - |
49 |
| -JsonElement userinfo = claims.getAsJsonObject().get("userinfo"); |
50 |
| -if (userinfo == null || !userinfo.isJsonObject()) { |
51 |
| -return obj; |
52 |
| -} |
| 25 | +public static JsonObject filterByScope(UserInfo ui, Set<String> scope) { |
53 | 26 |
|
54 |
| -// Filter claims from the request object with the claims from the claims request parameter, if it exists |
| 27 | +JsonObject uiJson = ui.toJson(); |
| 28 | +List<String> filteredClaims = translator.getClaimsForScopeSet(scope); |
| 29 | +JsonObject result = new JsonObject(); |
55 | 30 |
|
56 |
| -// Doing the set intersection manually because the claim entries may be referring to |
57 |
| -// the same claim but have different 'individual claim values', causing the Entry<> to be unequal, |
58 |
| -// which doesn't allow the use of the more compact Sets.intersection() type method. |
59 |
| -Set<Entry<String, JsonElement>> requestClaimsSet = Sets.newHashSet(); |
60 |
| -if (claimsRequest != null) { |
61 |
| - |
62 |
| -for (Entry<String, JsonElement> entry : userinfo.getAsJsonObject().entrySet()) { |
63 |
| -if (claimsRequest.has(entry.getKey())) { |
64 |
| -requestClaimsSet.add(entry); |
65 |
| -} |
| 31 | +for (String claim : filteredClaims) { |
| 32 | +if (uiJson.has(claim)) { |
| 33 | +result.add(claim, uiJson.get(claim)); |
66 | 34 | }
|
67 |
| - |
68 | 35 | }
|
69 | 36 |
|
70 |
| -//TODO: is there a way to use bean processors to do bean.getfield(name)? |
71 |
| -//Method reflection is OK, but need a service to translate scopes into claim names => field names |
72 |
| - |
73 |
| - |
74 |
| - |
75 |
| -// TODO: this method is likely to be fragile if the data model changes at all |
76 |
| - |
77 |
| -//For each claim found, add it if not already present |
78 |
| -for (Entry<String, JsonElement> i : requestClaimsSet) { |
79 |
| -String claimName = i.getKey(); |
80 |
| -if (!obj.has(claimName)) { |
81 |
| -String value = ""; |
82 |
| - |
83 |
| -String fieldName = translator.getFieldNameForClaim(claimName); |
84 |
| -Field field = ReflectionUtils.findField(DefaultUserInfo.class, fieldName); |
85 |
| - |
86 |
| -Object val = ReflectionUtils.getField(field, userinfo); |
87 |
| - |
88 |
| -//TODO:how to convert val to a String? Most claims can be converted directly; address is compound |
89 |
| - |
90 |
| - |
91 |
| -//Process claim names to go from "claim_name" to "ClaimName" |
92 |
| -String camelClaimName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, claimName); |
93 |
| -//Now we have "getClaimName" |
94 |
| -String methodName = "get" + camelClaimName; |
95 |
| -Method getter = null; |
96 |
| -try { |
97 |
| -getter = ui.getClass().getMethod(methodName); |
98 |
| -value = (String) getter.invoke(ui); |
99 |
| -obj.addProperty(claimName, value); |
100 |
| -} catch (SecurityException e) { |
101 |
| -logger.error("SecurityException in UserInfoView.java: ", e); |
102 |
| -} catch (NoSuchMethodException e) { |
103 |
| -logger.error("NoSuchMethodException in UserInfoView.java: ", e); |
104 |
| -} catch (IllegalArgumentException e) { |
105 |
| -logger.error("IllegalArgumentException in UserInfoView.java: ", e); |
106 |
| -} catch (IllegalAccessException e) { |
107 |
| -logger.error("IllegalAccessException in UserInfoView.java: ", e); |
108 |
| -} catch (InvocationTargetException e) { |
109 |
| -logger.error("InvocationTargetException in UserInfoView.java: ", e); |
110 |
| -} |
111 |
| -} |
112 |
| -} |
113 |
| -return obj; |
| 37 | +return result; |
114 | 38 | }
|
115 | 39 |
|
116 |
| -public static JsonObject toJson(UserInfo ui, Set<String> scope) { |
117 |
| - |
118 |
| -JsonObject obj = new JsonObject(); |
119 |
| - |
120 |
| -//TODO: This is a hack: the UserInfoInterceptor should use a serializer from this class, but it doesn't |
121 |
| -//have access to a scope set. It wants to just serialize whatever fields are present? |
122 |
| -if (scope == null) { |
123 |
| -Set<String> allScopes = Sets.newHashSet("openid", "profile", "email", "phone", "address"); |
124 |
| -scope = allScopes; |
125 |
| -} |
126 |
| - |
127 |
| -if (scope.contains("openid")) { |
128 |
| -obj.addProperty("sub", ui.getSub()); |
129 |
| -} |
130 |
| - |
131 |
| -if (scope.contains("profile")) { |
132 |
| -obj.addProperty("name", ui.getName()); |
133 |
| -obj.addProperty("preferred_username", ui.getPreferredUsername()); |
134 |
| -obj.addProperty("given_name", ui.getGivenName()); |
135 |
| -obj.addProperty("family_name", ui.getFamilyName()); |
136 |
| -obj.addProperty("middle_name", ui.getMiddleName()); |
137 |
| -obj.addProperty("nickname", ui.getNickname()); |
138 |
| -obj.addProperty("profile", ui.getProfile()); |
139 |
| -obj.addProperty("picture", ui.getPicture()); |
140 |
| -obj.addProperty("website", ui.getWebsite()); |
141 |
| -obj.addProperty("gender", ui.getGender()); |
142 |
| -obj.addProperty("zone_info", ui.getZoneinfo()); |
143 |
| -obj.addProperty("locale", ui.getLocale()); |
144 |
| -obj.addProperty("updated_time", ui.getUpdatedTime()); |
145 |
| -obj.addProperty("birthdate", ui.getBirthdate()); |
146 |
| -} |
| 40 | +/** |
| 41 | + * Build a JSON response according to the request object received. |
| 42 | + * |
| 43 | + * Claims requested in requestObj.userinfo.claims are added to any |
| 44 | + * claims corresponding to requested scopes, if any. |
| 45 | + * |
| 46 | + * @param ui the UserInfo to filter |
| 47 | + * @param scope the allowed scopes to filter by |
| 48 | + * @param authorizedClaims the claims authorized by the client or user |
| 49 | + * @param requestedClaims the claims requested in the RequestObject |
| 50 | + * @return the filtered JsonObject result |
| 51 | + */ |
| 52 | +public static JsonObject toJsonFromRequestObj(UserInfo ui, Set<String> scope, JsonObject authorizedClaims, JsonObject requestedClaims) { |
147 | 53 |
|
148 |
| -if (scope.contains("email")) { |
149 |
| -obj.addProperty("email", ui.getEmail()); |
150 |
| -obj.addProperty("email_verified", ui.getEmailVerified()); |
| 54 | +// Only proceed if we have both requested claims and authorized claims list. Otherwise just return |
| 55 | +// the scope-filtered claim set. |
| 56 | +if (requestedClaims == null || authorizedClaims == null) { |
| 57 | +return filterByScope(ui, scope); |
151 | 58 | }
|
152 |
| - |
153 |
| -if (scope.contains("phone")) { |
154 |
| -obj.addProperty("phone_number", ui.getPhoneNumber()); |
155 |
| -obj.addProperty("phone_number_verified", ui.getPhoneNumberVerified()); |
| 59 | + |
| 60 | +// get the base object |
| 61 | +JsonObject obj = ui.toJson(); |
| 62 | + |
| 63 | +List<String> allowedByScope = translator.getClaimsForScopeSet(scope); |
| 64 | +JsonObject userinfoAuthorized = authorizedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); |
| 65 | +JsonObject userinfoRequested = requestedClaims.getAsJsonObject().get("userinfo").getAsJsonObject(); |
| 66 | + |
| 67 | +if (userinfoAuthorized == null || !userinfoAuthorized.isJsonObject()) { |
| 68 | +return obj; |
156 | 69 | }
|
157 | 70 |
|
158 |
| -if (scope.contains("address") && ui.getAddress() != null) { |
159 |
| - |
160 |
| -JsonObject addr = new JsonObject(); |
161 |
| -addr.addProperty("formatted", ui.getAddress().getFormatted()); |
162 |
| -addr.addProperty("street_address", ui.getAddress().getStreetAddress()); |
163 |
| -addr.addProperty("locality", ui.getAddress().getLocality()); |
164 |
| -addr.addProperty("region", ui.getAddress().getRegion()); |
165 |
| -addr.addProperty("postal_code", ui.getAddress().getPostalCode()); |
166 |
| -addr.addProperty("country", ui.getAddress().getCountry()); |
167 |
| - |
168 |
| -obj.add("address", addr); |
| 71 | +// Filter claims by performing a manual intersection of claims that are allowed by the given scope, requested, and authorized. |
| 72 | +// We cannot use Sets.intersection() or similar because Entry<> objects will evaluate to being unequal if their values are |
| 73 | +// different, whereas we are only interested in matching the Entry<>'s key values. |
| 74 | +JsonObject result = new JsonObject(); |
| 75 | +for (Entry<String, JsonElement> entry : userinfoAuthorized.getAsJsonObject().entrySet()) { |
| 76 | +if (userinfoRequested.has(entry.getKey()) && allowedByScope.contains(entry.getKey())) { |
| 77 | +result.add(entry.getKey(), entry.getValue()); |
| 78 | +} |
169 | 79 | }
|
170 |
| - |
171 |
| -return obj; |
| 80 | +return result; |
172 | 81 | }
|
173 |
| - |
174 | 82 | }
|
0 commit comments