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