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