refactored userinfo serializer

pull/523/head
Justin Richer 2013-09-27 13:57:15 -04:00
parent 1a82060cf5
commit 190caee9a1
4 changed files with 189 additions and 162 deletions

View File

@ -1,99 +1,20 @@
/**
*
*/
package org.mitre.openid.connect.service;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
/**
* Service to map scopes to claims, and claims to Java field names
*
* @author Amanda Anganes
* @author jricher
*
*/
public class ScopeClaimTranslationService {
public interface ScopeClaimTranslationService {
private SetMultimap<String, String> scopesToClaims = HashMultimap.create();
private Map<String, String> claimsToFields = Maps.newHashMap();
/**
* Default constructor; initializes scopesToClaims map
*/
public ScopeClaimTranslationService() {
scopesToClaims.put("openid", "sub");
scopesToClaims.put("profile", "name");
scopesToClaims.put("profile", "preferred_username");
scopesToClaims.put("profile", "given_name");
scopesToClaims.put("profile", "family_name");
scopesToClaims.put("profile", "middle_name");
scopesToClaims.put("profile", "nickname");
scopesToClaims.put("profile", "profile");
scopesToClaims.put("profile", "picture");
scopesToClaims.put("profile", "website");
scopesToClaims.put("profile", "gender");
scopesToClaims.put("profile", "zone_info");
scopesToClaims.put("profile", "locale");
scopesToClaims.put("profile", "updated_time");
scopesToClaims.put("profile", "birthdate");
scopesToClaims.put("email", "email");
scopesToClaims.put("email", "email_verified");
scopesToClaims.put("phone", "phone_number");
scopesToClaims.put("phone", "phone_number_verified");
scopesToClaims.put("address", "address");
claimsToFields.put("sub", "sub");
claimsToFields.put("name", "name");
claimsToFields.put("preferred_username", "preferredUsername");
claimsToFields.put("given_name", "givenName");
claimsToFields.put("family_name", "familyName");
claimsToFields.put("middle_name", "middleName");
claimsToFields.put("nickname", "nickname");
claimsToFields.put("profile", "profile");
claimsToFields.put("picture", "picture");
claimsToFields.put("website", "website");
claimsToFields.put("gender", "gender");
claimsToFields.put("zone_info", "zoneinfo");
claimsToFields.put("locale", "locale");
claimsToFields.put("updated_time", "updatedTime");
claimsToFields.put("birthdate", "birthdate");
claimsToFields.put("email", "email");
claimsToFields.put("email_verified", "emailVerified");
claimsToFields.put("phone_number", "phoneNumber");
claimsToFields.put("phone_number_verified", "phoneNumberVerified");
claimsToFields.put("address", "address");
}
public Set<String> getClaimsForScope(String scope) {
if (scopesToClaims.containsKey(scope)) {
return scopesToClaims.get(scope);
} else {
return new HashSet<String>();
}
}
public Set<String> getClaimsForScopeSet(Set<String> scopes) {
Set<String> result = new HashSet<String>();
for (String scope : scopes) {
result.addAll(getClaimsForScope(scope));
}
return result;
}
public String getFieldNameForClaim(String claim) {
return claimsToFields.get(claim);
}
}
public Set<String> getClaimsForScope(String scope);
public Set<String> getClaimsForScopeSet(Set<String> scopes);
public String getFieldNameForClaim(String claim);
}

View File

@ -1,70 +0,0 @@
package org.mitre.openid.connect.view;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import org.mitre.openid.connect.model.UserInfo;
import org.mitre.openid.connect.service.ScopeClaimTranslationService;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
public class UserInfoSerializer {
private static ScopeClaimTranslationService translator = new ScopeClaimTranslationService();
/**
* 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 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<String> scope, JsonObject authorizedClaims, JsonObject requestedClaims) {
// get the base object
JsonObject obj = ui.toJson();
Set<String> allowedByScope = translator.getClaimsForScopeSet(scope);
Set<String> authorizedByClaims = new HashSet<String>();
Set<String> requestedByClaims = new HashSet<String>();
if (authorizedClaims != null) {
JsonObject userinfoAuthorized = authorizedClaims.getAsJsonObject().get("userinfo").getAsJsonObject();
for (Entry<String, JsonElement> entry : userinfoAuthorized.getAsJsonObject().entrySet()) {
authorizedByClaims.add(entry.getKey());
}
}
if (requestedClaims != null) {
JsonObject userinfoRequested = requestedClaims.getAsJsonObject().get("userinfo").getAsJsonObject();
for (Entry<String, JsonElement> entry : userinfoRequested.getAsJsonObject().entrySet()) {
requestedByClaims.add(entry.getKey());
}
}
// 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<String, JsonElement> entry : obj.entrySet()) {
if (allowedByScope.contains(entry.getKey())
|| authorizedByClaims.contains(entry.getKey())) {
// it's allowed either by scope or by the authorized claims (either way is fine with us)
if (requestedByClaims.isEmpty() || requestedByClaims.contains(entry.getKey())) {
// the requested claims are empty (so we allow all), or they're not empty and this claim was specifically asked for
result.add(entry.getKey(), entry.getValue());
} // otherwise there were specific claims requested and this wasn't one of them
}
}
return result;
}
}

View File

@ -0,0 +1,115 @@
package org.mitre.openid.connect.service.impl;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.mitre.openid.connect.service.ScopeClaimTranslationService;
import org.springframework.stereotype.Service;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
/**
* Service to map scopes to claims, and claims to Java field names
*
* @author Amanda Anganes
*
*/
@Service("scopeClaimTranslator")
public class DefaultScopeClaimTranslationService implements ScopeClaimTranslationService {
private SetMultimap<String, String> scopesToClaims = HashMultimap.create();
private Map<String, String> claimsToFields = Maps.newHashMap();
/**
* Default constructor; initializes scopesToClaims map
*/
public DefaultScopeClaimTranslationService() {
scopesToClaims.put("openid", "sub");
scopesToClaims.put("profile", "name");
scopesToClaims.put("profile", "preferred_username");
scopesToClaims.put("profile", "given_name");
scopesToClaims.put("profile", "family_name");
scopesToClaims.put("profile", "middle_name");
scopesToClaims.put("profile", "nickname");
scopesToClaims.put("profile", "profile");
scopesToClaims.put("profile", "picture");
scopesToClaims.put("profile", "website");
scopesToClaims.put("profile", "gender");
scopesToClaims.put("profile", "zone_info");
scopesToClaims.put("profile", "locale");
scopesToClaims.put("profile", "updated_time");
scopesToClaims.put("profile", "birthdate");
scopesToClaims.put("email", "email");
scopesToClaims.put("email", "email_verified");
scopesToClaims.put("phone", "phone_number");
scopesToClaims.put("phone", "phone_number_verified");
scopesToClaims.put("address", "address");
claimsToFields.put("sub", "sub");
claimsToFields.put("name", "name");
claimsToFields.put("preferred_username", "preferredUsername");
claimsToFields.put("given_name", "givenName");
claimsToFields.put("family_name", "familyName");
claimsToFields.put("middle_name", "middleName");
claimsToFields.put("nickname", "nickname");
claimsToFields.put("profile", "profile");
claimsToFields.put("picture", "picture");
claimsToFields.put("website", "website");
claimsToFields.put("gender", "gender");
claimsToFields.put("zone_info", "zoneinfo");
claimsToFields.put("locale", "locale");
claimsToFields.put("updated_time", "updatedTime");
claimsToFields.put("birthdate", "birthdate");
claimsToFields.put("email", "email");
claimsToFields.put("email_verified", "emailVerified");
claimsToFields.put("phone_number", "phoneNumber");
claimsToFields.put("phone_number_verified", "phoneNumberVerified");
claimsToFields.put("address", "address");
}
/* (non-Javadoc)
* @see org.mitre.openid.connect.service.ScopeClaimTranslationService#getClaimsForScope(java.lang.String)
*/
@Override
public Set<String> getClaimsForScope(String scope) {
if (scopesToClaims.containsKey(scope)) {
return scopesToClaims.get(scope);
} else {
return new HashSet<String>();
}
}
/* (non-Javadoc)
* @see org.mitre.openid.connect.service.ScopeClaimTranslationService#getClaimsForScopeSet(java.util.Set)
*/
@Override
public Set<String> getClaimsForScopeSet(Set<String> scopes) {
Set<String> result = new HashSet<String>();
for (String scope : scopes) {
result.addAll(getClaimsForScope(scope));
}
return result;
}
/* (non-Javadoc)
* @see org.mitre.openid.connect.service.ScopeClaimTranslationService#getFieldNameForClaim(java.lang.String)
*/
@Override
public String getFieldNameForClaim(String claim) {
return claimsToFields.get(claim);
}
}

View File

@ -18,15 +18,19 @@ package org.mitre.openid.connect.view;
import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.web.servlet.view.AbstractView;
@ -35,6 +39,7 @@ import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@ -44,6 +49,9 @@ public class UserInfoView extends AbstractView {
private static JsonParser jsonParser = new JsonParser();
private static Logger logger = LoggerFactory.getLogger(UserInfoView.class);
@Autowired
private ScopeClaimTranslationService translator;
private Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() {
@ -95,7 +103,7 @@ public class UserInfoView extends AbstractView {
if (model.get("requestedClaims") != null) {
requestedClaims = jsonParser.parse((String) model.get("requestedClaims")).getAsJsonObject();
}
gson.toJson(UserInfoSerializer.toJsonFromRequestObj(userInfo, scope, authorizedClaims, requestedClaims), out);
gson.toJson(toJsonFromRequestObj(userInfo, scope, authorizedClaims, requestedClaims), out);
} catch (IOException e) {
@ -105,4 +113,57 @@ public class UserInfoView extends AbstractView {
}
/**
* 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 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
*/
private JsonObject toJsonFromRequestObj(UserInfo ui, Set<String> scope, JsonObject authorizedClaims, JsonObject requestedClaims) {
// get the base object
JsonObject obj = ui.toJson();
Set<String> allowedByScope = translator.getClaimsForScopeSet(scope);
Set<String> authorizedByClaims = new HashSet<String>();
Set<String> requestedByClaims = new HashSet<String>();
if (authorizedClaims != null) {
JsonObject userinfoAuthorized = authorizedClaims.getAsJsonObject().get("userinfo").getAsJsonObject();
for (Entry<String, JsonElement> entry : userinfoAuthorized.getAsJsonObject().entrySet()) {
authorizedByClaims.add(entry.getKey());
}
}
if (requestedClaims != null) {
JsonObject userinfoRequested = requestedClaims.getAsJsonObject().get("userinfo").getAsJsonObject();
for (Entry<String, JsonElement> entry : userinfoRequested.getAsJsonObject().entrySet()) {
requestedByClaims.add(entry.getKey());
}
}
// 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<String, JsonElement> entry : obj.entrySet()) {
if (allowedByScope.contains(entry.getKey())
|| authorizedByClaims.contains(entry.getKey())) {
// it's allowed either by scope or by the authorized claims (either way is fine with us)
if (requestedByClaims.isEmpty() || requestedByClaims.contains(entry.getKey())) {
// the requested claims are empty (so we allow all), or they're not empty and this claim was specifically asked for
result.add(entry.getKey(), entry.getValue());
} // otherwise there were specific claims requested and this wasn't one of them
}
}
return result;
}
}