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; 019 020 021import java.util.*; 022 023import net.jcip.annotations.Immutable; 024import net.minidev.json.JSONObject; 025 026import com.nimbusds.langtag.LangTag; 027import com.nimbusds.langtag.LangTagException; 028import com.nimbusds.oauth2.sdk.ParseException; 029import com.nimbusds.oauth2.sdk.ResponseType; 030import com.nimbusds.oauth2.sdk.Scope; 031import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 032import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement; 033 034 035/** 036 * Specifies the individual claims to return from the UserInfo endpoint and / 037 * or in the ID Token. 038 * 039 * <p>Related specifications: 040 * 041 * <ul> 042 * <li>OpenID Connect Core 1.0, section 5.5. 043 * </ul> 044 */ 045public class ClaimsRequest { 046 047 048 /** 049 * Individual claim request. 050 * 051 * <p>Related specifications: 052 * 053 * <ul> 054 * <li>OpenID Connect Core 1.0, section 5.5.1. 055 * </ul> 056 */ 057 @Immutable 058 public static class Entry { 059 060 061 /** 062 * The claim name. 063 */ 064 private final String claimName; 065 066 067 /** 068 * The claim requirement. 069 */ 070 private final ClaimRequirement requirement; 071 072 073 /** 074 * Optional language tag. 075 */ 076 private final LangTag langTag; 077 078 079 /** 080 * Optional claim value. 081 */ 082 private final String value; 083 084 085 /** 086 * Optional claim values. 087 */ 088 private final List<String> values; 089 090 091 /** 092 * Optional additional claim information. 093 * 094 * <p>Example additional information in the "info" member: 095 * 096 * <pre> 097 * { 098 * "userinfo" : { 099 * "email": null, 100 * "email_verified": null, 101 * "http://example.info/claims/groups" : { "info" : "custom information" } } 102 * } 103 * </pre> 104 */ 105 private final Map<String, Object> additionalInformation; 106 107 108 /** 109 * Creates a new individual claim request. The claim 110 * requirement is set to voluntary (the default) and no 111 * expected value(s) are specified. 112 * 113 * @param claimName The claim name. Must not be {@code null}. 114 * @param langTag Optional language tag for the claim. 115 */ 116 public Entry(final String claimName, final LangTag langTag) { 117 118 this(claimName, ClaimRequirement.VOLUNTARY, langTag, null, null); 119 } 120 121 122 /** 123 * Creates a new individual claim request. 124 * 125 * @param claimName The claim name. Must not be {@code null}. 126 * @param requirement The claim requirement. Must not be 127 * {@code null}. 128 */ 129 public Entry(final String claimName, final ClaimRequirement requirement) { 130 131 this(claimName, requirement, null, null, null); 132 } 133 134 135 /** 136 * Creates a new individual claim request. 137 * 138 * @param claimName The claim name. Must not be {@code null}. 139 * @param requirement The claim requirement. Must not be 140 * {@code null}. 141 * @param langTag Optional language tag for the claim. 142 * @param value Optional expected value for the claim. 143 */ 144 public Entry(final String claimName, final ClaimRequirement requirement, 145 final LangTag langTag, final String value) { 146 147 this(claimName, requirement, langTag, value, null); 148 } 149 150 151 /** 152 * Creates a new individual claim request. 153 * 154 * @param claimName The claim name. Must not be {@code null}. 155 * @param requirement The claim requirement. Must not be 156 * {@code null}. 157 * @param langTag Optional language tag for the claim. 158 * @param values Optional expected values for the claim. 159 */ 160 public Entry(final String claimName, final ClaimRequirement requirement, 161 final LangTag langTag, final List<String> values) { 162 163 this(claimName, requirement, langTag, null, values, null); 164 } 165 166 167 /** 168 * Creates a new individual claim request. This constructor is 169 * to be used privately. Ensures that {@code value} and 170 * {@code values} are not simultaneously specified. 171 * 172 * @param claimName The claim name. Must not be {@code null}. 173 * @param requirement The claim requirement. Must not be 174 * {@code null}. 175 * @param langTag Optional language tag for the claim. 176 * @param value Optional expected value for the claim. If 177 * set, then the {@code values} parameter 178 * must not be set. 179 * @param values Optional expected values for the claim. 180 * If set, then the {@code value} parameter 181 * must not be set. 182 */ 183 private Entry(final String claimName, final ClaimRequirement requirement, final LangTag langTag, 184 final String value, final List<String> values) { 185 this(claimName, requirement, langTag, value, values, null); 186 } 187 188 189 /** 190 * Creates a new individual claim request. This constructor is 191 * to be used privately. Ensures that {@code value} and 192 * {@code values} are not simultaneously specified. 193 * 194 * @param claimName The claim name. Must not be 195 * {@code null}. 196 * @param requirement The claim requirement. Must not 197 * be {@code null}. 198 * @param langTag Optional language tag for the 199 * claim. 200 * @param value Optional expected value for the 201 * claim. If set, then the {@code 202 * values} parameter must not be 203 * set. 204 * @param values Optional expected values for 205 * the claim. If set, then the 206 * {@code value} parameter must 207 * not be set. 208 * @param additionalInformation Optional additional information 209 * about the requested Claims. 210 */ 211 private Entry(final String claimName, final ClaimRequirement requirement, final LangTag langTag, 212 final String value, final List<String> values, final Map<String, Object> additionalInformation) { 213 214 if (claimName == null) 215 throw new IllegalArgumentException("The claim name must not be null"); 216 217 this.claimName = claimName; 218 219 220 if (requirement == null) 221 throw new IllegalArgumentException("The claim requirement must not be null"); 222 223 this.requirement = requirement; 224 225 226 this.langTag = langTag; 227 228 229 if (value != null && values == null) { 230 231 this.value = value; 232 this.values = null; 233 234 } else if (value == null && values != null) { 235 236 this.value = null; 237 this.values = values; 238 239 } else if (value == null && values == null) { 240 241 this.value = null; 242 this.values = null; 243 244 } else { 245 246 throw new IllegalArgumentException("Either value or values must be specified, but not both"); 247 } 248 249 this.additionalInformation = additionalInformation; 250 } 251 252 253 /** 254 * Gets the claim name. 255 * 256 * @return The claim name. 257 */ 258 public String getClaimName() { 259 260 return claimName; 261 } 262 263 264 /** 265 * Gets the claim name, optionally with the language tag 266 * appended. 267 * 268 * <p>Example with language tag: 269 * 270 * <pre> 271 * name#de-DE 272 * </pre> 273 * 274 * @param withLangTag If {@code true} the language tag will be 275 * appended to the name (if any), else not. 276 * 277 * @return The claim name, with optionally appended language 278 * tag. 279 */ 280 public String getClaimName(final boolean withLangTag) { 281 282 if (withLangTag && langTag != null) 283 return claimName + "#" + langTag.toString(); 284 else 285 return claimName; 286 } 287 288 289 /** 290 * Gets the claim requirement. 291 * 292 * @return The claim requirement. 293 */ 294 public ClaimRequirement getClaimRequirement() { 295 296 return requirement; 297 } 298 299 300 /** 301 * Gets the optional language tag for the claim. 302 * 303 * @return The language tag, {@code null} if not specified. 304 */ 305 public LangTag getLangTag() { 306 307 return langTag; 308 } 309 310 311 /** 312 * Gets the optional value for the claim. 313 * 314 * @return The value, {@code null} if not specified. 315 */ 316 public String getValue() { 317 318 return value; 319 } 320 321 322 /** 323 * Gets the optional values for the claim. 324 * 325 * @return The values, {@code null} if not specified. 326 */ 327 public List<String> getValues() { 328 329 return values; 330 } 331 332 333 /** 334 * Gets the optional additional information for the claim. 335 * 336 * <p>Example additional information in the "info" member: 337 * 338 * <pre> 339 * { 340 * "userinfo" : { 341 * "email": null, 342 * "email_verified": null, 343 * "http://example.info/claims/groups" : { "info" : "custom information" } } 344 * } 345 * </pre> 346 * 347 * @return The additional information, {@code null} if not 348 * specified. 349 */ 350 public Map<String, Object> getAdditionalInformation() { 351 return additionalInformation; 352 } 353 354 355 /** 356 * Returns the JSON object representation of the specified 357 * collection of individual claim requests. 358 * 359 * <p>Example: 360 * 361 * <pre> 362 * { 363 * "given_name": {"essential": true}, 364 * "nickname": null, 365 * "email": {"essential": true}, 366 * "email_verified": {"essential": true}, 367 * "picture": null, 368 * "http://example.info/claims/groups": null 369 * } 370 * </pre> 371 * 372 * @param entries The entries to serialise. Must not be 373 * {@code null}. 374 * @return The corresponding JSON object, empty if no claims 375 * were found. 376 */ 377 public static JSONObject toJSONObject(final Collection<Entry> entries) { 378 379 JSONObject o = new JSONObject(); 380 381 for (Entry entry : entries) { 382 383 // Compose the optional value 384 JSONObject entrySpec = null; 385 386 if (entry.getValue() != null) { 387 388 entrySpec = new JSONObject(); 389 entrySpec.put("value", entry.getValue()); 390 } 391 392 if (entry.getValues() != null) { 393 394 // Either "value" or "values", or none 395 // may be defined 396 entrySpec = new JSONObject(); 397 entrySpec.put("values", entry.getValues()); 398 } 399 400 if (entry.getClaimRequirement().equals(ClaimRequirement.ESSENTIAL)) { 401 402 if (entrySpec == null) 403 entrySpec = new JSONObject(); 404 405 entrySpec.put("essential", true); 406 } 407 408 if (entry.getAdditionalInformation() != null) { 409 if (entrySpec == null) { 410 entrySpec = new JSONObject(); 411 } 412 for (Map.Entry<String, Object> additionalInformationEntry : entry.getAdditionalInformation().entrySet()) { 413 entrySpec.put(additionalInformationEntry.getKey(), additionalInformationEntry.getValue()); 414 } 415 } 416 417 o.put(entry.getClaimName(true), entrySpec); 418 } 419 420 return o; 421 } 422 423 424 /** 425 * Parses a collection of individual claim requests from the 426 * specified JSON object. Request entries that are not 427 * understood are silently ignored. 428 * 429 * @param jsonObject The JSON object to parse. Must not be 430 * {@code null}. 431 * 432 * @return The collection of claim requests. 433 */ 434 public static Collection<Entry> parseEntries(final JSONObject jsonObject) { 435 436 Collection<Entry> entries = new LinkedList<>(); 437 438 if (jsonObject.isEmpty()) 439 return entries; 440 441 for (Map.Entry<String, Object> member : jsonObject.entrySet()) { 442 443 // Process the key 444 String claimNameWithOptLangTag = member.getKey(); 445 446 String claimName; 447 LangTag langTag = null; 448 449 if (claimNameWithOptLangTag.contains("#")) { 450 451 String[] parts = claimNameWithOptLangTag.split("#", 2); 452 453 claimName = parts[0]; 454 455 try { 456 langTag = LangTag.parse(parts[1]); 457 458 } catch (LangTagException e) { 459 460 // Ignore and continue 461 continue; 462 } 463 464 } else { 465 claimName = claimNameWithOptLangTag; 466 } 467 468 // Parse the optional value 469 if (member.getValue() == null) { 470 471 // Voluntary claim with no value(s) 472 entries.add(new Entry(claimName, langTag)); 473 continue; 474 } 475 476 try { 477 JSONObject entrySpec = (JSONObject) member.getValue(); 478 479 ClaimRequirement requirement = ClaimRequirement.VOLUNTARY; 480 481 if (entrySpec.containsKey("essential")) { 482 483 boolean isEssential = (Boolean) entrySpec.get("essential"); 484 485 if (isEssential) 486 requirement = ClaimRequirement.ESSENTIAL; 487 } 488 489 if (entrySpec.containsKey("value")) { 490 491 String expectedValue = (String) entrySpec.get("value"); 492 Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(entrySpec); 493 entries.add(new Entry(claimName, requirement, langTag, expectedValue, null, additionalInformation)); 494 495 } else if (entrySpec.containsKey("values")) { 496 497 List<String> expectedValues = new LinkedList<>(); 498 499 for (Object v : (List) entrySpec.get("values")) { 500 501 expectedValues.add((String) v); 502 } 503 Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(entrySpec); 504 505 entries.add(new Entry(claimName, requirement, langTag, null, expectedValues, additionalInformation)); 506 507 } else { 508 Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(entrySpec); 509 entries.add(new Entry(claimName, requirement, langTag, (String) null, null, additionalInformation)); 510 } 511 512 } catch (Exception e) { 513 // Ignore and continue 514 } 515 } 516 517 return entries; 518 } 519 520 521 private static Map<String, Object> getAdditionalInformationFromClaim(JSONObject entrySpec) { 522 List<String> keysToRemove = Arrays.asList("essential", "value", "values"); 523 entrySpec.keySet().removeAll(keysToRemove); 524 Map<String, Object> additionalClaimInformation = new HashMap<>(); 525 for (Map.Entry<String, Object> additionalClaimInformationEntry : entrySpec.entrySet()) { 526 additionalClaimInformation.put(additionalClaimInformationEntry.getKey(), additionalClaimInformationEntry.getValue()); 527 } 528 return additionalClaimInformation.isEmpty() ? null : additionalClaimInformation; 529 } 530 } 531 532 533 /** 534 * The requested ID token claims, keyed by claim name and language tag. 535 */ 536 private final Map<Map.Entry<String, LangTag>, Entry> idTokenClaims = 537 new HashMap<>(); 538 539 540 /** 541 * The requested UserInfo claims, keyed by claim name and language tag. 542 */ 543 private final Map<Map.Entry<String, LangTag>, Entry> userInfoClaims = 544 new HashMap<>(); 545 546 547 /** 548 * Creates a new empty claims request. 549 */ 550 public ClaimsRequest() { 551 552 // Nothing to initialise 553 } 554 555 556 /** 557 * Adds the entries from the specified other claims request. 558 * 559 * @param other The other claims request. If {@code null} no claims 560 * request entries will be added to this claims request. 561 */ 562 public void add(final ClaimsRequest other) { 563 564 if (other == null) 565 return; 566 567 idTokenClaims.putAll(other.idTokenClaims); 568 userInfoClaims.putAll(other.userInfoClaims); 569 } 570 571 572 /** 573 * Adds the specified ID token claim to the request. It is marked as 574 * voluntary and no language tag and value(s) are associated with it. 575 * 576 * @param claimName The claim name. Must not be {@code null}. 577 */ 578 public void addIDTokenClaim(final String claimName) { 579 580 addIDTokenClaim(claimName, ClaimRequirement.VOLUNTARY); 581 } 582 583 584 /** 585 * Adds the specified ID token claim to the request. No language tag 586 * and value(s) are associated with it. 587 * 588 * @param claimName The claim name. Must not be {@code null}. 589 * @param requirement The claim requirement. Must not be {@code null}. 590 */ 591 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement) { 592 593 addIDTokenClaim(claimName, requirement, null); 594 } 595 596 597 /** 598 * Adds the specified ID token claim to the request. No value(s) are 599 * associated with it. 600 * 601 * @param claimName The claim name. Must not be {@code null}. 602 * @param requirement The claim requirement. Must not be {@code null}. 603 * @param langTag The associated language tag, {@code null} if not 604 * specified. 605 */ 606 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 607 final LangTag langTag) { 608 609 610 addIDTokenClaim(claimName, requirement, langTag, (String) null); 611 } 612 613 614 /** 615 * Adds the specified ID token claim to the request. 616 * 617 * @param claimName The claim name. Must not be {@code null}. 618 * @param requirement The claim requirement. Must not be {@code null}. 619 * @param langTag The associated language tag, {@code null} if not 620 * specified. 621 * @param value The expected claim value, {@code null} if not 622 * specified. 623 */ 624 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 625 final LangTag langTag, final String value) { 626 627 addIDTokenClaim(new Entry(claimName, requirement, langTag, value)); 628 } 629 630 631 /** 632 * Adds the specified ID token claim to the request. 633 * 634 * @param claimName The claim name. Must not be 635 * {@code null}. 636 * @param requirement The claim requirement. Must not be 637 * {@code null}. 638 * @param langTag The associated language tag, 639 * {@code null} if not specified. 640 * @param value The expected claim value, {@code null} 641 * if not specified. 642 * @param additionalInformation The additional information for this 643 * claim, {@code null} if not specified. 644 */ 645 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 646 final LangTag langTag, final String value, final Map<String, Object> additionalInformation) { 647 648 addIDTokenClaim(new Entry(claimName, requirement, langTag, value, null, additionalInformation)); 649 } 650 651 652 /** 653 * Adds the specified ID token claim to the request. 654 * 655 * @param claimName The claim name. Must not be {@code null}. 656 * @param requirement The claim requirement. Must not be {@code null}. 657 * @param langTag The associated language tag, {@code null} if not 658 * specified. 659 * @param values The expected claim values, {@code null} if not 660 * specified. 661 */ 662 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 663 final LangTag langTag, final List<String> values) { 664 665 addIDTokenClaim(new Entry(claimName, requirement, langTag, values)); 666 } 667 668 669 /** 670 * Adds the specified ID token claim to the request. 671 * 672 * @param claimName The claim name. Must not be 673 * {@code null}. 674 * @param requirement The claim requirement. Must not be 675 * {@code null}. 676 * @param langTag The associated language tag, 677 * {@code null} if not specified. 678 * @param values The expected claim values, {@code null} 679 * if not specified. 680 * @param additionalInformation The additional information for this 681 * claim, {@code null} if not specified. 682 */ 683 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 684 final LangTag langTag, final List<String> values, final Map<String, Object> additionalInformation) { 685 686 addIDTokenClaim(new Entry(claimName, requirement, langTag, null, values, additionalInformation)); 687 } 688 689 690 /** 691 * Adds the specified ID token claim to the request. 692 * 693 * @param entry The individual ID token claim request. Must not be 694 * {@code null}. 695 */ 696 public void addIDTokenClaim(final Entry entry) { 697 698 Map.Entry<String, LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 699 entry.getClaimName(), 700 entry.getLangTag()); 701 702 idTokenClaims.put(key, entry); 703 } 704 705 706 /** 707 * Gets the requested ID token claims. 708 * 709 * @return The ID token claims, as an unmodifiable collection, empty 710 * set if none. 711 */ 712 public Collection<Entry> getIDTokenClaims() { 713 714 return Collections.unmodifiableCollection(idTokenClaims.values()); 715 } 716 717 718 /** 719 * Gets the names of the requested ID token claim names. 720 * 721 * @param withLangTag If {@code true} the language tags, if any, will 722 * be appended to the names, else not. 723 * 724 * @return The ID token claim names, as an unmodifiable set, empty set 725 * if none. 726 */ 727 public Set<String> getIDTokenClaimNames(final boolean withLangTag) { 728 729 Set<String> names = new HashSet<>(); 730 731 for (Entry en : idTokenClaims.values()) 732 names.add(en.getClaimName(withLangTag)); 733 734 return Collections.unmodifiableSet(names); 735 } 736 737 738 /** 739 * Removes the specified ID token claim from the request. 740 * 741 * @param claimName The claim name. Must not be {@code null}. 742 * @param langTag The associated language tag, {@code null} if none. 743 * 744 * @return The removed ID token claim, {@code null} if not found. 745 */ 746 public Entry removeIDTokenClaim(final String claimName, final LangTag langTag) { 747 748 Map.Entry<String, LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 749 claimName, 750 langTag); 751 752 return idTokenClaims.remove(key); 753 } 754 755 756 /** 757 * Removes the specified ID token claims from the request, in all 758 * existing language tag variations. 759 * 760 * @param claimName The claim name. Must not be {@code null}. 761 * 762 * @return The removed ID token claims, as an unmodifiable collection, 763 * empty set if none were found. 764 */ 765 public Collection<Entry> removeIDTokenClaims(final String claimName) { 766 767 Collection<Entry> removedClaims = new LinkedList<>(); 768 769 Iterator<Map.Entry<Map.Entry<String, LangTag>, Entry>> it = idTokenClaims.entrySet().iterator(); 770 771 while (it.hasNext()) { 772 773 Map.Entry<Map.Entry<String, LangTag>, Entry> reqEntry = it.next(); 774 775 if (reqEntry.getKey().getKey().equals(claimName)) { 776 777 removedClaims.add(reqEntry.getValue()); 778 779 it.remove(); 780 } 781 } 782 783 return Collections.unmodifiableCollection(removedClaims); 784 } 785 786 787 /** 788 * Adds the specified UserInfo claim to the request. It is marked as 789 * voluntary and no language tag and value(s) are associated with it. 790 * 791 * @param claimName The claim name. Must not be {@code null}. 792 */ 793 public void addUserInfoClaim(final String claimName) { 794 795 addUserInfoClaim(claimName, ClaimRequirement.VOLUNTARY); 796 } 797 798 799 /** 800 * Adds the specified UserInfo claim to the request. No language tag and 801 * value(s) are associated with it. 802 * 803 * @param claimName The claim name. Must not be {@code null}. 804 * @param requirement The claim requirement. Must not be {@code null}. 805 */ 806 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement) { 807 808 addUserInfoClaim(claimName, requirement, null); 809 } 810 811 812 /** 813 * Adds the specified UserInfo claim to the request. No value(s) are 814 * associated with it. 815 * 816 * @param claimName The claim name. Must not be {@code null}. 817 * @param requirement The claim requirement. Must not be {@code null}. 818 * @param langTag The associated language tag, {@code null} if not 819 * specified. 820 */ 821 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 822 final LangTag langTag) { 823 824 825 addUserInfoClaim(claimName, requirement, langTag, (String) null); 826 } 827 828 829 /** 830 * Adds the specified UserInfo claim to the request. 831 * 832 * @param claimName The claim name. Must not be {@code null}. 833 * @param requirement The claim requirement. Must not be {@code null}. 834 * @param langTag The associated language tag, {@code null} if not 835 * specified. 836 * @param value The expected claim value, {@code null} if not 837 * specified. 838 */ 839 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 840 final LangTag langTag, final String value) { 841 842 addUserInfoClaim(new Entry(claimName, requirement, langTag, value)); 843 } 844 845 846 /** 847 * Adds the specified UserInfo claim to the request. 848 * 849 * @param claimName The claim name. Must not be {@code 850 * null}. 851 * @param requirement The claim requirement. Must not be 852 * {@code null}. 853 * @param langTag The associated language tag, {@code 854 * null} if not specified. 855 * @param value The expected claim value, {@code null} 856 * if not specified. 857 * @param additionalInformation The additional information for this 858 * claim, {@code null} if not specified. 859 */ 860 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 861 final LangTag langTag, final String value, final Map<String, Object> additionalInformation) { 862 863 addUserInfoClaim(new Entry(claimName, requirement, langTag, value, null, additionalInformation)); 864 } 865 866 867 /** 868 * Adds the specified UserInfo claim to the request. 869 * 870 * @param claimName The claim name. Must not be {@code null}. 871 * @param requirement The claim requirement. Must not be {@code null}. 872 * @param langTag The associated language tag, {@code null} if not 873 * specified. 874 * @param values The expected claim values, {@code null} if not 875 * specified. 876 */ 877 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 878 final LangTag langTag, final List<String> values) { 879 880 addUserInfoClaim(new Entry(claimName, requirement, langTag, values)); 881 } 882 883 884 /** 885 * Adds the specified UserInfo claim to the request. 886 * 887 * @param claimName The claim name. Must not be 888 * {@code null}. 889 * @param requirement The claim requirement. Must not be 890 * {@code null}. 891 * @param langTag The associated language tag, 892 * {@code null} if not specified. 893 * @param values The expected claim values, {@code null} 894 * if not specified. 895 * @param additionalInformation The additional information for this 896 * claim, {@code null} if not specified. 897 */ 898 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 899 final LangTag langTag, final List<String> values, final Map<String, Object> additionalInformation) { 900 901 addUserInfoClaim(new Entry(claimName, requirement, langTag, null, values, additionalInformation)); 902 } 903 904 905 /** 906 * Adds the specified UserInfo claim to the request. 907 * 908 * @param entry The individual UserInfo claim request. Must not be 909 * {@code null}. 910 */ 911 public void addUserInfoClaim(final Entry entry) { 912 913 Map.Entry<String, LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 914 entry.getClaimName(), 915 entry.getLangTag()); 916 917 userInfoClaims.put(key, entry); 918 } 919 920 921 /** 922 * Gets the requested UserInfo claims. 923 * 924 * @return The UserInfo claims, as an unmodifiable collection, empty 925 * set if none. 926 */ 927 public Collection<Entry> getUserInfoClaims() { 928 929 return Collections.unmodifiableCollection(userInfoClaims.values()); 930 } 931 932 933 /** 934 * Gets the names of the requested UserInfo claim names. 935 * 936 * @param withLangTag If {@code true} the language tags, if any, will 937 * be appended to the names, else not. 938 * 939 * @return The UserInfo claim names, as an unmodifiable set, empty set 940 * if none. 941 */ 942 public Set<String> getUserInfoClaimNames(final boolean withLangTag) { 943 944 Set<String> names = new HashSet<>(); 945 946 for (Entry en : userInfoClaims.values()) 947 names.add(en.getClaimName(withLangTag)); 948 949 return Collections.unmodifiableSet(names); 950 } 951 952 953 /** 954 * Removes the specified UserInfo claim from the request. 955 * 956 * @param claimName The claim name. Must not be {@code null}. 957 * @param langTag The associated language tag, {@code null} if none. 958 * 959 * @return The removed UserInfo claim, {@code null} if not found. 960 */ 961 public Entry removeUserInfoClaim(final String claimName, final LangTag langTag) { 962 963 Map.Entry<String, LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 964 claimName, 965 langTag); 966 967 return userInfoClaims.remove(key); 968 } 969 970 971 /** 972 * Removes the specified UserInfo claims from the request, in all 973 * existing language tag variations. 974 * 975 * @param claimName The claim name. Must not be {@code null}. 976 * 977 * @return The removed UserInfo claims, as an unmodifiable collection, 978 * empty set if none were found. 979 */ 980 public Collection<Entry> removeUserInfoClaims(final String claimName) { 981 982 Collection<Entry> removedClaims = new LinkedList<>(); 983 984 Iterator<Map.Entry<Map.Entry<String, LangTag>, Entry>> it = userInfoClaims.entrySet().iterator(); 985 986 while (it.hasNext()) { 987 988 Map.Entry<Map.Entry<String, LangTag>, Entry> reqEntry = it.next(); 989 990 if (reqEntry.getKey().getKey().equals(claimName)) { 991 992 removedClaims.add(reqEntry.getValue()); 993 994 it.remove(); 995 } 996 } 997 998 return Collections.unmodifiableCollection(removedClaims); 999 } 1000 1001 1002 /** 1003 * Returns the JSON object representation of this claims request. 1004 * 1005 * <p>Example: 1006 * 1007 * <pre> 1008 * { 1009 * "userinfo": 1010 * { 1011 * "given_name": {"essential": true}, 1012 * "nickname": null, 1013 * "email": {"essential": true}, 1014 * "email_verified": {"essential": true}, 1015 * "picture": null, 1016 * "http://example.info/claims/groups": null 1017 * }, 1018 * "id_token": 1019 * { 1020 * "auth_time": {"essential": true}, 1021 * "acr": {"values": ["urn:mace:incommon:iap:silver"] } 1022 * } 1023 * } 1024 * </pre> 1025 * 1026 * @return The corresponding JSON object, empty if no ID token and 1027 * UserInfo claims are specified. 1028 */ 1029 public JSONObject toJSONObject() { 1030 1031 JSONObject o = new JSONObject(); 1032 1033 Collection<Entry> idTokenEntries = getIDTokenClaims(); 1034 1035 if (!idTokenEntries.isEmpty()) { 1036 1037 o.put("id_token", Entry.toJSONObject(idTokenEntries)); 1038 } 1039 1040 Collection<Entry> userInfoEntries = getUserInfoClaims(); 1041 1042 if (!userInfoEntries.isEmpty()) { 1043 1044 o.put("userinfo", Entry.toJSONObject(userInfoEntries)); 1045 } 1046 1047 return o; 1048 } 1049 1050 1051 @Override 1052 public String toString() { 1053 1054 return toJSONObject().toString(); 1055 } 1056 1057 1058 /** 1059 * Resolves the claims request for the specified response type and 1060 * scope. The scope values that are {@link OIDCScopeValue standard 1061 * OpenID scope values} are resolved to their respective individual 1062 * claims requests, any other scope values are ignored. 1063 * 1064 * @param responseType The response type. Must not be {@code null}. 1065 * @param scope The scope, {@code null} if not specified (for a 1066 * plain OAuth 2.0 authorisation request with no 1067 * scope explicitly specified). 1068 * 1069 * @return The claims request. 1070 */ 1071 public static ClaimsRequest resolve(final ResponseType responseType, final Scope scope) { 1072 1073 return resolve(responseType, scope, Collections.<Scope.Value, Set<String>>emptyMap()); 1074 } 1075 1076 1077 /** 1078 * Resolves the claims request for the specified response type and 1079 * scope. The scope values that are {@link OIDCScopeValue standard 1080 * OpenID scope values} are resolved to their respective individual 1081 * claims requests, any other scope values are checked in the specified 1082 * custom claims map and resolved accordingly. 1083 * 1084 * @param responseType The response type. Must not be {@code null}. 1085 * @param scope The scope, {@code null} if not specified (for a 1086 * plain OAuth 2.0 authorisation request with no 1087 * scope explicitly specified). 1088 * @param customClaims Custom scope value to set of claim names map, 1089 * {@code null} if not specified. 1090 * 1091 * @return The claims request. 1092 */ 1093 public static ClaimsRequest resolve(final ResponseType responseType, 1094 final Scope scope, 1095 final Map<Scope.Value, Set<String>> customClaims) { 1096 1097 // Determine the claims target (ID token or UserInfo) 1098 final boolean switchToIDToken = 1099 responseType.contains(OIDCResponseTypeValue.ID_TOKEN) && 1100 !responseType.contains(ResponseType.Value.CODE) && 1101 !responseType.contains(ResponseType.Value.TOKEN); 1102 1103 ClaimsRequest claimsRequest = new ClaimsRequest(); 1104 1105 if (scope == null) { 1106 // Plain OAuth 2.0 mode 1107 return claimsRequest; 1108 } 1109 1110 for (Scope.Value value : scope) { 1111 1112 Set<ClaimsRequest.Entry> entries; 1113 1114 if (value.equals(OIDCScopeValue.PROFILE)) { 1115 1116 entries = OIDCScopeValue.PROFILE.toClaimsRequestEntries(); 1117 1118 } else if (value.equals(OIDCScopeValue.EMAIL)) { 1119 1120 entries = OIDCScopeValue.EMAIL.toClaimsRequestEntries(); 1121 1122 } else if (value.equals(OIDCScopeValue.PHONE)) { 1123 1124 entries = OIDCScopeValue.PHONE.toClaimsRequestEntries(); 1125 1126 } else if (value.equals(OIDCScopeValue.ADDRESS)) { 1127 1128 entries = OIDCScopeValue.ADDRESS.toClaimsRequestEntries(); 1129 1130 } else if (customClaims != null && customClaims.containsKey(value)) { 1131 1132 // Process custom scope value -> claim names expansion, e.g. 1133 // "corp_profile" -> ["employeeNumber", "dept", "ext"] 1134 Set<String> claimNames = customClaims.get(value); 1135 1136 if (claimNames == null || claimNames.isEmpty()) { 1137 continue; // skip 1138 } 1139 1140 entries = new HashSet<>(); 1141 1142 for (String claimName: claimNames) { 1143 entries.add(new ClaimsRequest.Entry(claimName, ClaimRequirement.VOLUNTARY)); 1144 } 1145 1146 } else { 1147 1148 continue; // skip 1149 } 1150 1151 for (ClaimsRequest.Entry en : entries) { 1152 1153 if (switchToIDToken) 1154 claimsRequest.addIDTokenClaim(en); 1155 else 1156 claimsRequest.addUserInfoClaim(en); 1157 } 1158 } 1159 1160 return claimsRequest; 1161 } 1162 1163 1164 private static Map<String, Object> resolveAdditionalInformationForClaim(final Map<String, Object> customClaims) { 1165 customClaims.remove("essential"); 1166 customClaims.remove("value"); 1167 customClaims.remove("values"); 1168 return customClaims.isEmpty() ? null : customClaims; 1169 } 1170 1171 1172 /** 1173 * Resolves the merged claims request from the specified OpenID 1174 * authentication request parameters. The scope values that are {@link 1175 * OIDCScopeValue standard OpenID scope values} are resolved to their 1176 * respective individual claims requests, any other scope values are 1177 * ignored. 1178 * 1179 * @param responseType The response type. Must not be {@code null}. 1180 * @param scope The scope, {@code null} if not specified (for a 1181 * plain OAuth 2.0 authorisation request with no 1182 * scope explicitly specified). 1183 * @param claimsRequest The claims request, corresponding to the 1184 * optional {@code claims} OpenID Connect 1185 * authorisation request parameter, {@code null} 1186 * if not specified. 1187 * 1188 * @return The merged claims request. 1189 */ 1190 public static ClaimsRequest resolve(final ResponseType responseType, 1191 final Scope scope, 1192 final ClaimsRequest claimsRequest) { 1193 1194 return resolve(responseType, scope, claimsRequest, Collections.<Scope.Value, Set<String>>emptyMap()); 1195 } 1196 1197 1198 /** 1199 * Resolves the merged claims request from the specified OpenID 1200 * authentication request parameters. The scope values that are {@link 1201 * OIDCScopeValue standard OpenID scope values} are resolved to their 1202 * respective individual claims requests, any other scope values are 1203 * checked in the specified custom claims map and resolved accordingly. 1204 * 1205 * @param responseType The response type. Must not be {@code null}. 1206 * @param scope The scope, {@code null} if not specified (for a 1207 * plain OAuth 2.0 authorisation request with no 1208 * scope explicitly specified). 1209 * @param claimsRequest The claims request, corresponding to the 1210 * optional {@code claims} OpenID Connect 1211 * authorisation request parameter, {@code null} 1212 * if not specified. 1213 * @param customClaims Custom scope value to set of claim names map, 1214 * {@code null} if not specified. 1215 * 1216 * @return The merged claims request. 1217 */ 1218 public static ClaimsRequest resolve(final ResponseType responseType, 1219 final Scope scope, 1220 final ClaimsRequest claimsRequest, 1221 final Map<Scope.Value, Set<String>> customClaims) { 1222 1223 ClaimsRequest mergedClaimsRequest = resolve(responseType, scope, customClaims); 1224 1225 mergedClaimsRequest.add(claimsRequest); 1226 1227 return mergedClaimsRequest; 1228 } 1229 1230 1231 /** 1232 * Resolves the merged claims request for the specified OpenID 1233 * authentication request. The scope values that are {@link 1234 * OIDCScopeValue standard OpenID scope values} are resolved to their 1235 * respective individual claims requests, any other scope values are 1236 * ignored. 1237 * 1238 * @param authRequest The OpenID authentication request. Must not be 1239 * {@code null}. 1240 * 1241 * @return The merged claims request. 1242 */ 1243 public static ClaimsRequest resolve(final AuthenticationRequest authRequest) { 1244 1245 return resolve(authRequest.getResponseType(), authRequest.getScope(), authRequest.getClaims()); 1246 } 1247 1248 1249 /** 1250 * Parses a claims request from the specified JSON object 1251 * representation. Unexpected members in the JSON object are silently 1252 * ignored. 1253 * 1254 * @param jsonObject The JSON object to parse. Must not be 1255 * {@code null}. 1256 * 1257 * @return The claims request. 1258 */ 1259 public static ClaimsRequest parse(final JSONObject jsonObject) { 1260 1261 ClaimsRequest claimsRequest = new ClaimsRequest(); 1262 1263 try { 1264 if (jsonObject.containsKey("id_token")) { 1265 1266 JSONObject idTokenObject = (JSONObject) jsonObject.get("id_token"); 1267 1268 Collection<Entry> idTokenClaims = Entry.parseEntries(idTokenObject); 1269 1270 for (Entry entry : idTokenClaims) 1271 claimsRequest.addIDTokenClaim(entry); 1272 } 1273 1274 1275 if (jsonObject.containsKey("userinfo")) { 1276 1277 JSONObject userInfoObject = (JSONObject) jsonObject.get("userinfo"); 1278 1279 Collection<Entry> userInfoClaims = Entry.parseEntries(userInfoObject); 1280 1281 for (Entry entry : userInfoClaims) 1282 claimsRequest.addUserInfoClaim(entry); 1283 } 1284 1285 } catch (Exception e) { 1286 1287 // Ignore 1288 } 1289 1290 return claimsRequest; 1291 } 1292 1293 1294 /** 1295 * Parses a claims request from the specified JSON object string 1296 * representation. Unexpected members in the JSON object are silently 1297 * ignored. 1298 * 1299 * @param json The JSON object string to parse. Must not be 1300 * {@code null}. 1301 * 1302 * @return The claims request. 1303 * 1304 * @throws ParseException If the string couldn't be parsed to a valid 1305 * JSON object. 1306 */ 1307 public static ClaimsRequest parse(final String json) 1308 throws ParseException { 1309 1310 return parse(JSONObjectUtils.parse(json)); 1311 } 1312}