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.oauth2.sdk.client; 019 020 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.util.*; 024import javax.mail.internet.AddressException; 025import javax.mail.internet.InternetAddress; 026 027import com.nimbusds.jose.JWSAlgorithm; 028import com.nimbusds.jose.jwk.JWKSet; 029import com.nimbusds.langtag.LangTag; 030import com.nimbusds.langtag.LangTagUtils; 031import com.nimbusds.oauth2.sdk.GrantType; 032import com.nimbusds.oauth2.sdk.ParseException; 033import com.nimbusds.oauth2.sdk.ResponseType; 034import com.nimbusds.oauth2.sdk.Scope; 035import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; 036import com.nimbusds.oauth2.sdk.id.SoftwareID; 037import com.nimbusds.oauth2.sdk.id.SoftwareVersion; 038import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 039import net.minidev.json.JSONArray; 040import net.minidev.json.JSONObject; 041 042 043/** 044 * Client metadata. 045 * 046 * <p>Example client metadata, serialised to a JSON object: 047 * 048 * <pre> 049 * { 050 * "redirect_uris" : ["https://client.example.org/callback", 051 * "https://client.example.org/callback2"], 052 * "client_name" : "My Example Client", 053 * "client_name#ja-Jpan-JP" : "\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D", 054 * "token_endpoint_auth_method" : "client_secret_basic", 055 * "scope" : "read write dolphin", 056 * "logo_uri" : "https://client.example.org/logo.png", 057 * "jwks_uri" : "https://client.example.org/my_public_keys.jwks" 058 * } 059 * </pre> 060 * 061 * <p>Related specifications: 062 * 063 * <ul> 064 * <li>OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591), section 065 * 2. 066 * <li>Mutual TLS Profile for OAuth 2.0 (draft-ietf-oauth-mtls-03), section 067 * 2.3. 068 * </ul> 069 */ 070public class ClientMetadata { 071 072 073 /** 074 * The registered parameter names. 075 */ 076 private static final Set<String> REGISTERED_PARAMETER_NAMES; 077 078 079 static { 080 Set<String> p = new HashSet<>(); 081 082 p.add("redirect_uris"); 083 p.add("scope"); 084 p.add("response_types"); 085 p.add("grant_types"); 086 p.add("contacts"); 087 p.add("client_name"); 088 p.add("logo_uri"); 089 p.add("client_uri"); 090 p.add("policy_uri"); 091 p.add("tos_uri"); 092 p.add("token_endpoint_auth_method"); 093 p.add("token_endpoint_auth_signing_alg"); 094 p.add("jwks_uri"); 095 p.add("jwks"); 096 p.add("software_id"); 097 p.add("software_version"); 098 p.add("mutual_tls_sender_constrained_access_tokens"); 099 p.add("tls_client_auth_subject_dn"); 100 p.add("tls_client_auth_root_dn"); 101 102 REGISTERED_PARAMETER_NAMES = Collections.unmodifiableSet(p); 103 } 104 105 106 /** 107 * Redirect URIs. 108 */ 109 private Set<URI> redirectURIs; 110 111 112 /** 113 * The client OAuth 2.0 scope. 114 */ 115 private Scope scope; 116 117 118 /** 119 * The expected OAuth 2.0 response types. 120 */ 121 private Set<ResponseType> responseTypes; 122 123 124 /** 125 * The expected OAuth 2.0 grant types. 126 */ 127 private Set<GrantType> grantTypes; 128 129 130 /** 131 * Administrator email contacts for the client. 132 */ 133 private List<String> contacts; 134 135 136 /** 137 * The client name. 138 */ 139 private final Map<LangTag,String> nameEntries; 140 141 142 /** 143 * The client application logo. 144 */ 145 private final Map<LangTag,URI> logoURIEntries; 146 147 148 /** 149 * The client URI entries. 150 */ 151 private final Map<LangTag,URI> uriEntries; 152 153 154 /** 155 * The client policy for use of end-user data. 156 */ 157 private Map<LangTag,URI> policyURIEntries; 158 159 160 /** 161 * The client terms of service. 162 */ 163 private final Map<LangTag,URI> tosURIEntries; 164 165 166 /** 167 * Token endpoint authentication method. 168 */ 169 private ClientAuthenticationMethod authMethod; 170 171 172 /** 173 * The JSON Web Signature (JWS) algorithm required for 174 * {@code private_key_jwt} and {@code client_secret_jwt} 175 * authentication at the Token endpoint. 176 */ 177 private JWSAlgorithm authJWSAlg; 178 179 180 /** 181 * URI for this client's JSON Web Key (JWK) set containing key(s) that 182 * are used in signing requests to the server and key(s) for encrypting 183 * responses. 184 */ 185 private URI jwkSetURI; 186 187 188 /** 189 * Client's JSON Web Key (JWK) set containing key(s) that are used in 190 * signing requests to the server and key(s) for encrypting responses. 191 * Intended as an alternative to {@link #jwkSetURI} for native clients. 192 */ 193 private JWKSet jwkSet; 194 195 196 /** 197 * Identifier for the OAuth 2.0 client software. 198 */ 199 private SoftwareID softwareID; 200 201 202 /** 203 * Version identifier for the OAuth 2.0 client software. 204 */ 205 private SoftwareVersion softwareVersion; 206 207 208 /** 209 * Preference for mutual TLS sender constrained access tokens. 210 */ 211 private boolean mutualTLSSenderConstrainedAccessTokens = false; 212 213 214 /** 215 * The expected subject distinguished name (DN) of the client X.509 216 * certificate the in mutual TLS authentication. 217 */ 218 private String tlsClientAuthSubjectDN = null; 219 220 221 /** 222 * The expected distinguished name (DN) of the root issuer of the X.509 223 * client certificate in mutual TLS authentication. 224 */ 225 private String tlsClientAuthRootDN = null; 226 227 228 /** 229 * The custom metadata fields. 230 */ 231 private JSONObject customFields; 232 233 234 /** 235 * Creates a new OAuth 2.0 client metadata instance. 236 */ 237 public ClientMetadata() { 238 239 nameEntries = new HashMap<>(); 240 logoURIEntries = new HashMap<>(); 241 uriEntries = new HashMap<>(); 242 policyURIEntries = new HashMap<>(); 243 policyURIEntries = new HashMap<>(); 244 tosURIEntries = new HashMap<>(); 245 customFields = new JSONObject(); 246 } 247 248 249 /** 250 * Creates a shallow copy of the specified OAuth 2.0 client metadata 251 * instance. 252 * 253 * @param metadata The client metadata to copy. Must not be 254 * {@code null}. 255 */ 256 public ClientMetadata(final ClientMetadata metadata) { 257 258 redirectURIs = metadata.redirectURIs; 259 scope = metadata.scope; 260 responseTypes = metadata.responseTypes; 261 grantTypes = metadata.grantTypes; 262 contacts = metadata.contacts; 263 nameEntries = metadata.nameEntries; 264 logoURIEntries = metadata.logoURIEntries; 265 uriEntries = metadata.uriEntries; 266 policyURIEntries = metadata.policyURIEntries; 267 tosURIEntries = metadata.tosURIEntries; 268 authMethod = metadata.authMethod; 269 authJWSAlg = metadata.authJWSAlg; 270 jwkSetURI = metadata.jwkSetURI; 271 jwkSet = metadata.getJWKSet(); 272 softwareID = metadata.softwareID; 273 softwareVersion = metadata.softwareVersion; 274 mutualTLSSenderConstrainedAccessTokens = metadata.mutualTLSSenderConstrainedAccessTokens; 275 tlsClientAuthSubjectDN = metadata.tlsClientAuthSubjectDN; 276 tlsClientAuthRootDN = metadata.tlsClientAuthRootDN; 277 customFields = metadata.customFields; 278 } 279 280 281 /** 282 * Gets the registered (standard) OAuth 2.0 client metadata parameter 283 * names. 284 * 285 * @return The registered parameter names, as an unmodifiable set. 286 */ 287 public static Set<String> getRegisteredParameterNames() { 288 289 return REGISTERED_PARAMETER_NAMES; 290 } 291 292 293 /** 294 * Gets the redirection URIs for this client. Corresponds to the 295 * {@code redirect_uris} client metadata field. 296 * 297 * @return The redirection URIs, {@code null} if not specified. 298 */ 299 public Set<URI> getRedirectionURIs() { 300 301 return redirectURIs; 302 } 303 304 305 /** 306 * Gets the redirection URIs for this client as strings. Corresponds to 307 * the {@code redirect_uris} client metadata field. 308 * 309 * <p>This short-hand method is intended to enable string-based URI 310 * comparison. 311 * 312 * @return The redirection URIs as strings, {@code null} if not 313 * specified. 314 */ 315 public Set<String> getRedirectionURIStrings() { 316 317 if (redirectURIs == null) 318 return null; 319 320 Set<String> uriStrings = new HashSet<>(); 321 322 for (URI uri: redirectURIs) 323 uriStrings.add(uri.toString()); 324 325 return uriStrings; 326 } 327 328 329 /** 330 * Sets the redirection URIs for this client. Corresponds to the 331 * {@code redirect_uris} client metadata field. 332 * 333 * @param redirectURIs The redirection URIs, {@code null} if not 334 * specified. Valid redirection URIs must not 335 * contain a fragment. 336 */ 337 public void setRedirectionURIs(final Set<URI> redirectURIs) { 338 339 if (redirectURIs != null) { 340 // check URIs 341 for (URI uri: redirectURIs) { 342 if (uri == null) { 343 throw new IllegalArgumentException("The redirect_uri must not be null"); 344 } 345 if (uri.getFragment() != null) { 346 throw new IllegalArgumentException("The redirect_uri must not contain fragment"); 347 } 348 } 349 this.redirectURIs = redirectURIs; 350 } else { 351 this.redirectURIs = null; 352 } 353 } 354 355 356 /** 357 * Sets a single redirection URI for this client. Corresponds to the 358 * {@code redirect_uris} client metadata field. 359 * 360 * @param redirectURI The redirection URIs, {@code null} if not 361 * specified. A valid redirection URI must not 362 * contain a fragment. 363 */ 364 public void setRedirectionURI(final URI redirectURI) { 365 366 setRedirectionURIs(redirectURI != null ? Collections.singleton(redirectURI) : null); 367 } 368 369 370 /** 371 * Gets the scope values that the client can use when requesting access 372 * tokens. Corresponds to the {@code scope} client metadata field. 373 * 374 * @return The scope, {@code null} if not specified. 375 */ 376 public Scope getScope() { 377 378 return scope; 379 } 380 381 382 /** 383 * Checks if the scope matadata field is set and contains the specified 384 * scope value. 385 * 386 * @param scopeValue The scope value. Must not be {@code null}. 387 * 388 * @return {@code true} if the scope value is contained, else 389 * {@code false}. 390 */ 391 public boolean hasScopeValue(final Scope.Value scopeValue) { 392 393 return scope != null && scope.contains(scopeValue); 394 } 395 396 397 /** 398 * Sets the scope values that the client can use when requesting access 399 * tokens. Corresponds to the {@code scope} client metadata field. 400 * 401 * @param scope The scope, {@code null} if not specified. 402 */ 403 public void setScope(final Scope scope) { 404 405 this.scope = scope; 406 } 407 408 409 /** 410 * Gets the expected OAuth 2.0 response types. Corresponds to the 411 * {@code response_types} client metadata field. 412 * 413 * @return The response types, {@code null} if not specified. 414 */ 415 public Set<ResponseType> getResponseTypes() { 416 417 return responseTypes; 418 } 419 420 421 /** 422 * Sets the expected OAuth 2.0 response types. Corresponds to the 423 * {@code response_types} client metadata field. 424 * 425 * @param responseTypes The response types, {@code null} if not 426 * specified. 427 */ 428 public void setResponseTypes(final Set<ResponseType> responseTypes) { 429 430 this.responseTypes = responseTypes; 431 } 432 433 434 /** 435 * Gets the expected OAuth 2.0 grant types. Corresponds to the 436 * {@code grant_types} client metadata field. 437 * 438 * @return The grant types, {@code null} if not specified. 439 */ 440 public Set<GrantType> getGrantTypes() { 441 442 return grantTypes; 443 } 444 445 446 /** 447 * Sets the expected OAuth 2.0 grant types. Corresponds to the 448 * {@code grant_types} client metadata field. 449 * 450 * @param grantTypes The grant types, {@code null} if not specified. 451 */ 452 public void setGrantTypes(final Set<GrantType> grantTypes) { 453 454 this.grantTypes = grantTypes; 455 } 456 457 458 /** 459 * Gets the administrator email contacts for the client. Corresponds to 460 * the {@code contacts} client metadata field. 461 * 462 * <p>Use {@link #getEmailContacts()} instead. 463 * 464 * @return The administrator email contacts, {@code null} if not 465 * specified. 466 */ 467 @Deprecated 468 public List<InternetAddress> getContacts() { 469 470 if (contacts == null) 471 return null; 472 473 List<InternetAddress> addresses = new LinkedList<>(); 474 for (String s: contacts) { 475 if (s == null) continue; 476 try { 477 addresses.add(new InternetAddress(s, false)); 478 } catch (AddressException e) { 479 // ignore 480 } 481 } 482 return addresses; 483 } 484 485 486 /** 487 * Sets the administrator email contacts for the client. Corresponds to 488 * the {@code contacts} client metadata field. 489 * 490 * <p>Use {@link #setEmailContacts(List)} instead. 491 * 492 * @param contacts The administrator email contacts, {@code null} if 493 * not specified. 494 */ 495 @Deprecated 496 public void setContacts(final List<InternetAddress> contacts) { 497 498 if (contacts == null) { 499 this.contacts = null; 500 return; 501 } 502 503 List<String> addresses = new LinkedList<>(); 504 for (InternetAddress a: contacts) { 505 if (a != null) { 506 addresses.add(a.toString()); 507 } 508 } 509 this.contacts = addresses; 510 } 511 512 513 /** 514 * Gets the administrator email contacts for the client. Corresponds to 515 * the {@code contacts} client metadata field. 516 * 517 * @return The administrator email contacts, {@code null} if not 518 * specified. 519 */ 520 public List<String> getEmailContacts() { 521 522 return contacts; 523 } 524 525 526 /** 527 * Sets the administrator email contacts for the client. Corresponds to 528 * the {@code contacts} client metadata field. 529 * 530 * @param contacts The administrator email contacts, {@code null} if 531 * not specified. 532 */ 533 public void setEmailContacts(final List<String> contacts) { 534 535 this.contacts = contacts; 536 } 537 538 539 /** 540 * Gets the client name. Corresponds to the {@code client_name} client 541 * metadata field, with no language tag. 542 * 543 * @return The client name, {@code null} if not specified. 544 */ 545 public String getName() { 546 547 return getName(null); 548 } 549 550 551 /** 552 * Gets the client name. Corresponds to the {@code client_name} client 553 * metadata field, with an optional language tag. 554 * 555 * @param langTag The language tag of the entry, {@code null} to get 556 * the non-tagged entry. 557 * 558 * @return The client name, {@code null} if not specified. 559 */ 560 public String getName(final LangTag langTag) { 561 562 return nameEntries.get(langTag); 563 } 564 565 566 /** 567 * Gets the client name entries. Corresponds to the {@code client_name} 568 * client metadata field. 569 * 570 * @return The client name entries, empty map if none. 571 */ 572 public Map<LangTag,String> getNameEntries() { 573 574 return nameEntries; 575 } 576 577 578 /** 579 * Sets the client name. Corresponds to the {@code client_name} client 580 * metadata field, with no language tag. 581 * 582 * @param name The client name, {@code null} if not specified. 583 */ 584 public void setName(final String name) { 585 586 nameEntries.put(null, name); 587 } 588 589 590 /** 591 * Sets the client name. Corresponds to the {@code client_name} client 592 * metadata field, with an optional language tag. 593 * 594 * @param name The client name. Must not be {@code null}. 595 * @param langTag The language tag, {@code null} if not specified. 596 */ 597 public void setName(final String name, final LangTag langTag) { 598 599 nameEntries.put(langTag, name); 600 } 601 602 603 /** 604 * Gets the client application logo. Corresponds to the 605 * {@code logo_uri} client metadata field, with no language 606 * tag. 607 * 608 * @return The logo URI, {@code null} if not specified. 609 */ 610 public URI getLogoURI() { 611 612 return getLogoURI(null); 613 } 614 615 616 /** 617 * Gets the client application logo. Corresponds to the 618 * {@code logo_uri} client metadata field, with an optional 619 * language tag. 620 * 621 * @return The logo URI, {@code null} if not specified. 622 */ 623 public URI getLogoURI(final LangTag langTag) { 624 625 return logoURIEntries.get(langTag); 626 } 627 628 629 /** 630 * Gets the client application logo entries. Corresponds to the 631 * {@code logo_uri} client metadata field. 632 * 633 * @return The logo URI entries, empty map if none. 634 */ 635 public Map<LangTag,URI> getLogoURIEntries() { 636 637 return logoURIEntries; 638 } 639 640 641 /** 642 * Sets the client application logo. Corresponds to the 643 * {@code logo_uri} client metadata field, with no language 644 * tag. 645 * 646 * @param logoURI The logo URI, {@code null} if not specified. 647 */ 648 public void setLogoURI(final URI logoURI) { 649 650 logoURIEntries.put(null, logoURI); 651 } 652 653 654 /** 655 * Sets the client application logo. Corresponds to the 656 * {@code logo_uri} client metadata field, with an optional 657 * language tag. 658 * 659 * @param logoURI The logo URI. Must not be {@code null}. 660 * @param langTag The language tag, {@code null} if not specified. 661 */ 662 public void setLogoURI(final URI logoURI, final LangTag langTag) { 663 664 logoURIEntries.put(langTag, logoURI); 665 } 666 667 668 /** 669 * Gets the client home page. Corresponds to the {@code client_uri} 670 * client metadata field, with no language tag. 671 * 672 * @return The client URI, {@code null} if not specified. 673 */ 674 public URI getURI() { 675 676 return getURI(null); 677 } 678 679 680 /** 681 * Gets the client home page. Corresponds to the {@code client_uri} 682 * client metadata field, with an optional language tag. 683 * 684 * @return The client URI, {@code null} if not specified. 685 */ 686 public URI getURI(final LangTag langTag) { 687 688 return uriEntries.get(langTag); 689 } 690 691 692 /** 693 * Gets the client home page entries. Corresponds to the 694 * {@code client_uri} client metadata field. 695 * 696 * @return The client URI entries, empty map if none. 697 */ 698 public Map<LangTag,URI> getURIEntries() { 699 700 return uriEntries; 701 } 702 703 704 /** 705 * Sets the client home page. Corresponds to the {@code client_uri} 706 * client metadata field, with no language tag. 707 * 708 * @param uri The client URI, {@code null} if not specified. 709 */ 710 public void setURI(final URI uri) { 711 712 uriEntries.put(null, uri); 713 } 714 715 716 /** 717 * Sets the client home page. Corresponds to the {@code client_uri} 718 * client metadata field, with an optional language tag. 719 * 720 * @param uri The URI. Must not be {@code null}. 721 * @param langTag The language tag, {@code null} if not specified. 722 */ 723 public void setURI(final URI uri, final LangTag langTag) { 724 725 uriEntries.put(langTag, uri); 726 } 727 728 729 /** 730 * Gets the client policy for use of end-user data. Corresponds to the 731 * {@code policy_uri} client metadata field, with no language 732 * tag. 733 * 734 * @return The policy URI, {@code null} if not specified. 735 */ 736 public URI getPolicyURI() { 737 738 return getPolicyURI(null); 739 } 740 741 742 /** 743 * Gets the client policy for use of end-user data. Corresponds to the 744 * {@code policy_uri} client metadata field, with an optional 745 * language tag. 746 * 747 * @return The policy URI, {@code null} if not specified. 748 */ 749 public URI getPolicyURI(final LangTag langTag) { 750 751 return policyURIEntries.get(langTag); 752 } 753 754 755 /** 756 * Gets the client policy entries for use of end-user data. 757 * Corresponds to the {@code policy_uri} client metadata field. 758 * 759 * @return The policy URI entries, empty map if none. 760 */ 761 public Map<LangTag,URI> getPolicyURIEntries() { 762 763 return policyURIEntries; 764 } 765 766 767 /** 768 * Sets the client policy for use of end-user data. Corresponds to the 769 * {@code policy_uri} client metadata field, with no language 770 * tag. 771 * 772 * @param policyURI The policy URI, {@code null} if not specified. 773 */ 774 public void setPolicyURI(final URI policyURI) { 775 776 policyURIEntries.put(null, policyURI); 777 } 778 779 780 /** 781 * Sets the client policy for use of end-user data. Corresponds to the 782 * {@code policy_uri} client metadata field, with an optional 783 * language tag. 784 * 785 * @param policyURI The policy URI. Must not be {@code null}. 786 * @param langTag The language tag, {@code null} if not specified. 787 */ 788 public void setPolicyURI(final URI policyURI, final LangTag langTag) { 789 790 policyURIEntries.put(langTag, policyURI); 791 } 792 793 794 /** 795 * Gets the client's terms of service. Corresponds to the 796 * {@code tos_uri} client metadata field, with no language 797 * tag. 798 * 799 * @return The terms of service URI, {@code null} if not specified. 800 */ 801 public URI getTermsOfServiceURI() { 802 803 return getTermsOfServiceURI(null); 804 } 805 806 807 /** 808 * Gets the client's terms of service. Corresponds to the 809 * {@code tos_uri} client metadata field, with an optional 810 * language tag. 811 * 812 * @return The terms of service URI, {@code null} if not specified. 813 */ 814 public URI getTermsOfServiceURI(final LangTag langTag) { 815 816 return tosURIEntries.get(langTag); 817 } 818 819 820 /** 821 * Gets the client's terms of service entries. Corresponds to the 822 * {@code tos_uri} client metadata field. 823 * 824 * @return The terms of service URI entries, empty map if none. 825 */ 826 public Map<LangTag,URI> getTermsOfServiceURIEntries() { 827 828 return tosURIEntries; 829 } 830 831 832 /** 833 * Sets the client's terms of service. Corresponds to the 834 * {@code tos_uri} client metadata field, with no language 835 * tag. 836 * 837 * @param tosURI The terms of service URI, {@code null} if not 838 * specified. 839 */ 840 public void setTermsOfServiceURI(final URI tosURI) { 841 842 tosURIEntries.put(null, tosURI); 843 } 844 845 846 /** 847 * Sets the client's terms of service. Corresponds to the 848 * {@code tos_uri} client metadata field, with an optional 849 * language tag. 850 * 851 * @param tosURI The terms of service URI. Must not be {@code null}. 852 * @param langTag The language tag, {@code null} if not specified. 853 */ 854 public void setTermsOfServiceURI(final URI tosURI, final LangTag langTag) { 855 856 tosURIEntries.put(langTag, tosURI); 857 } 858 859 860 /** 861 * Gets the Token endpoint authentication method. Corresponds to the 862 * {@code token_endpoint_auth_method} client metadata field. 863 * 864 * @return The Token endpoint authentication method, {@code null} if 865 * not specified. 866 */ 867 public ClientAuthenticationMethod getTokenEndpointAuthMethod() { 868 869 return authMethod; 870 } 871 872 873 /** 874 * Sets the Token endpoint authentication method. Corresponds to the 875 * {@code token_endpoint_auth_method} client metadata field. 876 * 877 * @param authMethod The Token endpoint authentication method, 878 * {@code null} if not specified. 879 */ 880 public void setTokenEndpointAuthMethod(final ClientAuthenticationMethod authMethod) { 881 882 this.authMethod = authMethod; 883 } 884 885 886 /** 887 * Gets the JSON Web Signature (JWS) algorithm required for 888 * {@code private_key_jwt} and {@code client_secret_jwt} 889 * authentication at the Token endpoint. Corresponds to the 890 * {@code token_endpoint_auth_signing_alg} client metadata field. 891 * 892 * @return The JWS algorithm, {@code null} if not specified. 893 */ 894 public JWSAlgorithm getTokenEndpointAuthJWSAlg() { 895 896 return authJWSAlg; 897 } 898 899 900 /** 901 * Sets the JSON Web Signature (JWS) algorithm required for 902 * {@code private_key_jwt} and {@code client_secret_jwt} 903 * authentication at the Token endpoint. Corresponds to the 904 * {@code token_endpoint_auth_signing_alg} client metadata field. 905 * 906 * @param authJWSAlg The JWS algorithm, {@code null} if not specified. 907 */ 908 public void setTokenEndpointAuthJWSAlg(final JWSAlgorithm authJWSAlg) { 909 910 this.authJWSAlg = authJWSAlg; 911 } 912 913 914 /** 915 * Gets the URI for this client's JSON Web Key (JWK) set containing 916 * key(s) that are used in signing requests to the server and key(s) 917 * for encrypting responses. Corresponds to the {@code jwks_uri} client 918 * metadata field. 919 * 920 * @return The JWK set URI, {@code null} if not specified. 921 */ 922 public URI getJWKSetURI() { 923 924 return jwkSetURI; 925 } 926 927 928 /** 929 * Sets the URI for this client's JSON Web Key (JWK) set containing 930 * key(s) that are used in signing requests to the server and key(s) 931 * for encrypting responses. Corresponds to the {@code jwks_uri} client 932 * metadata field. 933 * 934 * @param jwkSetURI The JWK set URI, {@code null} if not specified. 935 */ 936 public void setJWKSetURI(final URI jwkSetURI) { 937 938 this.jwkSetURI = jwkSetURI; 939 } 940 941 942 /** 943 * Gets this client's JSON Web Key (JWK) set containing key(s) that are 944 * used in signing requests to the server and key(s) for encrypting 945 * responses. Intended as an alternative to {@link #getJWKSetURI} for 946 * native clients. Corresponds to the {@code jwks} client metadata 947 * field. 948 * 949 * @return The JWK set, {@code null} if not specified. 950 */ 951 public JWKSet getJWKSet() { 952 953 return jwkSet; 954 } 955 956 957 /** 958 * Sets this client's JSON Web Key (JWK) set containing key(s) that are 959 * used in signing requests to the server and key(s) for encrypting 960 * responses. Intended as an alternative to {@link #getJWKSetURI} for 961 * native clients. Corresponds to the {@code jwks} client metadata 962 * field. 963 * 964 * @param jwkSet The JWK set, {@code null} if not specified. 965 */ 966 public void setJWKSet(final JWKSet jwkSet) { 967 968 this.jwkSet = jwkSet; 969 } 970 971 972 /** 973 * Gets the identifier for the OAuth 2.0 client software. Corresponds 974 * to the {@code software_id} client metadata field. 975 * 976 * @return The software identifier, {@code null} if not specified. 977 */ 978 public SoftwareID getSoftwareID() { 979 980 return softwareID; 981 } 982 983 984 /** 985 * Sets the identifier for the OAuth 2.0 client software. Corresponds 986 * to the {@code software_id} client metadata field. 987 * 988 * @param softwareID The software identifier, {@code null} if not 989 * specified. 990 */ 991 public void setSoftwareID(final SoftwareID softwareID) { 992 993 this.softwareID = softwareID; 994 } 995 996 997 /** 998 * Gets the version identifier for the OAuth 2.0 client software. 999 * Corresponds to the {@code software_version} client metadata field. 1000 * 1001 * @return The version identifier, {@code null} if not specified. 1002 */ 1003 public SoftwareVersion getSoftwareVersion() { 1004 1005 return softwareVersion; 1006 } 1007 1008 1009 /** 1010 * Sets the version identifier for the OAuth 2.0 client software. 1011 * Corresponds to the {@code software_version} client metadata field. 1012 * 1013 * @param softwareVersion The version identifier, {@code null} if not 1014 * specified. 1015 */ 1016 public void setSoftwareVersion(final SoftwareVersion softwareVersion) { 1017 1018 this.softwareVersion = softwareVersion; 1019 } 1020 1021 1022 /** 1023 * Sets the preference for mutual TLS sender constrained access tokens. 1024 * Corresponds to the 1025 * {@code mutual_tls_sender_constrained_access_tokens} client metadata 1026 * field. 1027 * 1028 * @return {@code true} indicates a preference for mutual TLS sender 1029 * constrained access tokens, {@code false} if none. 1030 */ 1031 public boolean getMutualTLSSenderConstrainedAccessTokens() { 1032 1033 return mutualTLSSenderConstrainedAccessTokens; 1034 } 1035 1036 1037 /** 1038 * Gets the preference for mutual TLS sender constrained access tokens. 1039 * Corresponds to the 1040 * {@code mutual_tls_sender_constrained_access_tokens} client metadata 1041 * field. 1042 * 1043 * @param tlsSenderAccessTokens {@code true} indicates a preference for 1044 * mutual TLS sender constrained access 1045 * tokens, {@code false} if none. 1046 */ 1047 public void setMutualTLSSenderConstrainedAccessTokens(final boolean tlsSenderAccessTokens) { 1048 1049 mutualTLSSenderConstrainedAccessTokens = tlsSenderAccessTokens; 1050 } 1051 1052 1053 /** 1054 * Gets the expected subject distinguished name (DN) of the client 1055 * X.509 certificate in mutual TLS authentication. 1056 * 1057 * @return The expected subject distinguished name (DN) of the client 1058 * X.509 certificate, {@code null} if not specified. 1059 */ 1060 public String getTLSClientAuthSubjectDN() { 1061 1062 return tlsClientAuthSubjectDN; 1063 } 1064 1065 1066 /** 1067 * Sets the expected subject distinguished name (DN) of the client 1068 * X.509 certificate in mutual TLS authentication. 1069 * 1070 * @param subjectDN The expected subject distinguished name (DN) of the 1071 * client X.509 certificate, {@code null} if not 1072 * specified. 1073 */ 1074 public void setTLSClientAuthSubjectDN(final String subjectDN) { 1075 1076 this.tlsClientAuthSubjectDN = subjectDN; 1077 } 1078 1079 1080 /** 1081 * Gets the expected distinguished name (DN) of the root issuer of the 1082 * X.509 client certificate in mutual TLS authentication. 1083 * 1084 * @return The expected distinguished name (DN) of the root issuer of 1085 * the X.509 client certificate, {@code null} if not specified. 1086 */ 1087 public String getTLSClientAuthRootDN() { 1088 1089 return tlsClientAuthRootDN; 1090 } 1091 1092 1093 /** 1094 * Sets the expected distinguished name (DN) of the root issuer of the 1095 * X.509 client certificate in mutual TLS authentication. 1096 * 1097 * @param rootDN The expected distinguished name (DN) of the root 1098 * issuer of the X.509 client certificate, {@code null} 1099 * if not specified. 1100 */ 1101 public void setTLSClientAuthRootDN(final String rootDN) { 1102 1103 this.tlsClientAuthRootDN = rootDN; 1104 } 1105 1106 1107 /** 1108 * Gets the specified custom metadata field. 1109 * 1110 * @param name The field name. Must not be {@code null}. 1111 * 1112 * @return The field value, typically serialisable to a JSON entity, 1113 * {@code null} if none. 1114 */ 1115 public Object getCustomField(final String name) { 1116 1117 return customFields.get(name); 1118 } 1119 1120 1121 /** 1122 * Gets the custom metadata fields. 1123 * 1124 * @return The custom metadata fields, as a JSON object, empty object 1125 * if none. 1126 */ 1127 public JSONObject getCustomFields() { 1128 1129 return customFields; 1130 } 1131 1132 1133 /** 1134 * Sets the specified custom metadata field. 1135 * 1136 * @param name The field name. Must not be {@code null}. 1137 * @param value The field value. Should serialise to a JSON entity. 1138 */ 1139 public void setCustomField(final String name, final Object value) { 1140 1141 customFields.put(name, value); 1142 } 1143 1144 1145 /** 1146 * Sets the custom metadata fields. 1147 * 1148 * @param customFields The custom metadata fields, as a JSON object, 1149 * empty object if none. Must not be {@code null}. 1150 */ 1151 public void setCustomFields(final JSONObject customFields) { 1152 1153 if (customFields == null) 1154 throw new IllegalArgumentException("The custom fields JSON object must not be null"); 1155 1156 this.customFields = customFields; 1157 } 1158 1159 1160 /** 1161 * Applies the client metadata defaults where no values have been 1162 * specified. 1163 * 1164 * <ul> 1165 * <li>The response types default to {@code ["code"]}. 1166 * <li>The grant types default to {@code ["authorization_code"]}. 1167 * <li>The client authentication method defaults to 1168 * "client_secret_basic", unless the grant type is "implicit" 1169 * only. 1170 * </ul> 1171 */ 1172 public void applyDefaults() { 1173 1174 if (responseTypes == null) { 1175 responseTypes = new HashSet<>(); 1176 responseTypes.add(ResponseType.getDefault()); 1177 } 1178 1179 if (grantTypes == null) { 1180 grantTypes = new HashSet<>(); 1181 grantTypes.add(GrantType.AUTHORIZATION_CODE); 1182 } 1183 1184 if (authMethod == null) { 1185 1186 if (grantTypes.contains(GrantType.IMPLICIT) && grantTypes.size() == 1) { 1187 authMethod = ClientAuthenticationMethod.NONE; 1188 } else { 1189 authMethod = ClientAuthenticationMethod.getDefault(); 1190 } 1191 } 1192 } 1193 1194 1195 /** 1196 * Returns the JSON object representation of this client metadata, 1197 * including any custom fields. 1198 * 1199 * @return The JSON object. 1200 */ 1201 public JSONObject toJSONObject() { 1202 1203 return toJSONObject(true); 1204 } 1205 1206 1207 /** 1208 * Returns the JSON object representation of this client metadata. 1209 * 1210 * @param includeCustomFields {@code true} to include any custom 1211 * metadata fields, {@code false} to omit 1212 * them. 1213 * 1214 * @return The JSON object. 1215 */ 1216 public JSONObject toJSONObject(final boolean includeCustomFields) { 1217 1218 JSONObject o; 1219 1220 if (includeCustomFields) 1221 o = new JSONObject(customFields); 1222 else 1223 o = new JSONObject(); 1224 1225 1226 if (redirectURIs != null) { 1227 1228 JSONArray uriList = new JSONArray(); 1229 1230 for (URI uri: redirectURIs) 1231 uriList.add(uri.toString()); 1232 1233 o.put("redirect_uris", uriList); 1234 } 1235 1236 1237 if (scope != null) 1238 o.put("scope", scope.toString()); 1239 1240 1241 if (responseTypes != null) { 1242 1243 JSONArray rtList = new JSONArray(); 1244 1245 for (ResponseType rt: responseTypes) 1246 rtList.add(rt.toString()); 1247 1248 o.put("response_types", rtList); 1249 } 1250 1251 1252 if (grantTypes != null) { 1253 1254 JSONArray grantList = new JSONArray(); 1255 1256 for (GrantType grant: grantTypes) 1257 grantList.add(grant.toString()); 1258 1259 o.put("grant_types", grantList); 1260 } 1261 1262 1263 if (contacts != null) { 1264 o.put("contacts", contacts); 1265 } 1266 1267 1268 if (! nameEntries.isEmpty()) { 1269 1270 for (Map.Entry<LangTag,String> entry: nameEntries.entrySet()) { 1271 1272 LangTag langTag = entry.getKey(); 1273 String name = entry.getValue(); 1274 1275 if (name == null) 1276 continue; 1277 1278 if (langTag == null) 1279 o.put("client_name", entry.getValue()); 1280 else 1281 o.put("client_name#" + langTag, entry.getValue()); 1282 } 1283 } 1284 1285 1286 if (! logoURIEntries.isEmpty()) { 1287 1288 for (Map.Entry<LangTag,URI> entry: logoURIEntries.entrySet()) { 1289 1290 LangTag langTag = entry.getKey(); 1291 URI uri = entry.getValue(); 1292 1293 if (uri == null) 1294 continue; 1295 1296 if (langTag == null) 1297 o.put("logo_uri", entry.getValue().toString()); 1298 else 1299 o.put("logo_uri#" + langTag, entry.getValue().toString()); 1300 } 1301 } 1302 1303 1304 if (! uriEntries.isEmpty()) { 1305 1306 for (Map.Entry<LangTag,URI> entry: uriEntries.entrySet()) { 1307 1308 LangTag langTag = entry.getKey(); 1309 URI uri = entry.getValue(); 1310 1311 if (uri == null) 1312 continue; 1313 1314 if (langTag == null) 1315 o.put("client_uri", entry.getValue().toString()); 1316 else 1317 o.put("client_uri#" + langTag, entry.getValue().toString()); 1318 } 1319 } 1320 1321 1322 if (! policyURIEntries.isEmpty()) { 1323 1324 for (Map.Entry<LangTag,URI> entry: policyURIEntries.entrySet()) { 1325 1326 LangTag langTag = entry.getKey(); 1327 URI uri = entry.getValue(); 1328 1329 if (uri == null) 1330 continue; 1331 1332 if (langTag == null) 1333 o.put("policy_uri", entry.getValue().toString()); 1334 else 1335 o.put("policy_uri#" + langTag, entry.getValue().toString()); 1336 } 1337 } 1338 1339 1340 if (! tosURIEntries.isEmpty()) { 1341 1342 for (Map.Entry<LangTag,URI> entry: tosURIEntries.entrySet()) { 1343 1344 LangTag langTag = entry.getKey(); 1345 URI uri = entry.getValue(); 1346 1347 if (uri == null) 1348 continue; 1349 1350 if (langTag == null) 1351 o.put("tos_uri", entry.getValue().toString()); 1352 else 1353 o.put("tos_uri#" + langTag, entry.getValue().toString()); 1354 } 1355 } 1356 1357 1358 if (authMethod != null) 1359 o.put("token_endpoint_auth_method", authMethod.toString()); 1360 1361 1362 if (authJWSAlg != null) 1363 o.put("token_endpoint_auth_signing_alg", authJWSAlg.getName()); 1364 1365 1366 if (jwkSetURI != null) 1367 o.put("jwks_uri", jwkSetURI.toString()); 1368 1369 1370 if (jwkSet != null) 1371 o.put("jwks", jwkSet.toJSONObject(true)); // prevent private keys from leaking 1372 1373 1374 if (softwareID != null) 1375 o.put("software_id", softwareID.getValue()); 1376 1377 if (softwareVersion != null) 1378 o.put("software_version", softwareVersion.getValue()); 1379 1380 o.put("mutual_tls_sender_constrained_access_tokens", mutualTLSSenderConstrainedAccessTokens); 1381 1382 if (tlsClientAuthSubjectDN != null) { 1383 o.put("tls_client_auth_subject_dn", tlsClientAuthSubjectDN); 1384 1385 if (tlsClientAuthRootDN != null) { 1386 o.put("tls_client_auth_root_dn", tlsClientAuthRootDN); 1387 } 1388 } 1389 1390 return o; 1391 } 1392 1393 1394 /** 1395 * Parses an client metadata instance from the specified JSON object. 1396 * 1397 * @param jsonObject The JSON object to parse. Must not be 1398 * {@code null}. 1399 * 1400 * @return The client metadata. 1401 * 1402 * @throws ParseException If the JSON object couldn't be parsed to a 1403 * client metadata instance. 1404 */ 1405 public static ClientMetadata parse(final JSONObject jsonObject) 1406 throws ParseException { 1407 1408 // Copy JSON object, then parse 1409 return parseFromModifiableJSONObject(new JSONObject(jsonObject)); 1410 } 1411 1412 1413 /** 1414 * Parses an client metadata instance from the specified JSON object. 1415 * 1416 * @param jsonObject The JSON object to parse, will be modified by 1417 * the parse routine. Must not be {@code null}. 1418 * 1419 * @return The client metadata. 1420 * 1421 * @throws ParseException If the JSON object couldn't be parsed to a 1422 * client metadata instance. 1423 */ 1424 private static ClientMetadata parseFromModifiableJSONObject(final JSONObject jsonObject) 1425 throws ParseException { 1426 1427 ClientMetadata metadata = new ClientMetadata(); 1428 1429 if (jsonObject.get("redirect_uris") != null) { 1430 1431 Set<URI> redirectURIs = new LinkedHashSet<>(); 1432 1433 for (String uriString: JSONObjectUtils.getStringArray(jsonObject, "redirect_uris")) { 1434 URI uri; 1435 try { 1436 uri = new URI(uriString); 1437 } catch (URISyntaxException e) { 1438 throw new ParseException("Invalid \"redirect_uris\" parameter: " + e.getMessage(), RegistrationError.INVALID_REDIRECT_URI.appendDescription(": " + e.getMessage())); 1439 } 1440 1441 if (uri.getFragment() != null) { 1442 String detail = "URI must not contain fragment"; 1443 throw new ParseException("Invalid \"redirect_uris\" parameter: " + detail, RegistrationError.INVALID_REDIRECT_URI.appendDescription(": " + detail)); 1444 } 1445 1446 redirectURIs.add(uri); 1447 } 1448 1449 metadata.setRedirectionURIs(redirectURIs); 1450 jsonObject.remove("redirect_uris"); 1451 } 1452 1453 try { 1454 1455 if (jsonObject.get("scope") != null) { 1456 metadata.setScope(Scope.parse(JSONObjectUtils.getString(jsonObject, "scope"))); 1457 jsonObject.remove("scope"); 1458 } 1459 1460 1461 if (jsonObject.get("response_types") != null) { 1462 1463 Set<ResponseType> responseTypes = new LinkedHashSet<>(); 1464 1465 for (String rt : JSONObjectUtils.getStringArray(jsonObject, "response_types")) { 1466 1467 responseTypes.add(ResponseType.parse(rt)); 1468 } 1469 1470 metadata.setResponseTypes(responseTypes); 1471 jsonObject.remove("response_types"); 1472 } 1473 1474 1475 if (jsonObject.get("grant_types") != null) { 1476 1477 Set<GrantType> grantTypes = new LinkedHashSet<>(); 1478 1479 for (String grant : JSONObjectUtils.getStringArray(jsonObject, "grant_types")) { 1480 1481 grantTypes.add(GrantType.parse(grant)); 1482 } 1483 1484 metadata.setGrantTypes(grantTypes); 1485 jsonObject.remove("grant_types"); 1486 } 1487 1488 1489 if (jsonObject.get("contacts") != null) { 1490 metadata.setEmailContacts(JSONObjectUtils.getStringList(jsonObject, "contacts")); 1491 jsonObject.remove("contacts"); 1492 } 1493 1494 1495 // Find lang-tagged client_name params 1496 Map<LangTag, Object> matches = LangTagUtils.find("client_name", jsonObject); 1497 1498 for (Map.Entry<LangTag, Object> entry : matches.entrySet()) { 1499 1500 try { 1501 metadata.setName((String) entry.getValue(), entry.getKey()); 1502 1503 } catch (ClassCastException e) { 1504 1505 throw new ParseException("Invalid \"client_name\" (language tag) parameter"); 1506 } 1507 1508 removeMember(jsonObject, "client_name", entry.getKey()); 1509 } 1510 1511 1512 matches = LangTagUtils.find("logo_uri", jsonObject); 1513 1514 for (Map.Entry<LangTag, Object> entry : matches.entrySet()) { 1515 1516 if (entry.getValue() == null) continue; 1517 1518 try { 1519 metadata.setLogoURI(new URI((String) entry.getValue()), entry.getKey()); 1520 1521 } catch (Exception e) { 1522 1523 throw new ParseException("Invalid \"logo_uri\" (language tag) parameter"); 1524 } 1525 1526 removeMember(jsonObject, "logo_uri", entry.getKey()); 1527 } 1528 1529 1530 matches = LangTagUtils.find("client_uri", jsonObject); 1531 1532 for (Map.Entry<LangTag, Object> entry : matches.entrySet()) { 1533 1534 if (entry.getValue() == null) continue; 1535 1536 try { 1537 metadata.setURI(new URI((String) entry.getValue()), entry.getKey()); 1538 1539 1540 } catch (Exception e) { 1541 1542 throw new ParseException("Invalid \"client_uri\" (language tag) parameter"); 1543 } 1544 1545 removeMember(jsonObject, "client_uri", entry.getKey()); 1546 } 1547 1548 1549 matches = LangTagUtils.find("policy_uri", jsonObject); 1550 1551 for (Map.Entry<LangTag, Object> entry : matches.entrySet()) { 1552 1553 if (entry.getValue() == null) continue; 1554 1555 try { 1556 metadata.setPolicyURI(new URI((String) entry.getValue()), entry.getKey()); 1557 1558 } catch (Exception e) { 1559 1560 throw new ParseException("Invalid \"policy_uri\" (language tag) parameter"); 1561 } 1562 1563 removeMember(jsonObject, "policy_uri", entry.getKey()); 1564 } 1565 1566 1567 matches = LangTagUtils.find("tos_uri", jsonObject); 1568 1569 for (Map.Entry<LangTag, Object> entry : matches.entrySet()) { 1570 1571 if (entry.getValue() == null) continue; 1572 1573 try { 1574 metadata.setTermsOfServiceURI(new URI((String) entry.getValue()), entry.getKey()); 1575 1576 } catch (Exception e) { 1577 1578 throw new ParseException("Invalid \"tos_uri\" (language tag) parameter"); 1579 } 1580 1581 removeMember(jsonObject, "tos_uri", entry.getKey()); 1582 } 1583 1584 1585 if (jsonObject.get("token_endpoint_auth_method") != null) { 1586 metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.parse( 1587 JSONObjectUtils.getString(jsonObject, "token_endpoint_auth_method"))); 1588 1589 jsonObject.remove("token_endpoint_auth_method"); 1590 } 1591 1592 1593 if (jsonObject.get("token_endpoint_auth_signing_alg") != null) { 1594 metadata.setTokenEndpointAuthJWSAlg(JWSAlgorithm.parse( 1595 JSONObjectUtils.getString(jsonObject, "token_endpoint_auth_signing_alg"))); 1596 1597 jsonObject.remove("token_endpoint_auth_signing_alg"); 1598 } 1599 1600 1601 if (jsonObject.get("jwks_uri") != null) { 1602 metadata.setJWKSetURI(JSONObjectUtils.getURI(jsonObject, "jwks_uri")); 1603 jsonObject.remove("jwks_uri"); 1604 } 1605 1606 if (jsonObject.get("jwks") != null) { 1607 1608 try { 1609 metadata.setJWKSet(JWKSet.parse(JSONObjectUtils.getJSONObject(jsonObject, "jwks"))); 1610 1611 } catch (java.text.ParseException e) { 1612 throw new ParseException(e.getMessage(), e); 1613 } 1614 1615 jsonObject.remove("jwks"); 1616 } 1617 1618 if (jsonObject.get("software_id") != null) { 1619 metadata.setSoftwareID(new SoftwareID(JSONObjectUtils.getString(jsonObject, "software_id"))); 1620 jsonObject.remove("software_id"); 1621 } 1622 1623 if (jsonObject.get("software_version") != null) { 1624 metadata.setSoftwareVersion(new SoftwareVersion(JSONObjectUtils.getString(jsonObject, "software_version"))); 1625 jsonObject.remove("software_version"); 1626 } 1627 1628 if (jsonObject.get("mutual_tls_sender_constrained_access_tokens") != null) { 1629 metadata.setMutualTLSSenderConstrainedAccessTokens(JSONObjectUtils.getBoolean(jsonObject, "mutual_tls_sender_constrained_access_tokens")); 1630 jsonObject.remove("mutual_tls_sender_constrained_access_tokens"); 1631 } 1632 1633 if (jsonObject.get("tls_client_auth_subject_dn") != null) { 1634 metadata.setTLSClientAuthSubjectDN(JSONObjectUtils.getString(jsonObject, "tls_client_auth_subject_dn")); 1635 jsonObject.remove("tls_client_auth_subject_dn"); 1636 1637 if (jsonObject.get("tls_client_auth_root_dn") != null) { 1638 metadata.setTLSClientAuthRootDN(JSONObjectUtils.getString(jsonObject, "tls_client_auth_root_dn")); 1639 jsonObject.remove("tls_client_auth_root_dn"); 1640 } 1641 } 1642 1643 } catch (ParseException e) { 1644 // Insert client_client_metadata error code so that it 1645 // can be reported back to the client if we have a 1646 // registration event 1647 throw new ParseException(e.getMessage(), RegistrationError.INVALID_CLIENT_METADATA.appendDescription(": " + e.getMessage()), e.getCause()); 1648 } 1649 1650 // The remaining fields are custom 1651 metadata.customFields = jsonObject; 1652 1653 return metadata; 1654 } 1655 1656 1657 /** 1658 * Removes a JSON object member with the specified base name and 1659 * optional language tag. 1660 * 1661 * @param jsonObject The JSON object. Must not be {@code null}. 1662 * @param name The base member name. Must not be {@code null}. 1663 * @param langTag The language tag, {@code null} if none. 1664 */ 1665 private static void removeMember(final JSONObject jsonObject, final String name, final LangTag langTag) { 1666 1667 if (langTag == null) 1668 jsonObject.remove(name); 1669 else 1670 jsonObject.remove(name + "#" + langTag); 1671 } 1672}