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