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; 019 020 021import java.net.MalformedURLException; 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.net.URL; 025import java.util.*; 026 027import com.nimbusds.oauth2.sdk.http.HTTPRequest; 028import com.nimbusds.oauth2.sdk.id.ClientID; 029import com.nimbusds.oauth2.sdk.id.State; 030import com.nimbusds.oauth2.sdk.pkce.CodeChallenge; 031import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod; 032import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; 033import com.nimbusds.oauth2.sdk.util.*; 034import net.jcip.annotations.Immutable; 035 036 037/** 038 * Authorisation request. Used to authenticate an end-user and request the 039 * end-user's consent to grant the client access to a protected resource. 040 * Supports custom request parameters. 041 * 042 * <p>Extending classes may define additional request parameters as well as 043 * enforce tighter requirements on the base parameters. 044 * 045 * <p>Example HTTP request: 046 * 047 * <pre> 048 * https://server.example.com/authorize? 049 * response_type=code 050 * &client_id=s6BhdRkqt3 051 * &state=xyz 052 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 053 * </pre> 054 * 055 * <p>Related specifications: 056 * 057 * <ul> 058 * <li>OAuth 2.0 (RFC 6749), sections 4.1.1 and 4.2.1. 059 * <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0. 060 * <li>OAuth 2.0 Form Post Response Mode 1.0. 061 * <li>Proof Key for Code Exchange by OAuth Public Clients (RFC 7636). 062 * <li>Resource Indicators for OAuth 2.0 063 * (draft-ietf-oauth-resource-indicators-00) 064 * <li>OAuth 2.0 Incremental Authorization 065 * (draft-ietf-oauth-incremental-authz-00) 066 * </ul> 067 */ 068@Immutable 069public class AuthorizationRequest extends AbstractRequest { 070 071 072 /** 073 * The registered parameter names. 074 */ 075 private static final Set<String> REGISTERED_PARAMETER_NAMES; 076 077 078 static { 079 Set<String> p = new HashSet<>(); 080 081 p.add("response_type"); 082 p.add("client_id"); 083 p.add("redirect_uri"); 084 p.add("scope"); 085 p.add("state"); 086 p.add("response_mode"); 087 p.add("code_challenge"); 088 p.add("code_challenge_method"); 089 p.add("resource"); 090 p.add("include_granted_scopes"); 091 092 REGISTERED_PARAMETER_NAMES = Collections.unmodifiableSet(p); 093 } 094 095 096 /** 097 * The response type (required). 098 */ 099 private final ResponseType rt; 100 101 102 /** 103 * The client identifier (required). 104 */ 105 private final ClientID clientID; 106 107 108 /** 109 * The redirection URI where the response will be sent (optional). 110 */ 111 private final URI redirectURI; 112 113 114 /** 115 * The scope (optional). 116 */ 117 private final Scope scope; 118 119 120 /** 121 * The opaque value to maintain state between the request and the 122 * callback (recommended). 123 */ 124 private final State state; 125 126 127 /** 128 * The response mode (optional). 129 */ 130 private final ResponseMode rm; 131 132 133 /** 134 * The authorisation code challenge for PKCE (optional). 135 */ 136 private final CodeChallenge codeChallenge; 137 138 139 /** 140 * The authorisation code challenge method for PKCE (optional). 141 */ 142 private final CodeChallengeMethod codeChallengeMethod; 143 144 145 /** 146 * The resource URI(s) (optional). 147 */ 148 private final List<URI> resources; 149 150 151 /** 152 * Indicates incremental authorisation (optional). 153 */ 154 private final boolean includeGrantedScopes; 155 156 157 /** 158 * Custom parameters. 159 */ 160 private final Map<String,List<String>> customParams; 161 162 163 /** 164 * Builder for constructing authorisation requests. 165 */ 166 public static class Builder { 167 168 169 /** 170 * The endpoint URI (optional). 171 */ 172 private URI uri; 173 174 175 /** 176 * The response type (required). 177 */ 178 private final ResponseType rt; 179 180 181 /** 182 * The client identifier (required). 183 */ 184 private final ClientID clientID; 185 186 187 /** 188 * The redirection URI where the response will be sent 189 * (optional). 190 */ 191 private URI redirectURI; 192 193 194 /** 195 * The scope (optional). 196 */ 197 private Scope scope; 198 199 200 /** 201 * The opaque value to maintain state between the request and 202 * the callback (recommended). 203 */ 204 private State state; 205 206 207 /** 208 * The response mode (optional). 209 */ 210 private ResponseMode rm; 211 212 213 /** 214 * The authorisation code challenge for PKCE (optional). 215 */ 216 private CodeChallenge codeChallenge; 217 218 219 /** 220 * The authorisation code challenge method for PKCE (optional). 221 */ 222 private CodeChallengeMethod codeChallengeMethod; 223 224 225 /** 226 * Indicates incremental authorisation (optional). 227 */ 228 private boolean includeGrantedScopes; 229 230 231 /** 232 * The resource URI(s) (optional). 233 */ 234 private List<URI> resources; 235 236 237 /** 238 * Custom parameters. 239 */ 240 private final Map<String,List<String>> customParams = new HashMap<>(); 241 242 243 /** 244 * Creates a new authorisation request builder. 245 * 246 * @param rt The response type. Corresponds to the 247 * {@code response_type} parameter. Must not be 248 * {@code null}. 249 * @param clientID The client identifier. Corresponds to the 250 * {@code client_id} parameter. Must not be 251 * {@code null}. 252 */ 253 public Builder(final ResponseType rt, final ClientID clientID) { 254 255 if (rt == null) 256 throw new IllegalArgumentException("The response type must not be null"); 257 258 this.rt = rt; 259 260 261 if (clientID == null) 262 throw new IllegalArgumentException("The client ID must not be null"); 263 264 this.clientID = clientID; 265 } 266 267 268 /** 269 * Creates a new authorisation request builder from the 270 * specified request. 271 * 272 * @param request The authorisation request. Must not be 273 * {@code null}. 274 */ 275 public Builder(final AuthorizationRequest request) { 276 277 uri = request.getEndpointURI(); 278 scope = request.scope; 279 rt = request.getResponseType(); 280 clientID = request.getClientID(); 281 redirectURI = request.getRedirectionURI(); 282 state = request.getState(); 283 rm = request.getResponseMode(); 284 codeChallenge = request.getCodeChallenge(); 285 codeChallengeMethod = request.getCodeChallengeMethod(); 286 resources = request.getResources(); 287 includeGrantedScopes = request.includeGrantedScopes(); 288 customParams.putAll(request.getCustomParameters()); 289 } 290 291 292 /** 293 * Sets the redirection URI. Corresponds to the optional 294 * {@code redirection_uri} parameter. 295 * 296 * @param redirectURI The redirection URI, {@code null} if not 297 * specified. 298 * 299 * @return This builder. 300 */ 301 public Builder redirectionURI(final URI redirectURI) { 302 303 this.redirectURI = redirectURI; 304 return this; 305 } 306 307 308 /** 309 * Sets the scope. Corresponds to the optional {@code scope} 310 * parameter. 311 * 312 * @param scope The scope, {@code null} if not specified. 313 * 314 * @return This builder. 315 */ 316 public Builder scope(final Scope scope) { 317 318 this.scope = scope; 319 return this; 320 } 321 322 323 /** 324 * Sets the state. Corresponds to the recommended {@code state} 325 * parameter. 326 * 327 * @param state The state, {@code null} if not specified. 328 * 329 * @return This builder. 330 */ 331 public Builder state(final State state) { 332 333 this.state = state; 334 return this; 335 } 336 337 338 /** 339 * Sets the response mode. Corresponds to the optional 340 * {@code response_mode} parameter. Use of this parameter is 341 * not recommended unless a non-default response mode is 342 * requested (e.g. form_post). 343 * 344 * @param rm The response mode, {@code null} if not specified. 345 * 346 * @return This builder. 347 */ 348 public Builder responseMode(final ResponseMode rm) { 349 350 this.rm = rm; 351 return this; 352 } 353 354 355 /** 356 * Sets the code challenge for Proof Key for Code Exchange 357 * (PKCE) by public OAuth clients. 358 * 359 * @param codeChallenge The code challenge, {@code null} 360 * if not specified. 361 * @param codeChallengeMethod The code challenge method, 362 * {@code null} if not specified. 363 * 364 * @return This builder. 365 */ 366 @Deprecated 367 public Builder codeChallenge(final CodeChallenge codeChallenge, final CodeChallengeMethod codeChallengeMethod) { 368 369 this.codeChallenge = codeChallenge; 370 this.codeChallengeMethod = codeChallengeMethod; 371 return this; 372 } 373 374 375 /** 376 * Sets the code challenge for Proof Key for Code Exchange 377 * (PKCE) by public OAuth clients. 378 * 379 * @param codeVerifier The code verifier to use to 380 * compute the code challenge, 381 * {@code null} if PKCE is not 382 * specified. 383 * @param codeChallengeMethod The code challenge method, 384 * {@code null} if not specified. 385 * Defaults to 386 * {@link CodeChallengeMethod#PLAIN} 387 * if a code verifier is specified. 388 * 389 * @return This builder. 390 */ 391 public Builder codeChallenge(final CodeVerifier codeVerifier, final CodeChallengeMethod codeChallengeMethod) { 392 393 if (codeVerifier != null) { 394 CodeChallengeMethod method = codeChallengeMethod != null ? codeChallengeMethod : CodeChallengeMethod.getDefault(); 395 this.codeChallenge = CodeChallenge.compute(method, codeVerifier); 396 this.codeChallengeMethod = method; 397 } else { 398 this.codeChallenge = null; 399 this.codeChallengeMethod = null; 400 } 401 return this; 402 } 403 404 405 /** 406 * Sets the resource server URI(s). 407 * 408 * @param resources The resource URI(s), {@code null} if not 409 * specified. 410 * 411 * @return This builder. 412 */ 413 public Builder resources(final URI ... resources) { 414 if (resources != null) { 415 this.resources = Arrays.asList(resources); 416 } else { 417 this.resources = null; 418 } 419 return this; 420 } 421 422 423 /** 424 * Requests incremental authorisation. 425 * 426 * @param includeGrantedScopes {@code true} to request 427 * incremental authorisation. 428 * 429 * @return This builder. 430 */ 431 public Builder includeGrantedScopes(final boolean includeGrantedScopes) { 432 433 this.includeGrantedScopes = includeGrantedScopes; 434 return this; 435 } 436 437 438 /** 439 * Sets a custom parameter. 440 * 441 * @param name The parameter name. Must not be {@code null}. 442 * @param values The parameter values, {@code null} if not 443 * specified. 444 * 445 * @return This builder. 446 */ 447 public Builder customParameter(final String name, final String ... values) { 448 449 if (values == null || values.length == 0) { 450 customParams.remove(name); 451 } else { 452 customParams.put(name, Arrays.asList(values)); 453 } 454 455 return this; 456 } 457 458 459 /** 460 * Sets the URI of the endpoint (HTTP or HTTPS) for which the 461 * request is intended. 462 * 463 * @param uri The endpoint URI, {@code null} if not specified. 464 * 465 * @return This builder. 466 */ 467 public Builder endpointURI(final URI uri) { 468 469 this.uri = uri; 470 return this; 471 } 472 473 474 /** 475 * Builds a new authorisation request. 476 * 477 * @return The authorisation request. 478 */ 479 public AuthorizationRequest build() { 480 481 try { 482 return new AuthorizationRequest(uri, rt, rm, clientID, redirectURI, scope, state, codeChallenge, codeChallengeMethod, resources, includeGrantedScopes, customParams); 483 } catch (IllegalArgumentException e) { 484 throw new IllegalStateException(e.getMessage(), e); 485 } 486 } 487 } 488 489 490 /** 491 * Creates a new minimal authorisation request. 492 * 493 * @param uri The URI of the authorisation endpoint. May be 494 * {@code null} if the {@link #toHTTPRequest} method 495 * will not be used. 496 * @param rt The response type. Corresponds to the 497 * {@code response_type} parameter. Must not be 498 * {@code null}. 499 * @param clientID The client identifier. Corresponds to the 500 * {@code client_id} parameter. Must not be 501 * {@code null}. 502 */ 503 public AuthorizationRequest(final URI uri, 504 final ResponseType rt, 505 final ClientID clientID) { 506 507 this(uri, rt, null, clientID, null, null, null, null, null, null, false, null); 508 } 509 510 511 /** 512 * Creates a new authorisation request. 513 * 514 * @param uri The URI of the authorisation endpoint. 515 * May be {@code null} if the 516 * {@link #toHTTPRequest} method will not be 517 * used. 518 * @param rt The response type. Corresponds to the 519 * {@code response_type} parameter. Must not 520 * be {@code null}. 521 * @param rm The response mode. Corresponds to the 522 * optional {@code response_mode} parameter. 523 * Use of this parameter is not recommended 524 * unless a non-default response mode is 525 * requested (e.g. form_post). 526 * @param clientID The client identifier. Corresponds to the 527 * {@code client_id} parameter. Must not be 528 * {@code null}. 529 * @param redirectURI The redirection URI. Corresponds to the 530 * optional {@code redirect_uri} parameter. 531 * {@code null} if not specified. 532 * @param scope The request scope. Corresponds to the 533 * optional {@code scope} parameter. 534 * {@code null} if not specified. 535 * @param state The state. Corresponds to the recommended 536 * {@code state} parameter. {@code null} if 537 * not specified. 538 */ 539 public AuthorizationRequest(final URI uri, 540 final ResponseType rt, 541 final ResponseMode rm, 542 final ClientID clientID, 543 final URI redirectURI, 544 final Scope scope, 545 final State state) { 546 547 this(uri, rt, rm, clientID, redirectURI, scope, state, null, null, null, false, null); 548 } 549 550 551 /** 552 * Creates a new authorisation request with extension and custom 553 * parameters. 554 * 555 * @param uri The URI of the authorisation endpoint. 556 * May be {@code null} if the 557 * {@link #toHTTPRequest} method will not 558 * be used. 559 * @param rt The response type. Corresponds to the 560 * {@code response_type} parameter. Must 561 * not be {@code null}. 562 * @param rm The response mode. Corresponds to the 563 * optional {@code response_mode} 564 * parameter. Use of this parameter is not 565 * recommended unless a non-default 566 * response mode is requested (e.g. 567 * form_post). 568 * @param clientID The client identifier. Corresponds to 569 * the {@code client_id} parameter. Must 570 * not be {@code null}. 571 * @param redirectURI The redirection URI. Corresponds to the 572 * optional {@code redirect_uri} parameter. 573 * {@code null} if not specified. 574 * @param scope The request scope. Corresponds to the 575 * optional {@code scope} parameter. 576 * {@code null} if not specified. 577 * @param state The state. Corresponds to the 578 * recommended {@code state} parameter. 579 * {@code null} if not specified. 580 * @param codeChallenge The code challenge for PKCE, 581 * {@code null} if not specified. 582 * @param codeChallengeMethod The code challenge method for PKCE, 583 * {@code null} if not specified. 584 * @param resources The resource URI(s), {@code null} if not 585 * specified. 586 * @param includeGrantedScopes {@code true} to request incremental 587 * authorisation. 588 * @param customParams Custom parameters, empty map or 589 * {@code null} if none. 590 */ 591 public AuthorizationRequest(final URI uri, 592 final ResponseType rt, 593 final ResponseMode rm, 594 final ClientID clientID, 595 final URI redirectURI, 596 final Scope scope, 597 final State state, 598 final CodeChallenge codeChallenge, 599 final CodeChallengeMethod codeChallengeMethod, 600 final List<URI> resources, 601 final boolean includeGrantedScopes, 602 final Map<String,List<String>> customParams) { 603 604 super(uri); 605 606 if (rt == null) 607 throw new IllegalArgumentException("The response type must not be null"); 608 609 this.rt = rt; 610 611 this.rm = rm; 612 613 614 if (clientID == null) 615 throw new IllegalArgumentException("The client ID must not be null"); 616 617 this.clientID = clientID; 618 619 620 this.redirectURI = redirectURI; 621 this.scope = scope; 622 this.state = state; 623 624 this.codeChallenge = codeChallenge; 625 this.codeChallengeMethod = codeChallengeMethod; 626 627 if (resources != null) { 628 for (URI resourceURI: resources) { 629 if (! ResourceUtils.isValidResourceURI(resourceURI)) 630 throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI); 631 } 632 } 633 634 this.resources = resources; 635 636 this.includeGrantedScopes = includeGrantedScopes; 637 638 if (MapUtils.isNotEmpty(customParams)) { 639 this.customParams = Collections.unmodifiableMap(customParams); 640 } else { 641 this.customParams = Collections.emptyMap(); 642 } 643 } 644 645 646 /** 647 * Returns the registered (standard) OAuth 2.0 authorisation request 648 * parameter names. 649 * 650 * @return The registered OAuth 2.0 authorisation request parameter 651 * names, as a unmodifiable set. 652 */ 653 public static Set<String> getRegisteredParameterNames() { 654 655 return REGISTERED_PARAMETER_NAMES; 656 } 657 658 659 /** 660 * Gets the response type. Corresponds to the {@code response_type} 661 * parameter. 662 * 663 * @return The response type. 664 */ 665 public ResponseType getResponseType() { 666 667 return rt; 668 } 669 670 671 /** 672 * Gets the optional response mode. Corresponds to the optional 673 * {@code response_mode} parameter. 674 * 675 * @return The response mode, {@code null} if not specified. 676 */ 677 public ResponseMode getResponseMode() { 678 679 return rm; 680 } 681 682 683 /** 684 * Returns the implied response mode, determined by the optional 685 * {@code response_mode} parameter, and if that isn't specified, by 686 * the {@code response_type}. 687 * 688 * @return The implied response mode. 689 */ 690 public ResponseMode impliedResponseMode() { 691 692 if (rm != null) { 693 return rm; 694 } else if (rt.impliesImplicitFlow() || rt.impliesHybridFlow()) { 695 return ResponseMode.FRAGMENT; 696 } else { 697 return ResponseMode.QUERY; 698 } 699 } 700 701 702 /** 703 * Gets the client identifier. Corresponds to the {@code client_id} 704 * parameter. 705 * 706 * @return The client identifier. 707 */ 708 public ClientID getClientID() { 709 710 return clientID; 711 } 712 713 714 /** 715 * Gets the redirection URI. Corresponds to the optional 716 * {@code redirection_uri} parameter. 717 * 718 * @return The redirection URI, {@code null} if not specified. 719 */ 720 public URI getRedirectionURI() { 721 722 return redirectURI; 723 } 724 725 726 /** 727 * Gets the scope. Corresponds to the optional {@code scope} parameter. 728 * 729 * @return The scope, {@code null} if not specified. 730 */ 731 public Scope getScope() { 732 733 return scope; 734 } 735 736 737 /** 738 * Gets the state. Corresponds to the recommended {@code state} 739 * parameter. 740 * 741 * @return The state, {@code null} if not specified. 742 */ 743 public State getState() { 744 745 return state; 746 } 747 748 749 /** 750 * Returns the code challenge for PKCE. 751 * 752 * @return The code challenge, {@code null} if not specified. 753 */ 754 public CodeChallenge getCodeChallenge() { 755 756 return codeChallenge; 757 } 758 759 760 /** 761 * Returns the code challenge method for PKCE. 762 * 763 * @return The code challenge method, {@code null} if not specified. 764 */ 765 public CodeChallengeMethod getCodeChallengeMethod() { 766 767 return codeChallengeMethod; 768 } 769 770 771 /** 772 * Returns the resource server URI. 773 * 774 * @return The resource URI(s), {@code null} if not specified. 775 */ 776 public List<URI> getResources() { 777 778 return resources; 779 } 780 781 782 /** 783 * Returns {@code true} if incremental authorisation is requested. 784 * 785 * @return {@code true} if incremental authorisation is requested, 786 * else {@code false}. 787 */ 788 public boolean includeGrantedScopes() { 789 790 return includeGrantedScopes; 791 } 792 793 794 /** 795 * Returns the additional custom parameters. 796 * 797 * @return The additional custom parameters as a unmodifiable map, 798 * empty map if none. 799 */ 800 public Map<String,List<String>> getCustomParameters () { 801 802 return customParams; 803 } 804 805 806 /** 807 * Returns the specified custom parameter. 808 * 809 * @param name The parameter name. Must not be {@code null}. 810 * 811 * @return The parameter value(s), {@code null} if not specified. 812 */ 813 public List<String> getCustomParameter(final String name) { 814 815 return customParams.get(name); 816 } 817 818 819 /** 820 * Returns the URI query parameters for this authorisation request. 821 * Query parameters which are part of the authorisation endpoint are 822 * not included. 823 * 824 * <p>Example parameters: 825 * 826 * <pre> 827 * response_type = code 828 * client_id = s6BhdRkqt3 829 * state = xyz 830 * redirect_uri = https://client.example.com/cb 831 * </pre> 832 * 833 * @return The parameters. 834 */ 835 public Map<String,List<String>> toParameters() { 836 837 Map<String,List<String>> params = new LinkedHashMap<>(); 838 839 // Put custom params first, so they may be overwritten by std params 840 params.putAll(customParams); 841 842 params.put("response_type", Collections.singletonList(rt.toString())); 843 params.put("client_id", Collections.singletonList(clientID.getValue())); 844 845 if (rm != null) { 846 params.put("response_mode", Collections.singletonList(rm.getValue())); 847 } 848 849 if (redirectURI != null) 850 params.put("redirect_uri", Collections.singletonList(redirectURI.toString())); 851 852 if (scope != null) 853 params.put("scope", Collections.singletonList(scope.toString())); 854 855 if (state != null) 856 params.put("state", Collections.singletonList(state.getValue())); 857 858 if (codeChallenge != null) { 859 params.put("code_challenge", Collections.singletonList(codeChallenge.getValue())); 860 861 if (codeChallengeMethod != null) { 862 params.put("code_challenge_method", Collections.singletonList(codeChallengeMethod.getValue())); 863 } 864 } 865 866 if (includeGrantedScopes) { 867 params.put("include_granted_scopes", Collections.singletonList("true")); 868 } 869 870 if (resources != null) { 871 List<String> resourceValues = new LinkedList<>(); 872 for (URI resourceURI: resources) { 873 if (resourceURI != null) { 874 resourceValues.add(resourceURI.toString()); 875 } 876 } 877 params.put("resource", resourceValues); 878 } 879 880 return params; 881 } 882 883 884 /** 885 * Returns the URI query string for this authorisation request. 886 * 887 * <p>Note that the '?' character preceding the query string in an URI 888 * is not included in the returned string. 889 * 890 * <p>Example URI query string: 891 * 892 * <pre> 893 * response_type=code 894 * &client_id=s6BhdRkqt3 895 * &state=xyz 896 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 897 * </pre> 898 * 899 * @return The URI query string. 900 */ 901 public String toQueryString() { 902 903 Map<String, List<String>> params = new HashMap<>(); 904 if (getEndpointURI() != null) { 905 params.putAll(URLUtils.parseParameters(getEndpointURI().getQuery())); 906 } 907 params.putAll(toParameters()); 908 909 return URLUtils.serializeParameters(params); 910 } 911 912 913 /** 914 * Returns the complete URI representation for this authorisation 915 * request, consisting of the {@link #getEndpointURI authorization 916 * endpoint URI} with the {@link #toQueryString query string} appended. 917 * 918 * <p>Example URI: 919 * 920 * <pre> 921 * https://server.example.com/authorize? 922 * response_type=code 923 * &client_id=s6BhdRkqt3 924 * &state=xyz 925 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 926 * </pre> 927 * 928 * @return The URI representation. 929 */ 930 public URI toURI() { 931 932 if (getEndpointURI() == null) 933 throw new SerializeException("The authorization endpoint URI is not specified"); 934 935 StringBuilder sb = new StringBuilder(URIUtils.stripQueryString(getEndpointURI()).toString()); 936 sb.append('?'); 937 sb.append(toQueryString()); 938 try { 939 return new URI(sb.toString()); 940 } catch (URISyntaxException e) { 941 throw new SerializeException("Couldn't append query string: " + e.getMessage(), e); 942 } 943 } 944 945 946 /** 947 * Returns the matching HTTP request. 948 * 949 * @param method The HTTP request method which can be GET or POST. Must 950 * not be {@code null}. 951 * 952 * @return The HTTP request. 953 */ 954 public HTTPRequest toHTTPRequest(final HTTPRequest.Method method) { 955 956 if (getEndpointURI() == null) 957 throw new SerializeException("The endpoint URI is not specified"); 958 959 HTTPRequest httpRequest; 960 961 URL endpointURL; 962 963 try { 964 endpointURL = getEndpointURI().toURL(); 965 966 } catch (MalformedURLException e) { 967 968 throw new SerializeException(e.getMessage(), e); 969 } 970 971 if (method.equals(HTTPRequest.Method.GET)) { 972 973 httpRequest = new HTTPRequest(HTTPRequest.Method.GET, endpointURL); 974 975 } else if (method.equals(HTTPRequest.Method.POST)) { 976 977 httpRequest = new HTTPRequest(HTTPRequest.Method.POST, endpointURL); 978 979 } else { 980 981 throw new IllegalArgumentException("The HTTP request method must be GET or POST"); 982 } 983 984 httpRequest.setQuery(toQueryString()); 985 986 return httpRequest; 987 } 988 989 990 @Override 991 public HTTPRequest toHTTPRequest() { 992 993 return toHTTPRequest(HTTPRequest.Method.GET); 994 } 995 996 997 /** 998 * Parses an authorisation request from the specified URI query 999 * parameters. 1000 * 1001 * <p>Example parameters: 1002 * 1003 * <pre> 1004 * response_type = code 1005 * client_id = s6BhdRkqt3 1006 * state = xyz 1007 * redirect_uri = https://client.example.com/cb 1008 * </pre> 1009 * 1010 * @param params The parameters. Must not be {@code null}. 1011 * 1012 * @return The authorisation request. 1013 * 1014 * @throws ParseException If the parameters couldn't be parsed to an 1015 * authorisation request. 1016 */ 1017 public static AuthorizationRequest parse(final Map<String,List<String>> params) 1018 throws ParseException { 1019 1020 return parse(null, params); 1021 } 1022 1023 1024 /** 1025 * Parses an authorisation request from the specified URI and query 1026 * parameters. 1027 * 1028 * <p>Example parameters: 1029 * 1030 * <pre> 1031 * response_type = code 1032 * client_id = s6BhdRkqt3 1033 * state = xyz 1034 * redirect_uri = https://client.example.com/cb 1035 * </pre> 1036 * 1037 * @param uri The URI of the authorisation endpoint. May be 1038 * {@code null} if the {@link #toHTTPRequest()} method 1039 * will not be used. 1040 * @param params The parameters. Must not be {@code null}. 1041 * 1042 * @return The authorisation request. 1043 * 1044 * @throws ParseException If the parameters couldn't be parsed to an 1045 * authorisation request. 1046 */ 1047 public static AuthorizationRequest parse(final URI uri, final Map<String,List<String>> params) 1048 throws ParseException { 1049 1050 // Parse mandatory client ID first 1051 String v = MultivaluedMapUtils.getFirstValue(params, "client_id"); 1052 1053 if (StringUtils.isBlank(v)) { 1054 String msg = "Missing \"client_id\" parameter"; 1055 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 1056 } 1057 1058 ClientID clientID = new ClientID(v); 1059 1060 1061 // Parse optional redirection URI second 1062 v = MultivaluedMapUtils.getFirstValue(params, "redirect_uri"); 1063 1064 URI redirectURI = null; 1065 1066 if (StringUtils.isNotBlank(v)) { 1067 1068 try { 1069 redirectURI = new URI(v); 1070 1071 } catch (URISyntaxException e) { 1072 String msg = "Invalid \"redirect_uri\" parameter: " + e.getMessage(); 1073 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1074 clientID, null, null, null, e); 1075 } 1076 } 1077 1078 1079 // Parse optional state third 1080 State state = State.parse(MultivaluedMapUtils.getFirstValue(params, "state")); 1081 1082 1083 // Parse mandatory response type 1084 v = MultivaluedMapUtils.getFirstValue(params, "response_type"); 1085 1086 ResponseType rt; 1087 1088 try { 1089 rt = ResponseType.parse(v); 1090 1091 } catch (ParseException e) { 1092 // Only cause 1093 String msg = "Missing \"response_type\" parameter"; 1094 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1095 clientID, redirectURI, null, state, e); 1096 } 1097 1098 1099 // Parse the optional response mode 1100 v = MultivaluedMapUtils.getFirstValue(params, "response_mode"); 1101 1102 ResponseMode rm = null; 1103 1104 if (StringUtils.isNotBlank(v)) { 1105 rm = new ResponseMode(v); 1106 } 1107 1108 1109 // Parse optional scope 1110 v = MultivaluedMapUtils.getFirstValue(params, "scope"); 1111 1112 Scope scope = null; 1113 1114 if (StringUtils.isNotBlank(v)) 1115 scope = Scope.parse(v); 1116 1117 1118 // Parse optional code challenge and method for PKCE 1119 CodeChallenge codeChallenge = null; 1120 CodeChallengeMethod codeChallengeMethod = null; 1121 1122 v = MultivaluedMapUtils.getFirstValue(params, "code_challenge"); 1123 1124 if (StringUtils.isNotBlank(v)) 1125 codeChallenge = CodeChallenge.parse(v); 1126 1127 if (codeChallenge != null) { 1128 1129 v = MultivaluedMapUtils.getFirstValue(params, "code_challenge_method"); 1130 1131 if (StringUtils.isNotBlank(v)) 1132 codeChallengeMethod = CodeChallengeMethod.parse(v); 1133 } 1134 1135 List<URI> resources = null; 1136 1137 List<String> vList = params.get("resource"); 1138 1139 if (vList != null) { 1140 1141 resources = new LinkedList<>(); 1142 1143 for (String uriValue: vList) { 1144 1145 if (uriValue == null) 1146 continue; 1147 1148 String errMsg = "Invalid \"resource\" parameter: Must be an absolute URI and with no query or fragment: " + uriValue; 1149 1150 URI resourceURI; 1151 try { 1152 resourceURI = new URI(uriValue); 1153 } catch (URISyntaxException e) { 1154 throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg), 1155 clientID, redirectURI, null, state, e); 1156 } 1157 1158 if (! ResourceUtils.isValidResourceURI(resourceURI)) { 1159 throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg), 1160 clientID, redirectURI, null, state, null); 1161 } 1162 1163 resources.add(resourceURI); 1164 } 1165 } 1166 1167 boolean includeGrantedScopes = false; 1168 v = MultivaluedMapUtils.getFirstValue(params, "include_granted_scopes"); 1169 if ("true".equals(v)) { 1170 includeGrantedScopes = true; 1171 } 1172 1173 // Parse custom parameters 1174 Map<String,List<String>> customParams = null; 1175 1176 for (Map.Entry<String,List<String>> p: params.entrySet()) { 1177 1178 if (! REGISTERED_PARAMETER_NAMES.contains(p.getKey())) { 1179 // We have a custom parameter 1180 if (customParams == null) { 1181 customParams = new HashMap<>(); 1182 } 1183 customParams.put(p.getKey(), p.getValue()); 1184 } 1185 } 1186 1187 1188 return new AuthorizationRequest(uri, rt, rm, clientID, redirectURI, scope, state, codeChallenge, codeChallengeMethod, resources, includeGrantedScopes, customParams); 1189 } 1190 1191 1192 /** 1193 * Parses an authorisation request from the specified URI query string. 1194 * 1195 * <p>Example URI query string: 1196 * 1197 * <pre> 1198 * response_type=code 1199 * &client_id=s6BhdRkqt3 1200 * &state=xyz 1201 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 1202 * </pre> 1203 * 1204 * @param query The URI query string. Must not be {@code null}. 1205 * 1206 * @return The authorisation request. 1207 * 1208 * @throws ParseException If the query string couldn't be parsed to an 1209 * authorisation request. 1210 */ 1211 public static AuthorizationRequest parse(final String query) 1212 throws ParseException { 1213 1214 return parse(null, URLUtils.parseParameters(query)); 1215 } 1216 1217 1218 /** 1219 * Parses an authorisation request from the specified URI and query 1220 * string. 1221 * 1222 * <p>Example URI query string: 1223 * 1224 * <pre> 1225 * response_type=code 1226 * &client_id=s6BhdRkqt3 1227 * &state=xyz 1228 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 1229 * </pre> 1230 * 1231 * @param uri The URI of the authorisation endpoint. May be 1232 * {@code null} if the {@link #toHTTPRequest()} method 1233 * will not be used. 1234 * @param query The URI query string. Must not be {@code null}. 1235 * 1236 * @return The authorisation request. 1237 * 1238 * @throws ParseException If the query string couldn't be parsed to an 1239 * authorisation request. 1240 */ 1241 public static AuthorizationRequest parse(final URI uri, final String query) 1242 throws ParseException { 1243 1244 return parse(uri, URLUtils.parseParameters(query)); 1245 } 1246 1247 1248 /** 1249 * Parses an authorisation request from the specified URI. 1250 * 1251 * <p>Example URI: 1252 * 1253 * <pre> 1254 * https://server.example.com/authorize? 1255 * response_type=code 1256 * &client_id=s6BhdRkqt3 1257 * &state=xyz 1258 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 1259 * </pre> 1260 * 1261 * @param uri The URI. Must not be {@code null}. 1262 * 1263 * @return The authorisation request. 1264 * 1265 * @throws ParseException If the URI couldn't be parsed to an 1266 * authorisation request. 1267 */ 1268 public static AuthorizationRequest parse(final URI uri) 1269 throws ParseException { 1270 1271 return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery())); 1272 } 1273 1274 1275 /** 1276 * Parses an authorisation request from the specified HTTP request. 1277 * 1278 * <p>Example HTTP request (GET): 1279 * 1280 * <pre> 1281 * https://server.example.com/authorize? 1282 * response_type=code 1283 * &client_id=s6BhdRkqt3 1284 * &state=xyz 1285 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 1286 * </pre> 1287 * 1288 * @param httpRequest The HTTP request. Must not be {@code null}. 1289 * 1290 * @return The authorisation request. 1291 * 1292 * @throws ParseException If the HTTP request couldn't be parsed to an 1293 * authorisation request. 1294 */ 1295 public static AuthorizationRequest parse(final HTTPRequest httpRequest) 1296 throws ParseException { 1297 1298 String query = httpRequest.getQuery(); 1299 1300 if (query == null) 1301 throw new ParseException("Missing URI query string"); 1302 1303 try { 1304 return parse(URIUtils.getBaseURI(httpRequest.getURL().toURI()), query); 1305 1306 } catch (URISyntaxException e) { 1307 1308 throw new ParseException(e.getMessage(), e); 1309 } 1310 } 1311}