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 * @param jsonObject The JSON object to parse. Must not be 352 * {@code null}. 353 * 354 * @return The collection of claim requests. 355 */ 356 public static Collection<Entry> parseEntries(final JSONObject jsonObject) { 357 358 Collection<Entry> entries = new LinkedList<>(); 359 360 if (jsonObject.isEmpty()) 361 return entries; 362 363 for (Map.Entry<String,Object> member: jsonObject.entrySet()) { 364 365 // Process the key 366 String claimNameWithOptLangTag = member.getKey(); 367 368 String claimName; 369 LangTag langTag = null; 370 371 if (claimNameWithOptLangTag.contains("#")) { 372 373 String[] parts = claimNameWithOptLangTag.split("#", 2); 374 375 claimName = parts[0]; 376 377 try { 378 langTag = LangTag.parse(parts[1]); 379 380 } catch (LangTagException e) { 381 382 // Ignore and continue 383 continue; 384 } 385 386 } else { 387 claimName = claimNameWithOptLangTag; 388 } 389 390 // Parse the optional value 391 if (member.getValue() == null) { 392 393 // Voluntary claim with no value(s) 394 entries.add(new Entry(claimName, langTag)); 395 continue; 396 } 397 398 try { 399 JSONObject entrySpec = (JSONObject)member.getValue(); 400 401 ClaimRequirement requirement = ClaimRequirement.VOLUNTARY; 402 403 if (entrySpec.containsKey("essential")) { 404 405 boolean isEssential = (Boolean)entrySpec.get("essential"); 406 407 if (isEssential) 408 requirement = ClaimRequirement.ESSENTIAL; 409 } 410 411 if (entrySpec.containsKey("value")) { 412 413 String expectedValue = (String)entrySpec.get("value"); 414 415 entries.add(new Entry(claimName, requirement, langTag, expectedValue)); 416 417 } else if (entrySpec.containsKey("values")) { 418 419 List<String> expectedValues = new LinkedList<>(); 420 421 for (Object v: (List)entrySpec.get("values")) { 422 423 expectedValues.add((String)v); 424 } 425 426 entries.add(new Entry(claimName, requirement, langTag, expectedValues)); 427 428 } else { 429 entries.add(new Entry(claimName, requirement, langTag, (String)null)); 430 } 431 432 } catch (Exception e) { 433 // Ignore and continue 434 } 435 } 436 437 return entries; 438 } 439 } 440 441 442 /** 443 * The requested ID token claims, keyed by claim name and language tag. 444 */ 445 private final Map<Map.Entry<String,LangTag>,Entry> idTokenClaims = 446 new HashMap<>(); 447 448 449 /** 450 * The requested UserInfo claims, keyed by claim name and language tag. 451 */ 452 private final Map<Map.Entry<String,LangTag>,Entry> userInfoClaims = 453 new HashMap<>(); 454 455 456 /** 457 * Creates a new empty claims request. 458 */ 459 public ClaimsRequest() { 460 461 // Nothing to initialise 462 } 463 464 465 /** 466 * Adds the entries from the specified other claims request. 467 * 468 * @param other The other claims request. If {@code null} no claims 469 * request entries will be added to this claims request. 470 */ 471 public void add(final ClaimsRequest other) { 472 473 if (other == null) 474 return; 475 476 idTokenClaims.putAll(other.idTokenClaims); 477 userInfoClaims.putAll(other.userInfoClaims); 478 } 479 480 481 /** 482 * Adds the specified ID token claim to the request. It is marked as 483 * voluntary and no language tag and value(s) are associated with it. 484 * 485 * @param claimName The claim name. Must not be {@code null}. 486 */ 487 public void addIDTokenClaim(final String claimName) { 488 489 addIDTokenClaim(claimName, ClaimRequirement.VOLUNTARY); 490 } 491 492 493 /** 494 * Adds the specified ID token claim to the request. No language tag 495 * and value(s) are associated with it. 496 * 497 * @param claimName The claim name. Must not be {@code null}. 498 * @param requirement The claim requirement. Must not be {@code null}. 499 */ 500 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement) { 501 502 addIDTokenClaim(claimName, requirement, null); 503 } 504 505 506 /** 507 * Adds the specified ID token claim to the request. No value(s) are 508 * associated with it. 509 * 510 * @param claimName The claim name. Must not be {@code null}. 511 * @param requirement The claim requirement. Must not be {@code null}. 512 * @param langTag The associated language tag, {@code null} if not 513 * specified. 514 */ 515 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 516 final LangTag langTag) { 517 518 519 addIDTokenClaim(claimName, requirement, langTag, (String)null); 520 } 521 522 523 /** 524 * Adds the specified ID token claim to the request. 525 * 526 * @param claimName The claim name. Must not be {@code null}. 527 * @param requirement The claim requirement. Must not be {@code null}. 528 * @param langTag The associated language tag, {@code null} if not 529 * specified. 530 * @param value The expected claim value, {@code null} if not 531 * specified. 532 */ 533 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 534 final LangTag langTag, final String value) { 535 536 addIDTokenClaim(new Entry(claimName, requirement, langTag, value)); 537 } 538 539 540 /** 541 * Adds the specified ID token claim to the request. 542 * 543 * @param claimName The claim name. Must not be {@code null}. 544 * @param requirement The claim requirement. Must not be {@code null}. 545 * @param langTag The associated language tag, {@code null} if not 546 * specified. 547 * @param values The expected claim values, {@code null} if not 548 * specified. 549 */ 550 public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 551 final LangTag langTag, final List<String> values) { 552 553 addIDTokenClaim(new Entry(claimName, requirement, langTag, values)); 554 } 555 556 557 /** 558 * Adds the specified ID token claim to the request. 559 * 560 * @param entry The individual ID token claim request. Must not be 561 * {@code null}. 562 */ 563 public void addIDTokenClaim(final Entry entry) { 564 565 Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 566 entry.getClaimName(), 567 entry.getLangTag()); 568 569 idTokenClaims.put(key, entry); 570 } 571 572 573 /** 574 * Gets the requested ID token claims. 575 * 576 * @return The ID token claims, as an unmodifiable collection, empty 577 * set if none. 578 */ 579 public Collection<Entry> getIDTokenClaims() { 580 581 return Collections.unmodifiableCollection(idTokenClaims.values()); 582 } 583 584 585 /** 586 * Gets the names of the requested ID token claim names. 587 * 588 * @param withLangTag If {@code true} the language tags, if any, will 589 * be appended to the names, else not. 590 * 591 * 592 * @return The ID token claim names, as an unmodifiable set, empty set 593 * if none. 594 */ 595 public Set<String> getIDTokenClaimNames(final boolean withLangTag) { 596 597 Set<String> names = new HashSet<>(); 598 599 for (Entry en: idTokenClaims.values()) 600 names.add(en.getClaimName(withLangTag)); 601 602 return Collections.unmodifiableSet(names); 603 } 604 605 606 /** 607 * Removes the specified ID token claim from the request. 608 * 609 * @param claimName The claim name. Must not be {@code null}. 610 * @param langTag The associated language tag, {@code null} if none. 611 * 612 * @return The removed ID token claim, {@code null} if not found. 613 */ 614 public Entry removeIDTokenClaim(final String claimName, final LangTag langTag) { 615 616 Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 617 claimName, 618 langTag); 619 620 return idTokenClaims.remove(key); 621 } 622 623 624 /** 625 * Removes the specified ID token claims from the request, in all 626 * existing language tag variations. 627 * 628 * @param claimName The claim name. Must not be {@code null}. 629 * 630 * @return The removed ID token claims, as an unmodifiable collection, 631 * empty set if none were found. 632 */ 633 public Collection<Entry> removeIDTokenClaims(final String claimName) { 634 635 Collection<Entry> removedClaims = new LinkedList<>(); 636 637 Iterator<Map.Entry<Map.Entry<String,LangTag>,Entry>> it = idTokenClaims.entrySet().iterator(); 638 639 while (it.hasNext()) { 640 641 Map.Entry<Map.Entry<String,LangTag>,Entry> reqEntry = it.next(); 642 643 if (reqEntry.getKey().getKey().equals(claimName)) { 644 645 removedClaims.add(reqEntry.getValue()); 646 647 it.remove(); 648 } 649 } 650 651 return Collections.unmodifiableCollection(removedClaims); 652 } 653 654 655 /** 656 * Adds the specified UserInfo claim to the request. It is marked as 657 * voluntary and no language tag and value(s) are associated with it. 658 * 659 * @param claimName The claim name. Must not be {@code null}. 660 */ 661 public void addUserInfoClaim(final String claimName) { 662 663 addUserInfoClaim(claimName, ClaimRequirement.VOLUNTARY); 664 } 665 666 667 /** 668 * Adds the specified UserInfo claim to the request. No language tag 669 * and value(s) are associated with it. 670 * 671 * @param claimName The claim name. Must not be {@code null}. 672 * @param requirement The claim requirement. Must not be {@code null}. 673 */ 674 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement) { 675 676 addUserInfoClaim(claimName, requirement, null); 677 } 678 679 680 /** 681 * Adds the specified UserInfo claim to the request. No value(s) are 682 * associated with it. 683 * 684 * @param claimName The claim name. Must not be {@code null}. 685 * @param requirement The claim requirement. Must not be {@code null}. 686 * @param langTag The associated language tag, {@code null} if not 687 * specified. 688 */ 689 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 690 final LangTag langTag) { 691 692 693 addUserInfoClaim(claimName, requirement, langTag, (String)null); 694 } 695 696 697 /** 698 * Adds the specified UserInfo claim to the request. 699 * 700 * @param claimName The claim name. Must not be {@code null}. 701 * @param requirement The claim requirement. Must not be {@code null}. 702 * @param langTag The associated language tag, {@code null} if not 703 * specified. 704 * @param value The expected claim value, {@code null} if not 705 * specified. 706 */ 707 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 708 final LangTag langTag, final String value) { 709 710 addUserInfoClaim(new Entry(claimName, requirement, langTag, value)); 711 } 712 713 714 /** 715 * Adds the specified UserInfo claim to the request. 716 * 717 * @param claimName The claim name. Must not be {@code null}. 718 * @param requirement The claim requirement. Must not be {@code null}. 719 * @param langTag The associated language tag, {@code null} if not 720 * specified. 721 * @param values The expected claim values, {@code null} if not 722 * specified. 723 */ 724 public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 725 final LangTag langTag, final List<String> values) { 726 727 addUserInfoClaim(new Entry(claimName, requirement, langTag, values)); 728 } 729 730 731 /** 732 * Adds the specified UserInfo claim to the request. 733 * 734 * @param entry The individual UserInfo claim request. Must not be 735 * {@code null}. 736 */ 737 public void addUserInfoClaim(final Entry entry) { 738 739 Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 740 entry.getClaimName(), 741 entry.getLangTag()); 742 743 userInfoClaims.put(key, entry); 744 } 745 746 747 /** 748 * Gets the requested UserInfo claims. 749 * 750 * @return The UserInfo claims, as an unmodifiable collection, empty 751 * set if none. 752 */ 753 public Collection<Entry> getUserInfoClaims() { 754 755 return Collections.unmodifiableCollection(userInfoClaims.values()); 756 } 757 758 759 /** 760 * Gets the names of the requested UserInfo claim names. 761 * 762 * @param withLangTag If {@code true} the language tags, if any, will 763 * be appended to the names, else not. 764 * 765 * 766 * @return The UserInfo claim names, as an unmodifiable set, empty set 767 * if none. 768 */ 769 public Set<String> getUserInfoClaimNames(final boolean withLangTag) { 770 771 Set<String> names = new HashSet<>(); 772 773 for (Entry en: userInfoClaims.values()) 774 names.add(en.getClaimName(withLangTag)); 775 776 return Collections.unmodifiableSet(names); 777 } 778 779 780 /** 781 * Removes the specified UserInfo claim from the request. 782 * 783 * @param claimName The claim name. Must not be {@code null}. 784 * @param langTag The associated language tag, {@code null} if none. 785 * 786 * @return The removed UserInfo claim, {@code null} if not found. 787 */ 788 public Entry removeUserInfoClaim(final String claimName, final LangTag langTag) { 789 790 Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>( 791 claimName, 792 langTag); 793 794 return userInfoClaims.remove(key); 795 } 796 797 798 /** 799 * Removes the specified UserInfo claims from the request, in all 800 * existing language tag variations. 801 * 802 * @param claimName The claim name. Must not be {@code null}. 803 * 804 * @return The removed UserInfo claims, as an unmodifiable collection, 805 * empty set if none were found. 806 */ 807 public Collection<Entry> removeUserInfoClaims(final String claimName) { 808 809 Collection<Entry> removedClaims = new LinkedList<>(); 810 811 Iterator<Map.Entry<Map.Entry<String,LangTag>,Entry>> it = userInfoClaims.entrySet().iterator(); 812 813 while (it.hasNext()) { 814 815 Map.Entry<Map.Entry<String,LangTag>,Entry> reqEntry = it.next(); 816 817 if (reqEntry.getKey().getKey().equals(claimName)) { 818 819 removedClaims.add(reqEntry.getValue()); 820 821 it.remove(); 822 } 823 } 824 825 return Collections.unmodifiableCollection(removedClaims); 826 } 827 828 829 /** 830 * Returns the JSON object representation of this claims request. 831 * 832 * <p>Example: 833 * 834 * <pre> 835 * { 836 * "userinfo": 837 * { 838 * "given_name": {"essential": true}, 839 * "nickname": null, 840 * "email": {"essential": true}, 841 * "email_verified": {"essential": true}, 842 * "picture": null, 843 * "http://example.info/claims/groups": null 844 * }, 845 * "id_token": 846 * { 847 * "auth_time": {"essential": true}, 848 * "acr": {"values": ["urn:mace:incommon:iap:silver"] } 849 * } 850 * } 851 * </pre> 852 * 853 * @return The corresponding JSON object, empty if no ID token and 854 * UserInfo claims are specified. 855 */ 856 public JSONObject toJSONObject() { 857 858 JSONObject o = new JSONObject(); 859 860 Collection<Entry> idTokenEntries = getIDTokenClaims(); 861 862 if (! idTokenEntries.isEmpty()) { 863 864 o.put("id_token", Entry.toJSONObject(idTokenEntries)); 865 } 866 867 Collection<Entry> userInfoEntries = getUserInfoClaims(); 868 869 if (! userInfoEntries.isEmpty()) { 870 871 o.put("userinfo", Entry.toJSONObject(userInfoEntries)); 872 } 873 874 return o; 875 } 876 877 878 @Override 879 public String toString() { 880 881 return toJSONObject().toString(); 882 } 883 884 885 /** 886 * Resolves the claims request for the specified response type and 887 * scope. The scope values that are {@link OIDCScopeValue standard 888 * OpenID scope values} are resolved to their respective individual 889 * claims requests, any other scope values are ignored. 890 * 891 * @param responseType The response type. Must not be {@code null}. 892 * @param scope The scope, {@code null} if not specified (for a 893 * plain OAuth 2.0 authorisation request with no 894 * scope explicitly specified). 895 * 896 * @return The claims request. 897 */ 898 public static ClaimsRequest resolve(final ResponseType responseType, final Scope scope) { 899 900 return resolve(responseType, scope, Collections.<Scope.Value,Set<String>>emptyMap()); 901 } 902 903 904 /** 905 * Resolves the claims request for the specified response type and 906 * scope. The scope values that are {@link OIDCScopeValue standard 907 * OpenID scope values} are resolved to their respective individual 908 * claims requests, any other scope values are checked in the specified 909 * custom claims map and resolved accordingly. 910 * 911 * @param responseType The response type. Must not be {@code null}. 912 * @param scope The scope, {@code null} if not specified (for a 913 * plain OAuth 2.0 authorisation request with no 914 * scope explicitly specified). 915 * @param customClaims Custom scope to claim name map, {@code null} if 916 * not specified. 917 * 918 * @return The claims request. 919 */ 920 public static ClaimsRequest resolve(final ResponseType responseType, 921 final Scope scope, 922 final Map<Scope.Value,Set<String>> customClaims) { 923 924 // Determine the claims target (ID token or UserInfo) 925 final boolean switchToIDToken = 926 responseType.contains(OIDCResponseTypeValue.ID_TOKEN) && 927 ! responseType.contains(ResponseType.Value.CODE) && 928 ! responseType.contains(ResponseType.Value.TOKEN); 929 930 ClaimsRequest claimsRequest = new ClaimsRequest(); 931 932 if (scope == null) { 933 // Plain OAuth 2.0 mode 934 return claimsRequest; 935 } 936 937 for (Scope.Value value: scope) { 938 939 Set<ClaimsRequest.Entry> entries; 940 941 if (value.equals(OIDCScopeValue.PROFILE)) { 942 943 entries = OIDCScopeValue.PROFILE.toClaimsRequestEntries(); 944 945 } else if (value.equals(OIDCScopeValue.EMAIL)) { 946 947 entries = OIDCScopeValue.EMAIL.toClaimsRequestEntries(); 948 949 } else if (value.equals(OIDCScopeValue.PHONE)) { 950 951 entries = OIDCScopeValue.PHONE.toClaimsRequestEntries(); 952 953 } else if (value.equals(OIDCScopeValue.ADDRESS)) { 954 955 entries = OIDCScopeValue.ADDRESS.toClaimsRequestEntries(); 956 957 } else if (customClaims != null && customClaims.containsKey(value)) { 958 959 Set<String> claimNames = customClaims.get(value); 960 961 if (claimNames == null || claimNames.isEmpty()) { 962 continue; // skip 963 } 964 965 entries = new HashSet<>(); 966 967 for (String claimName: claimNames) { 968 entries.add(new ClaimsRequest.Entry(claimName, ClaimRequirement.VOLUNTARY)); 969 } 970 971 } else { 972 973 continue; // skip 974 } 975 976 for (ClaimsRequest.Entry en: entries) { 977 978 if (switchToIDToken) 979 claimsRequest.addIDTokenClaim(en); 980 else 981 claimsRequest.addUserInfoClaim(en); 982 } 983 } 984 985 return claimsRequest; 986 } 987 988 989 /** 990 * Resolves the merged claims request from the specified OpenID 991 * authentication request parameters. The scope values that are 992 * {@link OIDCScopeValue standard OpenID scope values} are resolved to 993 * their respective individual claims requests, any other scope values 994 * are ignored. 995 * 996 * @param responseType The response type. Must not be {@code null}. 997 * @param scope The scope, {@code null} if not specified (for a 998 * plain OAuth 2.0 authorisation request with no 999 * scope explicitly specified). 1000 * @param claimsRequest The claims request, corresponding to the 1001 * optional {@code claims} OpenID Connect 1002 * authorisation request parameter, {@code null} 1003 * if not specified. 1004 * 1005 * @return The merged claims request. 1006 */ 1007 public static ClaimsRequest resolve(final ResponseType responseType, 1008 final Scope scope, 1009 final ClaimsRequest claimsRequest) { 1010 1011 return resolve(responseType, scope, claimsRequest, Collections.<Scope.Value,Set<String>>emptyMap()); 1012 } 1013 1014 1015 /** 1016 * Resolves the merged claims request from the specified OpenID 1017 * authentication request parameters. The scope values that are 1018 * {@link OIDCScopeValue standard OpenID scope values} are resolved to 1019 * their respective individual claims requests, any other scope values 1020 * are checked in the specified custom claims map and resolved 1021 * accordingly. 1022 * 1023 * @param responseType The response type. Must not be {@code null}. 1024 * @param scope The scope, {@code null} if not specified (for a 1025 * plain OAuth 2.0 authorisation request with no 1026 * scope explicitly specified). 1027 * @param claimsRequest The claims request, corresponding to the 1028 * optional {@code claims} OpenID Connect 1029 * authorisation request parameter, {@code null} 1030 * if not specified. 1031 * @param customClaims Custom scope to claim name map, {@code null} if 1032 * not specified. 1033 * 1034 * @return The merged claims request. 1035 */ 1036 public static ClaimsRequest resolve(final ResponseType responseType, 1037 final Scope scope, 1038 final ClaimsRequest claimsRequest, 1039 final Map<Scope.Value,Set<String>> customClaims) { 1040 1041 ClaimsRequest mergedClaimsRequest = resolve(responseType, scope, customClaims); 1042 1043 mergedClaimsRequest.add(claimsRequest); 1044 1045 return mergedClaimsRequest; 1046 } 1047 1048 1049 /** 1050 * Resolves the merged claims request for the specified OpenID 1051 * authentication request. The scope values that are 1052 * {@link OIDCScopeValue standard OpenID scope values} are resolved to 1053 * their respective individual claims requests, any other scope values 1054 * are ignored. 1055 * 1056 * @param authRequest The OpenID authentication request. Must not be 1057 * {@code null}. 1058 * 1059 * @return The merged claims request. 1060 */ 1061 public static ClaimsRequest resolve(final AuthenticationRequest authRequest) { 1062 1063 return resolve(authRequest.getResponseType(), authRequest.getScope(), authRequest.getClaims()); 1064 } 1065 1066 1067 /** 1068 * Parses a claims request from the specified JSON object 1069 * representation. Unexpected members in the JSON object are silently 1070 * ignored. 1071 * 1072 * @param jsonObject The JSON object to parse. Must not be 1073 * {@code null}. 1074 * 1075 * @return The claims request. 1076 */ 1077 public static ClaimsRequest parse(final JSONObject jsonObject) { 1078 1079 ClaimsRequest claimsRequest = new ClaimsRequest(); 1080 1081 try { 1082 if (jsonObject.containsKey("id_token")) { 1083 1084 JSONObject idTokenObject = (JSONObject)jsonObject.get("id_token"); 1085 1086 Collection<Entry> idTokenClaims = Entry.parseEntries(idTokenObject); 1087 1088 for (Entry entry: idTokenClaims) 1089 claimsRequest.addIDTokenClaim(entry); 1090 } 1091 1092 1093 if (jsonObject.containsKey("userinfo")) { 1094 1095 JSONObject userInfoObject = (JSONObject)jsonObject.get("userinfo"); 1096 1097 Collection<Entry> userInfoClaims = Entry.parseEntries(userInfoObject); 1098 1099 for (Entry entry: userInfoClaims) 1100 claimsRequest.addUserInfoClaim(entry); 1101 } 1102 1103 } catch (Exception e) { 1104 1105 // Ignore 1106 } 1107 1108 return claimsRequest; 1109 } 1110 1111 1112 /** 1113 * Parses a claims request from the specified JSON object string 1114 * representation. Unexpected members in the JSON object are silently 1115 * ignored. 1116 * 1117 * @param json The JSON object string to parse. Must not be 1118 * {@code null}. 1119 * 1120 * @return The claims request. 1121 * 1122 * @throws ParseException If the string couldn't be parsed to a valid 1123 * JSON object. 1124 */ 1125 public static ClaimsRequest parse(final String json) 1126 throws ParseException { 1127 1128 return parse(JSONObjectUtils.parse(json)); 1129 } 1130}