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