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