001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2016, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * 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 distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.openid.connect.sdk.claims; 019 020 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.util.Collections; 024import java.util.HashSet; 025import java.util.Map; 026import java.util.Set; 027 028import net.minidev.json.JSONObject; 029 030import com.nimbusds.jwt.JWT; 031import com.nimbusds.jwt.JWTClaimsSet; 032import com.nimbusds.jwt.JWTParser; 033import com.nimbusds.oauth2.sdk.ParseException; 034import com.nimbusds.oauth2.sdk.id.Subject; 035import com.nimbusds.oauth2.sdk.token.AccessToken; 036import com.nimbusds.oauth2.sdk.token.TypelessAccessToken; 037import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 038import com.nimbusds.openid.connect.sdk.assurance.claims.VerifiedClaimsSet; 039 040 041/** 042 * UserInfo claims set, serialisable to a JSON object. 043 * 044 * <p>Supports normal, aggregated and distributed claims. 045 * 046 * <p>Example UserInfo claims set: 047 * 048 * <pre> 049 * { 050 * "sub" : "248289761001", 051 * "name" : "Jane Doe", 052 * "given_name" : "Jane", 053 * "family_name" : "Doe", 054 * "preferred_username" : "j.doe", 055 * "email" : "[email protected]", 056 * "picture" : "http://example.com/janedoe/me.jpg" 057 * } 058 * </pre> 059 * 060 * <p>Related specifications: 061 * 062 * <ul> 063 * <li>OpenID Connect Core 1.0, sections 5.1 and 5.6. 064 * <li>OpenID Connect for Identity Assurance 1.0, section 3.1. 065 * </ul> 066 */ 067public class UserInfo extends PersonClaims { 068 069 070 /** 071 * The subject claim name. 072 */ 073 public static final String SUB_CLAIM_NAME = "sub"; 074 075 076 077 /** 078 * The verified claims claim name. 079 */ 080 public static final String VERIFIED_CLAIMS_CLAIM_NAME = "verified_claims"; 081 082 083 /** 084 * Gets the names of the standard top-level UserInfo claims. 085 * 086 * @return The names of the standard top-level UserInfo claims 087 * (read-only set). 088 */ 089 public static Set<String> getStandardClaimNames() { 090 091 Set<String> names = new HashSet<>(PersonClaims.getStandardClaimNames()); 092 names.add(SUB_CLAIM_NAME); 093 names.add(VERIFIED_CLAIMS_CLAIM_NAME); 094 return Collections.unmodifiableSet(names); 095 } 096 097 098 /** 099 * Creates a new minimal UserInfo claims set. 100 * 101 * @param sub The subject. Must not be {@code null}. 102 */ 103 public UserInfo(final Subject sub) { 104 105 super(); 106 setClaim(SUB_CLAIM_NAME, sub.getValue()); 107 } 108 109 110 /** 111 * Creates a new UserInfo claims set from the specified JSON object. 112 * 113 * @param jsonObject The JSON object. Must not be {@code null}. 114 * 115 * @throws IllegalArgumentException If the JSON object doesn't contain 116 * a subject {@code sub} string claim. 117 */ 118 public UserInfo(final JSONObject jsonObject) { 119 120 super(jsonObject); 121 122 if (getStringClaim(SUB_CLAIM_NAME) == null) 123 throw new IllegalArgumentException("Missing or invalid \"sub\" claim"); 124 } 125 126 127 /** 128 * Creates a new UserInfo claims set from the specified JSON Web Token 129 * (JWT) claims set. 130 * 131 * @param jwtClaimsSet The JWT claims set. Must not be {@code null}. 132 * 133 * @throws IllegalArgumentException If the JWT claims set doesn't 134 * contain a subject {@code sub} 135 * string claim. 136 */ 137 public UserInfo(final JWTClaimsSet jwtClaimsSet) { 138 139 this(jwtClaimsSet.toJSONObject()); 140 } 141 142 143 /** 144 * Puts all claims from the specified other UserInfo claims set. 145 * Aggregated and distributed claims are properly merged. 146 * 147 * @param other The other UserInfo. Must have the same 148 * {@link #getSubject subject}. Must not be {@code null}. 149 * 150 * @throws IllegalArgumentException If the other UserInfo claims set 151 * doesn't have an identical subject, 152 * or if the external claims source ID 153 * of the other UserInfo matches an 154 * existing source ID. 155 */ 156 public void putAll(final UserInfo other) { 157 158 Subject otherSubject = other.getSubject(); 159 160 if (otherSubject == null) 161 throw new IllegalArgumentException("The subject of the other UserInfo is missing"); 162 163 if (! otherSubject.equals(getSubject())) 164 throw new IllegalArgumentException("The subject of the other UserInfo must be identical"); 165 166 // Save present aggregated and distributed claims, to prevent 167 // overwrite by put to claims JSON object 168 Set<AggregatedClaims> savedAggregatedClaims = getAggregatedClaims(); 169 Set<DistributedClaims> savedDistributedClaims = getDistributedClaims(); 170 171 // Save other present aggregated and distributed claims 172 Set<AggregatedClaims> otherAggregatedClaims = other.getAggregatedClaims(); 173 Set<DistributedClaims> otherDistributedClaims = other.getDistributedClaims(); 174 175 // Ensure external source IDs don't conflict during merge 176 Set<String> externalSourceIDs = new HashSet<>(); 177 178 if (savedAggregatedClaims != null) { 179 for (AggregatedClaims ac: savedAggregatedClaims) { 180 externalSourceIDs.add(ac.getSourceID()); 181 } 182 } 183 184 if (savedDistributedClaims != null) { 185 for (DistributedClaims dc: savedDistributedClaims) { 186 externalSourceIDs.add(dc.getSourceID()); 187 } 188 } 189 190 if (otherAggregatedClaims != null) { 191 for (AggregatedClaims ac: otherAggregatedClaims) { 192 if (externalSourceIDs.contains(ac.getSourceID())) { 193 throw new IllegalArgumentException("Aggregated claims source ID conflict: " + ac.getSourceID()); 194 } 195 } 196 } 197 198 if (otherDistributedClaims != null) { 199 for (DistributedClaims dc: otherDistributedClaims) { 200 if (externalSourceIDs.contains(dc.getSourceID())) { 201 throw new IllegalArgumentException("Distributed claims source ID conflict: " + dc.getSourceID()); 202 } 203 } 204 } 205 206 putAll((ClaimsSet)other); 207 208 // Merge saved external claims, if any 209 if (savedAggregatedClaims != null) { 210 for (AggregatedClaims ac: savedAggregatedClaims) { 211 addAggregatedClaims(ac); 212 } 213 } 214 215 if (savedDistributedClaims != null) { 216 for (DistributedClaims dc: savedDistributedClaims) { 217 addDistributedClaims(dc); 218 } 219 } 220 } 221 222 223 /** 224 * Gets the UserInfo subject. Corresponds to the {@code sub} claim. 225 * 226 * @return The subject. 227 */ 228 public Subject getSubject() { 229 230 return new Subject(getStringClaim(SUB_CLAIM_NAME)); 231 } 232 233 234 /** 235 * Gets the verified claims. Corresponds to the {@code verified_claims} 236 * claim from OpenID Connect for Identity Assurance 1.0. 237 * 238 * @return The verified claims set, {@code null} if not specified or 239 * parsing failed. 240 */ 241 public VerifiedClaimsSet getVerifiedClaimsSet() { 242 243 JSONObject jsonObject = getClaim(VERIFIED_CLAIMS_CLAIM_NAME, JSONObject.class); 244 if (jsonObject == null) { 245 return null; 246 } 247 try { 248 return VerifiedClaimsSet.parse(jsonObject); 249 } catch (ParseException e) { 250 return null; 251 } 252 } 253 254 255 /** 256 * Sets the verified claims. Corresponds to the {@code verified_claims} 257 * claim from OpenID Connect for Identity Assurance 1.0. 258 * 259 * @param verifiedClaims The verified claims set, {@code null} if not 260 * specified or parsing failed. 261 */ 262 public void setVerifiedClaims(final VerifiedClaimsSet verifiedClaims) { 263 264 if (verifiedClaims != null) { 265 setClaim(VERIFIED_CLAIMS_CLAIM_NAME, verifiedClaims.toJSONObject()); 266 } else { 267 setClaim(VERIFIED_CLAIMS_CLAIM_NAME, null); 268 } 269 } 270 271 272 /** 273 * Adds the specified aggregated claims provided by an external claims 274 * source. 275 * 276 * @param aggregatedClaims The aggregated claims instance, if 277 * {@code null} nothing will be added. 278 */ 279 public void addAggregatedClaims(final AggregatedClaims aggregatedClaims) { 280 281 if (aggregatedClaims == null) { 282 return; 283 } 284 285 aggregatedClaims.mergeInto(claims); 286 } 287 288 289 /** 290 * Gets the included aggregated claims provided by each external claims 291 * source. 292 * 293 * @return The aggregated claims, {@code null} if none are found. 294 */ 295 public Set<AggregatedClaims> getAggregatedClaims() { 296 297 Map<String,JSONObject> claimSources = ExternalClaimsUtils.getExternalClaimSources(claims); 298 299 if (claimSources == null) { 300 return null; // No external _claims_sources 301 } 302 303 Set<AggregatedClaims> aggregatedClaimsSet = new HashSet<>(); 304 305 for (Map.Entry<String,JSONObject> en: claimSources.entrySet()) { 306 307 String sourceID = en.getKey(); 308 JSONObject sourceSpec = en.getValue(); 309 310 Object jwtValue = sourceSpec.get("JWT"); 311 if (! (jwtValue instanceof String)) { 312 continue; // skip 313 } 314 315 JWT claimsJWT; 316 try { 317 claimsJWT = JWTParser.parse((String)jwtValue); 318 } catch (java.text.ParseException e) { 319 continue; // invalid JWT, skip 320 } 321 322 Set<String> claimNames = ExternalClaimsUtils.getExternalClaimNamesForSource(claims, sourceID); 323 324 if (claimNames.isEmpty()) { 325 continue; // skip 326 } 327 328 aggregatedClaimsSet.add(new AggregatedClaims(sourceID, claimNames, claimsJWT)); 329 } 330 331 if (aggregatedClaimsSet.isEmpty()) { 332 return null; 333 } 334 335 return aggregatedClaimsSet; 336 } 337 338 339 /** 340 * Adds the specified distributed claims from an external claims source. 341 * 342 * @param distributedClaims The distributed claims instance, if 343 * {@code null} nothing will be added. 344 */ 345 public void addDistributedClaims(final DistributedClaims distributedClaims) { 346 347 if (distributedClaims == null) { 348 return; 349 } 350 351 distributedClaims.mergeInto(claims); 352 } 353 354 355 /** 356 * Gets the included distributed claims provided by each external 357 * claims source. 358 * 359 * @return The distributed claims, {@code null} if none are found. 360 */ 361 public Set<DistributedClaims> getDistributedClaims() { 362 363 Map<String,JSONObject> claimSources = ExternalClaimsUtils.getExternalClaimSources(claims); 364 365 if (claimSources == null) { 366 return null; // No external _claims_sources 367 } 368 369 Set<DistributedClaims> distributedClaimsSet = new HashSet<>(); 370 371 for (Map.Entry<String,JSONObject> en: claimSources.entrySet()) { 372 373 String sourceID = en.getKey(); 374 JSONObject sourceSpec = en.getValue(); 375 376 Object endpointValue = sourceSpec.get("endpoint"); 377 if (! (endpointValue instanceof String)) { 378 continue; // skip 379 } 380 381 URI endpoint; 382 try { 383 endpoint = new URI((String)endpointValue); 384 } catch (URISyntaxException e) { 385 continue; // invalid URI, skip 386 } 387 388 AccessToken accessToken = null; 389 Object accessTokenValue = sourceSpec.get("access_token"); 390 if (accessTokenValue instanceof String) { 391 accessToken = new TypelessAccessToken((String)accessTokenValue); 392 } 393 394 Set<String> claimNames = ExternalClaimsUtils.getExternalClaimNamesForSource(claims, sourceID); 395 396 if (claimNames.isEmpty()) { 397 continue; // skip 398 } 399 400 distributedClaimsSet.add(new DistributedClaims(sourceID, claimNames, endpoint, accessToken)); 401 } 402 403 if (distributedClaimsSet.isEmpty()) { 404 return null; 405 } 406 407 return distributedClaimsSet; 408 } 409 410 411 /** 412 * Parses a UserInfo claims set from the specified JSON object string. 413 * 414 * @param json The JSON object string to parse. Must not be 415 * {@code null}. 416 * 417 * @return The UserInfo claims set. 418 * 419 * @throws ParseException If parsing failed. 420 */ 421 public static UserInfo parse(final String json) 422 throws ParseException { 423 424 JSONObject jsonObject = JSONObjectUtils.parse(json); 425 426 try { 427 return new UserInfo(jsonObject); 428 429 } catch (IllegalArgumentException e) { 430 431 throw new ParseException(e.getMessage(), e); 432 } 433 } 434}