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