001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2020, 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.util.*; 022 023import net.jcip.annotations.Immutable; 024import net.minidev.json.JSONAware; 025import net.minidev.json.JSONObject; 026 027import com.nimbusds.langtag.LangTag; 028import com.nimbusds.langtag.LangTagException; 029import com.nimbusds.oauth2.sdk.ParseException; 030import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 031 032 033/** 034 * OpenID Connect claims set request, intended to represent the 035 * {@code userinfo} and {@code id_token} elements in a 036 * {@link com.nimbusds.openid.connect.sdk.OIDCClaimsRequest claims} request 037 * parameter. 038 * 039 * <p>Example: 040 * 041 * <pre> 042 * { 043 * "given_name": {"essential": true}, 044 * "nickname": null, 045 * "email": {"essential": true}, 046 * "email_verified": {"essential": true}, 047 * "picture": null, 048 * "http://example.info/claims/groups": null 049 * } 050 * </pre> 051 * 052 * <p>Related specifications: 053 * 054 * <ul> 055 * <li>OpenID Connect Core 1.0, section 5.5. 056 * <li>OpenID Connect for Identity Assurance 1.0. 057 * </ul> 058 */ 059@Immutable 060public class ClaimsSetRequest implements JSONAware { 061 062 063 /** 064 * Individual OpenID claim request. 065 * 066 * <p>Related specifications: 067 * 068 * <ul> 069 * <li>OpenID Connect Core 1.0, section 5.5.1. 070 * <li>OpenID Connect for Identity Assurance 1.0. 071 * </ul> 072 */ 073 @Immutable 074 public static class Entry { 075 076 077 /** 078 * The claim name. 079 */ 080 private final String claimName; 081 082 083 /** 084 * The claim requirement. 085 */ 086 private final ClaimRequirement requirement; 087 088 089 /** 090 * Optional language tag. 091 */ 092 private final LangTag langTag; 093 094 095 /** 096 * Optional claim value. 097 */ 098 private final String value; 099 100 101 /** 102 * Optional claim values. 103 */ 104 private final List<String> values; 105 106 107 /** 108 * Optional claim purpose. 109 */ 110 private final String purpose; 111 112 113 /** 114 * Optional additional claim information. 115 * 116 * <p>Example additional information in the "info" member: 117 * 118 * <pre> 119 * { 120 * "userinfo" : { 121 * "email": null, 122 * "email_verified": null, 123 * "http://example.info/claims/groups" : { "info" : "custom information" } 124 * } 125 * } 126 * </pre> 127 */ 128 private final Map<String, Object> additionalInformation; 129 130 131 /** 132 * Creates a new individual claim request. The claim 133 * requirement is set to {@link ClaimRequirement#VOLUNTARY 134 * voluntary} (the default) and no expected value(s) or other 135 * parameters are specified. 136 * 137 * @param claimName The claim name. Must not be {@code null}. 138 */ 139 public Entry(final String claimName) { 140 this(claimName, ClaimRequirement.VOLUNTARY, null, null, null, null, null); 141 } 142 143 144 /** 145 * Creates a new individual claim request. This constructor is 146 * to be used privately. Ensures that {@code value} and 147 * {@code values} are not simultaneously specified. 148 * 149 * @param claimName The claim name. Must not be 150 * {@code null}. 151 * @param requirement The claim requirement. Must not 152 * be {@code null}. 153 * @param langTag Optional language tag for the 154 * claim. 155 * @param value Optional expected value for the 156 * claim. If set, then the {@code 157 * values} parameter must not be 158 * set. 159 * @param values Optional expected values for 160 * the claim. If set, then the 161 * {@code value} parameter must 162 * not be set. 163 * @param purpose The purpose for the requested 164 * claim, {@code null} if not 165 * specified. 166 * @param additionalInformation Optional additional information 167 */ 168 private Entry(final String claimName, 169 final ClaimRequirement requirement, 170 final LangTag langTag, 171 final String value, 172 final List<String> values, 173 final String purpose, 174 final Map<String, Object> additionalInformation) { 175 176 if (claimName == null) 177 throw new IllegalArgumentException("The claim name must not be null"); 178 179 this.claimName = claimName; 180 181 182 if (requirement == null) 183 throw new IllegalArgumentException("The claim requirement must not be null"); 184 185 this.requirement = requirement; 186 187 188 this.langTag = langTag; 189 190 191 if (value != null && values == null) { 192 193 this.value = value; 194 this.values = null; 195 196 } else if (value == null && values != null) { 197 198 this.value = null; 199 this.values = values; 200 201 } else if (value == null && values == null) { 202 203 this.value = null; 204 this.values = null; 205 206 } else { 207 208 throw new IllegalArgumentException("Either value or values must be specified, but not both"); 209 } 210 211 this.purpose = purpose; 212 213 this.additionalInformation = additionalInformation; 214 } 215 216 217 /** 218 * Returns the claim name. 219 * 220 * @return The claim name. 221 */ 222 public String getClaimName() { 223 return getClaimName(false); 224 } 225 226 227 /** 228 * Returns the claim name, optionally with the language tag 229 * appended. 230 * 231 * <p>Example with language tag: 232 * 233 * <pre> 234 * name#de-DE 235 * </pre> 236 * 237 * @param withLangTag If {@code true} the language tag will be 238 * appended to the name (if any), else not. 239 * 240 * @return The claim name, with optionally appended language 241 * tag. 242 */ 243 public String getClaimName(final boolean withLangTag) { 244 245 if (withLangTag && langTag != null) 246 return claimName + "#" + langTag.toString(); 247 else 248 return claimName; 249 } 250 251 252 /** 253 * Sets the claim requirement. 254 * 255 * @param requirement The claim requirement. Must not be 256 * {@code null}, 257 * 258 * @return The updated entry. 259 */ 260 public ClaimsSetRequest.Entry withClaimRequirement(final ClaimRequirement requirement) { 261 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation); 262 } 263 264 265 /** 266 * Returns the claim requirement. 267 * 268 * @return The claim requirement. 269 */ 270 public ClaimRequirement getClaimRequirement() { 271 return requirement; 272 } 273 274 275 /** 276 * Sets the language tag for the claim. 277 * 278 * @param langTag The language tag, {@code null} if not 279 * specified. 280 * 281 * @return The updated entry. 282 */ 283 public ClaimsSetRequest.Entry withLangTag(final LangTag langTag) { 284 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation); 285 } 286 287 288 /** 289 * Returns the optional language tag for the claim. 290 * 291 * @return The language tag, {@code null} if not specified. 292 */ 293 public LangTag getLangTag() { 294 return langTag; 295 } 296 297 298 /** 299 * Sets the requested value for the claim. 300 * 301 * @param value The value, {@code null} if not specified. 302 * 303 * @return The updated entry. 304 */ 305 public ClaimsSetRequest.Entry withValue(final String value) { 306 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, null, purpose, additionalInformation); 307 } 308 309 310 /** 311 * Returns the requested value for the claim. 312 * 313 * @return The value, {@code null} if not specified. 314 */ 315 public String getValue() { 316 return value; 317 } 318 319 320 /** 321 * Sets the requested values for the claim. 322 * 323 * @param values The values, {@code null} if not specified. 324 * 325 * @return The updated entry. 326 */ 327 public ClaimsSetRequest.Entry withValues(final List<String> values) { 328 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, null, values, purpose, additionalInformation); 329 } 330 331 332 /** 333 * Returns the requested values for the claim. 334 * 335 * @return The values, {@code null} if not specified. 336 */ 337 public List<String> getValues() { 338 return values; 339 } 340 341 342 /** 343 * Sets the purpose for which the claim is requested. 344 * 345 * @param purpose The purpose, {@code null} if not specified. 346 * 347 * @return The updated entry. 348 */ 349 public ClaimsSetRequest.Entry withPurpose(final String purpose) { 350 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation); 351 } 352 353 354 /** 355 * Returns the optional purpose for which the claim is 356 * requested. 357 * 358 * @return The purpose, {@code null} if not specified. 359 */ 360 public String getPurpose() { 361 return purpose; 362 } 363 364 365 /** 366 * Sets additional information for the requested claim. 367 * 368 * <p>Example additional information in the "info" member: 369 * 370 * <pre> 371 * { 372 * "userinfo" : { 373 * "email": null, 374 * "email_verified": null, 375 * "http://example.info/claims/groups" : { "info" : "custom information" } 376 * } 377 * } 378 * </pre> 379 * 380 * @param additionalInformation The additional information, 381 * {@code null} if not specified. 382 * 383 * @return The updated entry. 384 */ 385 public ClaimsSetRequest.Entry withAdditionalInformation(final Map<String, Object> additionalInformation) { 386 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation); 387 } 388 389 390 /** 391 * Returns the additional information for the claim. 392 * 393 * <p>Example additional information in the "info" member: 394 * 395 * <pre> 396 * { 397 * "userinfo" : { 398 * "email": null, 399 * "email_verified": null, 400 * "http://example.info/claims/groups" : { "info" : "custom information" } 401 * } 402 * } 403 * </pre> 404 * 405 * @return The additional information, {@code null} if not 406 * specified. 407 */ 408 public Map<String, Object> getAdditionalInformation() { 409 return additionalInformation; 410 } 411 412 413 /** 414 * Returns the JSON object entry for this individual claim 415 * request. 416 * 417 * @return The JSON object entry. 418 */ 419 public Map.Entry<String,JSONObject> toJSONObjectEntry() { 420 421 // Compose the optional value 422 JSONObject entrySpec = null; 423 424 if (getValue() != null) { 425 426 entrySpec = new JSONObject(); 427 entrySpec.put("value", getValue()); 428 } 429 430 if (getValues() != null) { 431 432 // Either "value" or "values", or none 433 // may be defined 434 entrySpec = new JSONObject(); 435 entrySpec.put("values", getValues()); 436 } 437 438 if (getClaimRequirement().equals(ClaimRequirement.ESSENTIAL)) { 439 440 if (entrySpec == null) 441 entrySpec = new JSONObject(); 442 443 entrySpec.put("essential", true); 444 } 445 446 if (getPurpose() != null) { 447 if (entrySpec == null) { 448 entrySpec = new JSONObject(); 449 } 450 entrySpec.put("purpose", getPurpose()); 451 } 452 453 if (getAdditionalInformation() != null) { 454 if (entrySpec == null) { 455 entrySpec = new JSONObject(); 456 } 457 for (Map.Entry<String, Object> additionalInformationEntry : getAdditionalInformation().entrySet()) { 458 entrySpec.put(additionalInformationEntry.getKey(), additionalInformationEntry.getValue()); 459 } 460 } 461 462 return new AbstractMap.SimpleImmutableEntry<>(getClaimName(true), entrySpec); 463 } 464 465 466 /** 467 * Parses an individual claim request from the specified JSON 468 * object entry. 469 * 470 * @param jsonObjectEntry The JSON object entry to parse. Must 471 * not be {@code null}. 472 * 473 * @return The individual claim request. 474 * 475 * @throws ParseException If parsing failed. 476 */ 477 public static ClaimsSetRequest.Entry parse(final Map.Entry<String,JSONObject> jsonObjectEntry) 478 throws ParseException { 479 480 // Process the key 481 String claimNameWithOptLangTag = jsonObjectEntry.getKey(); 482 483 String claimName; 484 LangTag langTag = null; 485 486 if (claimNameWithOptLangTag.contains("#")) { 487 488 String[] parts = claimNameWithOptLangTag.split("#", 2); 489 490 claimName = parts[0]; 491 492 try { 493 langTag = LangTag.parse(parts[1]); 494 } catch (LangTagException e) { 495 throw new ParseException(e.getMessage(), e); 496 } 497 498 } else { 499 claimName = claimNameWithOptLangTag; 500 } 501 502 // Parse the optional spec 503 504 JSONObject spec = jsonObjectEntry.getValue(); 505 506 if (spec == null) { 507 // Voluntary claim with no value(s) 508 return new ClaimsSetRequest.Entry(claimName).withLangTag(langTag); 509 } 510 511 ClaimRequirement requirement = ClaimRequirement.VOLUNTARY; 512 513 if (spec.containsKey("essential")) { 514 515 boolean isEssential = JSONObjectUtils.getBoolean(spec, "essential"); 516 517 if (isEssential) 518 requirement = ClaimRequirement.ESSENTIAL; 519 } 520 521 String purpose = JSONObjectUtils.getString(spec, "purpose", null); 522 523 if (spec.containsKey("value")) { 524 525 String expectedValue = JSONObjectUtils.getString(spec, "value", null); 526 Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(spec); 527 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, expectedValue, null, purpose, additionalInformation); 528 529 } else if (spec.containsKey("values")) { 530 531 List<String> expectedValues = JSONObjectUtils.getStringList(spec, "values", null); 532 Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(spec); 533 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, null, expectedValues, purpose, additionalInformation); 534 535 } else { 536 Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(spec); 537 return new ClaimsSetRequest.Entry(claimName, requirement, langTag, null, null, purpose, additionalInformation); 538 } 539 } 540 541 542 private static Map<String, Object> getAdditionalInformationFromClaim(final JSONObject spec) { 543 544 Set<String> stdKeys = new HashSet<>(Arrays.asList("essential", "value", "values", "purpose")); 545 546 Map<String, Object> additionalClaimInformation = new HashMap<>(); 547 548 for (Map.Entry<String, Object> additionalClaimInformationEntry : spec.entrySet()) { 549 if (stdKeys.contains(additionalClaimInformationEntry.getKey())) { 550 continue; // skip std key 551 } 552 additionalClaimInformation.put(additionalClaimInformationEntry.getKey(), additionalClaimInformationEntry.getValue()); 553 } 554 555 return additionalClaimInformation.isEmpty() ? null : additionalClaimInformation; 556 } 557 } 558 559 560 /** 561 * The request entries. 562 */ 563 private final Collection<ClaimsSetRequest.Entry> entries; 564 565 566 /** 567 * Creates a new empty OpenID Connect claims set request. 568 */ 569 public ClaimsSetRequest() { 570 this(Collections.<Entry>emptyList()); 571 } 572 573 574 /** 575 * Creates a new OpenID Connect claims set request. 576 * 577 * @param entries The request entries, empty collection if none. Must 578 * not be {@code null}. 579 */ 580 public ClaimsSetRequest(final Collection<ClaimsSetRequest.Entry> entries) { 581 if (entries == null) { 582 throw new IllegalArgumentException("The entries must not be null"); 583 } 584 this.entries = Collections.unmodifiableCollection(entries); 585 } 586 587 588 /** 589 * Adds the specified claim to the request, using default settings. 590 * Shorthand for {@link #add(Entry)}. 591 * 592 * @param claimName The claim name. Must not be {@code null}. 593 * 594 * @return The updated claims set request. 595 */ 596 public ClaimsSetRequest add(final String claimName) { 597 return add(new ClaimsSetRequest.Entry(claimName)); 598 } 599 600 601 /** 602 * Adds the specified claim to the request. 603 * 604 * @param entry The individual claim request. Must not be {@code null}. 605 * 606 * @return The updated claims set request. 607 */ 608 public ClaimsSetRequest add(final ClaimsSetRequest.Entry entry) { 609 List<Entry> updatedEntries = new LinkedList<>(getEntries()); 610 updatedEntries.add(entry); 611 return new ClaimsSetRequest(updatedEntries); 612 } 613 614 615 /** 616 * Gets the request entries. 617 * 618 * @return The request entries, empty collection if none. 619 */ 620 public Collection<ClaimsSetRequest.Entry> getEntries() { 621 return Collections.unmodifiableCollection(entries); 622 } 623 624 625 /** 626 * Gets the names of the requested claims. 627 * 628 * @param withLangTag If {@code true} the language tags, if any, will 629 * be appended to the names, else not. 630 * 631 * @return The claim names, as an unmodifiable set, empty set if none. 632 */ 633 public Set<String> getClaimNames(final boolean withLangTag) { 634 Set<String> names = new HashSet<>(); 635 for (ClaimsSetRequest.Entry en : entries) { 636 names.add(en.getClaimName(withLangTag)); 637 } 638 return Collections.unmodifiableSet(names); 639 } 640 641 642 /** 643 * Gets the specified claim entry from this request. 644 * 645 * @param claimName The claim name. Must not be {@code null}. 646 * @param langTag The associated language tag, {@code null} if none. 647 * 648 * @return The claim entry, {@code null} if not found. 649 */ 650 public Entry get(final String claimName, final LangTag langTag) { 651 652 for (ClaimsSetRequest.Entry en: getEntries()) { 653 if (claimName.equals(en.getClaimName()) && langTag == null && en.getLangTag() == null) { 654 // No lang tag 655 return en; 656 } else if (claimName.equals(en.getClaimName()) && langTag != null && langTag.equals(en.getLangTag())) { 657 // Matching lang tag 658 return en; 659 } 660 } 661 return null; 662 } 663 664 665 /** 666 * Deletes the specified claim from this request. 667 * 668 * @param claimName The claim name. Must not be {@code null}. 669 * @param langTag The associated language tag, {@code null} if none. 670 * 671 * @return The updated claims set request. 672 */ 673 public ClaimsSetRequest delete(final String claimName, final LangTag langTag) { 674 675 Collection<ClaimsSetRequest.Entry> updatedEntries = new LinkedList<>(); 676 677 for (ClaimsSetRequest.Entry en: getEntries()) { 678 if (claimName.equals(en.getClaimName()) && langTag == null && en.getLangTag() == null) { 679 // don't copy 680 } else if (claimName.equals(en.getClaimName()) && langTag != null && langTag.equals(en.getLangTag())) { 681 // don't copy 682 } else { 683 updatedEntries.add(en); 684 } 685 } 686 687 return new ClaimsSetRequest(updatedEntries); 688 } 689 690 691 /** 692 * Deletes the specified claim from this request, in all existing 693 * language tag variations if any. 694 * 695 * @param claimName The claim name. Must not be {@code null}. 696 * 697 * @return The updated claims set request. 698 */ 699 public ClaimsSetRequest delete(final String claimName) { 700 Collection<ClaimsSetRequest.Entry> updatedEntries = new LinkedList<>(); 701 702 for (ClaimsSetRequest.Entry en: getEntries()) { 703 if (claimName.equals(en.getClaimName())) { 704 // don't copy 705 } else { 706 updatedEntries.add(en); 707 } 708 } 709 710 return new ClaimsSetRequest(updatedEntries); 711 } 712 713 714 /** 715 * Returns the JSON object representation of this claims set request. 716 * 717 * <p>Example: 718 * 719 * <pre> 720 * { 721 * "given_name": {"essential": true}, 722 * "nickname": null, 723 * "email": {"essential": true}, 724 * "email_verified": {"essential": true}, 725 * "picture": null, 726 * "http://example.info/claims/groups": null 727 * } 728 * </pre> 729 * 730 * @return The JSON object, empty if no claims are specified. 731 */ 732 public JSONObject toJSONObject() { 733 JSONObject o = new JSONObject(); 734 for (ClaimsSetRequest.Entry entry : entries) { 735 Map.Entry<String, JSONObject> jsonObjectEntry = entry.toJSONObjectEntry(); 736 o.put(jsonObjectEntry.getKey(), jsonObjectEntry.getValue()); 737 } 738 return o; 739 } 740 741 742 @Override 743 public String toJSONString() { 744 return toJSONObject().toJSONString(); 745 } 746 747 748 @Override 749 public String toString() { 750 return toJSONString(); 751 } 752 753 754 /** 755 * Parses an OpenID Connect claims set request from the specified JSON 756 * object representation. 757 * 758 * @param jsonObject The JSON object to parse. Must not be 759 * {@code null}. 760 * 761 * @return The claims set request. 762 * 763 * @throws ParseException If parsing failed. 764 */ 765 public static ClaimsSetRequest parse(final JSONObject jsonObject) 766 throws ParseException { 767 768 ClaimsSetRequest claimsRequest = new ClaimsSetRequest(); 769 770 for (String key: jsonObject.keySet()) { 771 772 if ("verified_claims".equals(key)) { 773 // Implies nested VerifiedClaimsSetRequest, skip 774 continue; 775 } 776 777 JSONObject value = JSONObjectUtils.getJSONObject(jsonObject, key, null); 778 779 claimsRequest = claimsRequest.add(ClaimsSetRequest.Entry.parse(new AbstractMap.SimpleImmutableEntry<>(key, value))); 780 } 781 782 return claimsRequest; 783 } 784 785 786 /** 787 * Parses an OpenID Connect claims set request from the specified JSON 788 * object string representation. 789 * 790 * @param json The JSON object string to parse. Must not be 791 * {@code null}. 792 * 793 * @return The claims set request. 794 * 795 * @throws ParseException If parsing failed. 796 */ 797 public static ClaimsSetRequest parse(final String json) 798 throws ParseException { 799 800 return parse(JSONObjectUtils.parse(json)); 801 } 802}