001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Portions copyright 2011-2013 The MITRE Corporation 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 *******************************************************************************/ 018package org.mitre.openid.connect.view; 019 020import java.io.IOException; 021import java.io.Writer; 022import java.util.HashSet; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.Set; 026 027import javax.servlet.http.HttpServletRequest; 028import javax.servlet.http.HttpServletResponse; 029 030import org.mitre.openid.connect.model.UserInfo; 031import org.mitre.openid.connect.service.ScopeClaimTranslationService; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034import org.springframework.beans.factory.annotation.Autowired; 035import org.springframework.http.MediaType; 036import org.springframework.stereotype.Component; 037import org.springframework.validation.BeanPropertyBindingResult; 038import org.springframework.web.servlet.view.AbstractView; 039 040import com.google.gson.ExclusionStrategy; 041import com.google.gson.FieldAttributes; 042import com.google.gson.Gson; 043import com.google.gson.GsonBuilder; 044import com.google.gson.JsonElement; 045import com.google.gson.JsonObject; 046import com.google.gson.JsonParser; 047 048@Component(UserInfoView.VIEWNAME) 049public class UserInfoView extends AbstractView { 050 051 public static final String REQUESTED_CLAIMS = "requestedClaims"; 052 public static final String AUTHORIZED_CLAIMS = "authorizedClaims"; 053 public static final String SCOPE = "scope"; 054 public static final String USER_INFO = "userInfo"; 055 056 public static final String VIEWNAME = "userInfoView"; 057 058 private static JsonParser jsonParser = new JsonParser(); 059 060 /** 061 * Logger for this class 062 */ 063 private static final Logger logger = LoggerFactory.getLogger(UserInfoView.class); 064 065 @Autowired 066 private ScopeClaimTranslationService translator; 067 068 protected Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() { 069 070 @Override 071 public boolean shouldSkipField(FieldAttributes f) { 072 073 return false; 074 } 075 076 @Override 077 public boolean shouldSkipClass(Class<?> clazz) { 078 // skip the JPA binding wrapper 079 if (clazz.equals(BeanPropertyBindingResult.class)) { 080 return true; 081 } 082 return false; 083 } 084 085 }).create(); 086 087 /* 088 * (non-Javadoc) 089 * 090 * @see 091 * org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel 092 * (java.util.Map, javax.servlet.http.HttpServletRequest, 093 * javax.servlet.http.HttpServletResponse) 094 */ 095 @Override 096 protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) { 097 098 UserInfo userInfo = (UserInfo) model.get(USER_INFO); 099 100 Set<String> scope = (Set<String>) model.get(SCOPE); 101 102 response.setContentType(MediaType.APPLICATION_JSON_VALUE); 103 response.setCharacterEncoding("UTF-8"); 104 105 106 JsonObject authorizedClaims = null; 107 JsonObject requestedClaims = null; 108 if (model.get(AUTHORIZED_CLAIMS) != null) { 109 authorizedClaims = jsonParser.parse((String) model.get(AUTHORIZED_CLAIMS)).getAsJsonObject(); 110 } 111 if (model.get(REQUESTED_CLAIMS) != null) { 112 requestedClaims = jsonParser.parse((String) model.get(REQUESTED_CLAIMS)).getAsJsonObject(); 113 } 114 JsonObject json = toJsonFromRequestObj(userInfo, scope, authorizedClaims, requestedClaims); 115 116 writeOut(json, model, request, response); 117 } 118 119 protected void writeOut(JsonObject json, Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) { 120 try { 121 Writer out = response.getWriter(); 122 gson.toJson(json, out); 123 } catch (IOException e) { 124 125 logger.error("IOException in UserInfoView.java: ", e); 126 127 } 128 129 } 130 131 /** 132 * Build a JSON response according to the request object received. 133 * 134 * Claims requested in requestObj.userinfo.claims are added to any 135 * claims corresponding to requested scopes, if any. 136 * 137 * @param ui the UserInfo to filter 138 * @param scope the allowed scopes to filter by 139 * @param authorizedClaims the claims authorized by the client or user 140 * @param requestedClaims the claims requested in the RequestObject 141 * @return the filtered JsonObject result 142 */ 143 private JsonObject toJsonFromRequestObj(UserInfo ui, Set<String> scope, JsonObject authorizedClaims, JsonObject requestedClaims) { 144 145 // get the base object 146 JsonObject obj = ui.toJson(); 147 148 Set<String> allowedByScope = translator.getClaimsForScopeSet(scope); 149 Set<String> authorizedByClaims = extractUserInfoClaimsIntoSet(authorizedClaims); 150 Set<String> requestedByClaims = extractUserInfoClaimsIntoSet(requestedClaims); 151 152 // Filter claims by performing a manual intersection of claims that are allowed by the given scope, requested, and authorized. 153 // We cannot use Sets.intersection() or similar because Entry<> objects will evaluate to being unequal if their values are 154 // different, whereas we are only interested in matching the Entry<>'s key values. 155 JsonObject result = new JsonObject(); 156 for (Entry<String, JsonElement> entry : obj.entrySet()) { 157 158 if (allowedByScope.contains(entry.getKey()) 159 || authorizedByClaims.contains(entry.getKey())) { 160 // it's allowed either by scope or by the authorized claims (either way is fine with us) 161 162 if (requestedByClaims.isEmpty() || requestedByClaims.contains(entry.getKey())) { 163 // the requested claims are empty (so we allow all), or they're not empty and this claim was specifically asked for 164 result.add(entry.getKey(), entry.getValue()); 165 } // otherwise there were specific claims requested and this wasn't one of them 166 } 167 } 168 169 return result; 170 } 171 172 /** 173 * Pull the claims that have been targeted into a set for processing. 174 * Returns an empty set if the input is null. 175 * @param claims the claims request to process 176 */ 177 private Set<String> extractUserInfoClaimsIntoSet(JsonObject claims) { 178 Set<String> target = new HashSet<>(); 179 if (claims != null) { 180 JsonObject userinfoAuthorized = claims.getAsJsonObject("userinfo"); 181 if (userinfoAuthorized != null) { 182 for (Entry<String, JsonElement> entry : userinfoAuthorized.entrySet()) { 183 target.add(entry.getKey()); 184 } 185 } 186 } 187 return target; 188 } 189}