Skip to content

Commit 65a7e1d

Browse files
committed
Added UserInfo.toJson method; added ScopeClaimTranslationService; rewrote UserInfoSerializer to use both
1 parent 952accc commit 65a7e1d

File tree

6 files changed

+120
-291
lines changed

6 files changed

+120
-291
lines changed

openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,50 @@ public String getBirthdate() {
400400
public void setBirthdate(String birthdate) {
401401
this.birthdate = birthdate;
402402
}
403+
404+
@Override
405+
public JsonObject toJson() {
406+
JsonObject obj = new JsonObject();
407+
408+
obj.addProperty("sub", this.getSub());
409+
410+
obj.addProperty("name", this.getName());
411+
obj.addProperty("preferred_username", this.getPreferredUsername());
412+
obj.addProperty("given_name", this.getGivenName());
413+
obj.addProperty("family_name", this.getFamilyName());
414+
obj.addProperty("middle_name", this.getMiddleName());
415+
obj.addProperty("nickname", this.getNickname());
416+
obj.addProperty("profile", this.getProfile());
417+
obj.addProperty("picture", this.getPicture());
418+
obj.addProperty("website", this.getWebsite());
419+
obj.addProperty("gender", this.getGender());
420+
obj.addProperty("zone_info", this.getZoneinfo());
421+
obj.addProperty("locale", this.getLocale());
422+
obj.addProperty("updated_time", this.getUpdatedTime());
423+
obj.addProperty("birthdate", this.getBirthdate());
424+
425+
obj.addProperty("email", this.getEmail());
426+
obj.addProperty("email_verified", this.getEmailVerified());
427+
428+
obj.addProperty("phone_number", this.getPhoneNumber());
429+
obj.addProperty("phone_number_verified", this.getPhoneNumberVerified());
430+
431+
if (this.getAddress() != null) {
403432

433+
JsonObject addr = new JsonObject();
434+
addr.addProperty("formatted", this.getAddress().getFormatted());
435+
addr.addProperty("street_address", this.getAddress().getStreetAddress());
436+
addr.addProperty("locality", this.getAddress().getLocality());
437+
addr.addProperty("region", this.getAddress().getRegion());
438+
addr.addProperty("postal_code", this.getAddress().getPostalCode());
439+
addr.addProperty("country", this.getAddress().getCountry());
440+
441+
obj.add("address", addr);
442+
}
443+
444+
return obj;
445+
}
446+
404447
/**
405448
* Parse a JsonObject into a UserInfo.
406449
* @param o

openid-connect-common/src/main/java/org/mitre/openid/connect/model/UserInfo.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
******************************************************************************/
1717
package org.mitre.openid.connect.model;
1818

19+
import com.google.gson.JsonObject;
20+
1921

2022
public interface UserInfo {
2123

@@ -222,4 +224,12 @@ public interface UserInfo {
222224
* @param birthdate
223225
*/
224226
public abstract void setBirthdate(String birthdate);
227+
228+
/**
229+
* Serialize this UserInfo object to JSON
230+
*
231+
* @return
232+
*/
233+
public abstract JsonObject toJson();
234+
225235
}

openid-connect-common/src/main/java/org/mitre/openid/connect/service/ScopeClaimTranslationService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import java.util.List;
44
import java.util.Map;
5+
import java.util.Set;
56

67
import com.google.common.collect.ArrayListMultimap;
8+
import com.google.common.collect.Lists;
79
import com.google.common.collect.Maps;
810

911
/**
@@ -77,6 +79,14 @@ public ScopeClaimTranslationService() {
7779
public List<String> getClaimsForScope(String scope) {
7880
return scopesToClaims.get(scope);
7981
}
82+
83+
public List<String> getClaimsForScopeSet(Set<String> scopes) {
84+
List<String> result = Lists.newArrayList();
85+
for (String scope : scopes) {
86+
result.addAll(getClaimsForScope(scope));
87+
}
88+
return result;
89+
}
8090

8191
public String getFieldNameForClaim(String claim) {
8292
return claimsToFields.get(claim);
Lines changed: 50 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,82 @@
11
package org.mitre.openid.connect.view;
22

3-
import java.lang.reflect.Field;
4-
import java.lang.reflect.InvocationTargetException;
5-
import java.lang.reflect.Method;
3+
import java.util.List;
64
import java.util.Map.Entry;
75
import java.util.Set;
86

9-
import org.mitre.openid.connect.model.DefaultUserInfo;
107
import org.mitre.openid.connect.model.UserInfo;
118
import org.mitre.openid.connect.service.ScopeClaimTranslationService;
12-
import org.slf4j.Logger;
13-
import org.slf4j.LoggerFactory;
14-
import org.springframework.util.ReflectionUtils;
159

16-
import com.google.common.base.CaseFormat;
17-
import com.google.common.collect.Sets;
1810
import com.google.gson.JsonElement;
1911
import com.google.gson.JsonObject;
2012

2113
public class UserInfoSerializer {
2214

23-
private static Logger logger = LoggerFactory.getLogger(UserInfoSerializer.class);
24-
2515
private static ScopeClaimTranslationService translator = new ScopeClaimTranslationService();
2616

2717
/**
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.
3220
*
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
3824
*/
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) {
5326

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();
5530

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));
6634
}
67-
6835
}
6936

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;
11438
}
11539

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) {
14753

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);
15158
}
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;
15669
}
15770

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+
}
16979
}
170-
171-
return obj;
80+
return result;
17281
}
173-
17482
}

openid-connect-common/src/main/java/org/mitre/openid/connect/web/UserInfoInterceptor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.mitre.openid.connect.model.OIDCAuthenticationToken;
2929
import org.mitre.openid.connect.model.UserInfo;
3030
import org.mitre.openid.connect.service.UserInfoService;
31-
import org.mitre.openid.connect.view.UserInfoSerializer;
3231
import org.springframework.beans.factory.annotation.Autowired;
3332
import org.springframework.security.core.Authentication;
3433
import org.springframework.security.core.GrantedAuthority;
@@ -84,7 +83,7 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response,
8483
// if they're logging into this server from a remote OIDC server, pass through their user info
8584
OIDCAuthenticationToken oidc = (OIDCAuthenticationToken) p;
8685
modelAndView.addObject("userInfo", oidc.getUserInfo());
87-
modelAndView.addObject("userInfoJson", UserInfoSerializer.toJson(oidc.getUserInfo(), null));
86+
modelAndView.addObject("userInfoJson", oidc.getUserInfo().toJson());
8887
} else {
8988
if (p != null && p.getName() != null) { // don't bother checking if we don't have a principal
9089

@@ -94,7 +93,7 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response,
9493
// if we have one, inject it so views can use it
9594
if (user != null) {
9695
modelAndView.addObject("userInfo", user);
97-
modelAndView.addObject("userInfoJson", UserInfoSerializer.toJson(user, null));
96+
modelAndView.addObject("userInfoJson", user.toJson());
9897
}
9998
}
10099
}

0 commit comments

Comments
 (0)