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