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