001package com.nimbusds.oauth2.sdk; 002 003 004import java.net.MalformedURLException; 005import java.net.URI; 006import java.net.URISyntaxException; 007import java.net.URL; 008import java.util.LinkedHashMap; 009import java.util.Map; 010 011import com.nimbusds.oauth2.sdk.http.HTTPRequest; 012import com.nimbusds.oauth2.sdk.id.ClientID; 013import com.nimbusds.oauth2.sdk.id.State; 014import com.nimbusds.oauth2.sdk.pkce.CodeChallenge; 015import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod; 016import com.nimbusds.oauth2.sdk.util.URIUtils; 017import com.nimbusds.oauth2.sdk.util.URLUtils; 018import net.jcip.annotations.Immutable; 019import org.apache.commons.lang3.StringUtils; 020 021 022/** 023 * Authorisation request. Used to authenticate an end-user and request the 024 * end-user's consent to grant the client access to a protected resource. 025 * 026 * <p>Extending classes may define additional request parameters as well as 027 * enforce tighter requirements on the base parameters. 028 * 029 * <p>Example HTTP request: 030 * 031 * <pre> 032 * https://server.example.com/authorize? 033 * response_type=code 034 * &client_id=s6BhdRkqt3 035 * &state=xyz 036 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 037 * </pre> 038 * 039 * <p>Related specifications: 040 * 041 * <ul> 042 * <li>OAuth 2.0 (RFC 6749), sections 4.1.1 and 4.2.1. 043 * <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0. 044 * <li>OAuth 2.0 Form Post Response Mode 1.0. 045 * <li>Proof Key for Code Exchange by OAuth Public Clients (RFC 7636). 046 * </ul> 047 */ 048@Immutable 049public class AuthorizationRequest extends AbstractRequest { 050 051 052 /** 053 * The response type (required). 054 */ 055 private final ResponseType rt; 056 057 058 /** 059 * The client identifier (required). 060 */ 061 private final ClientID clientID; 062 063 064 /** 065 * The redirection URI where the response will be sent (optional). 066 */ 067 private final URI redirectURI; 068 069 070 /** 071 * The scope (optional). 072 */ 073 private final Scope scope; 074 075 076 /** 077 * The opaque value to maintain state between the request and the 078 * callback (recommended). 079 */ 080 private final State state; 081 082 083 /** 084 * The response mode (optional). 085 */ 086 private final ResponseMode rm; 087 088 089 /** 090 * The authorisation code challenge for PKCE (optional). 091 */ 092 private final CodeChallenge codeChallenge; 093 094 095 /** 096 * The authorisation code challenge method for PKCE (optional). 097 */ 098 private final CodeChallengeMethod codeChallengeMethod; 099 100 101 /** 102 * Builder for constructing authorisation requests. 103 */ 104 public static class Builder { 105 106 107 /** 108 * The endpoint URI (optional). 109 */ 110 private URI uri; 111 112 113 /** 114 * The response type (required). 115 */ 116 private final ResponseType rt; 117 118 119 /** 120 * The client identifier (required). 121 */ 122 private final ClientID clientID; 123 124 125 /** 126 * The redirection URI where the response will be sent 127 * (optional). 128 */ 129 private URI redirectURI; 130 131 132 /** 133 * The scope (optional). 134 */ 135 private Scope scope; 136 137 138 /** 139 * The opaque value to maintain state between the request and 140 * the callback (recommended). 141 */ 142 private State state; 143 144 145 /** 146 * The response mode (optional). 147 */ 148 private ResponseMode rm; 149 150 151 /** 152 * The authorisation code challenge for PKCE (optional). 153 */ 154 private CodeChallenge codeChallenge; 155 156 157 /** 158 * The authorisation code challenge method for PKCE (optional). 159 */ 160 private CodeChallengeMethod codeChallengeMethod; 161 162 163 /** 164 * Creates a new authorisation request builder. 165 * 166 * @param rt The response type. Corresponds to the 167 * {@code response_type} parameter. Must not be 168 * {@code null}. 169 * @param clientID The client identifier. Corresponds to the 170 * {@code client_id} parameter. Must not be 171 * {@code null}. 172 */ 173 public Builder(final ResponseType rt, final ClientID clientID) { 174 175 if (rt == null) 176 throw new IllegalArgumentException("The response type must not be null"); 177 178 this.rt = rt; 179 180 181 if (clientID == null) 182 throw new IllegalArgumentException("The client ID must not be null"); 183 184 this.clientID = clientID; 185 } 186 187 188 /** 189 * Sets the redirection URI. Corresponds to the optional 190 * {@code redirection_uri} parameter. 191 * 192 * @param redirectURI The redirection URI, {@code null} if not 193 * specified. 194 * 195 * @return This builder. 196 */ 197 public Builder redirectionURI(final URI redirectURI) { 198 199 this.redirectURI = redirectURI; 200 return this; 201 } 202 203 204 /** 205 * Sets the scope. Corresponds to the optional {@code scope} 206 * parameter. 207 * 208 * @param scope The scope, {@code null} if not specified. 209 * 210 * @return This builder. 211 */ 212 public Builder scope(final Scope scope) { 213 214 this.scope = scope; 215 return this; 216 } 217 218 219 /** 220 * Sets the state. Corresponds to the recommended {@code state} 221 * parameter. 222 * 223 * @param state The state, {@code null} if not specified. 224 * 225 * @return This builder. 226 */ 227 public Builder state(final State state) { 228 229 this.state = state; 230 return this; 231 } 232 233 234 /** 235 * Sets the response mode. Corresponds to the optional 236 * {@code response_mode} parameter. Use of this parameter is 237 * not recommended unless a non-default response mode is 238 * requested (e.g. form_post). 239 * 240 * @param rm The response mode, {@code null} if not specified. 241 * 242 * @return This builder. 243 */ 244 public Builder responseMode(final ResponseMode rm) { 245 246 this.rm = rm; 247 return this; 248 } 249 250 251 /** 252 * Sets the code challenge for Proof Key for Code Exchange 253 * (PKCE) by public OAuth clients. 254 * 255 * @param codeChallenge The code challenge, {@code null} 256 * if not specified. 257 * @param codeChallengeMethod The code challenge method, 258 * {@code null} if not specified. 259 * 260 * @return This builder. 261 */ 262 public Builder codeChallenge(final CodeChallenge codeChallenge, final CodeChallengeMethod codeChallengeMethod) { 263 264 this.codeChallenge = codeChallenge; 265 this.codeChallengeMethod = codeChallengeMethod; 266 return this; 267 } 268 269 270 /** 271 * Sets the URI of the endpoint (HTTP or HTTPS) for which the 272 * request is intended. 273 * 274 * @param uri The endpoint URI, {@code null} if not specified. 275 * 276 * @return This builder. 277 */ 278 public Builder endpointURI(final URI uri) { 279 280 this.uri = uri; 281 return this; 282 } 283 284 285 /** 286 * Builds a new authorisation request. 287 * 288 * @return The authorisation request. 289 */ 290 public AuthorizationRequest build() { 291 292 return new AuthorizationRequest(uri, rt, rm, clientID, redirectURI, scope, state, codeChallenge, codeChallengeMethod); 293 } 294 } 295 296 297 /** 298 * Creates a new minimal authorisation request. 299 * 300 * @param uri The URI of the authorisation endpoint. May be 301 * {@code null} if the {@link #toHTTPRequest} method 302 * will not be used. 303 * @param rt The response type. Corresponds to the 304 * {@code response_type} parameter. Must not be 305 * {@code null}. 306 * @param clientID The client identifier. Corresponds to the 307 * {@code client_id} parameter. Must not be 308 * {@code null}. 309 */ 310 public AuthorizationRequest(final URI uri, 311 final ResponseType rt, 312 final ClientID clientID) { 313 314 this(uri, rt, null, clientID, null, null, null, null, null); 315 } 316 317 318 /** 319 * Creates a new authorisation request. 320 * 321 * @param uri The URI of the authorisation endpoint. 322 * May be {@code null} if the 323 * {@link #toHTTPRequest} method will not be 324 * used. 325 * @param rt The response type. Corresponds to the 326 * {@code response_type} parameter. Must not 327 * be {@code null}. 328 * @param rm The response mode. Corresponds to the 329 * optional {@code response_mode} parameter. 330 * Use of this parameter is not recommended 331 * unless a non-default response mode is 332 * requested (e.g. form_post). 333 * @param clientID The client identifier. Corresponds to the 334 * {@code client_id} parameter. Must not be 335 * {@code null}. 336 * @param redirectURI The redirection URI. Corresponds to the 337 * optional {@code redirect_uri} parameter. 338 * {@code null} if not specified. 339 * @param scope The request scope. Corresponds to the 340 * optional {@code scope} parameter. 341 * {@code null} if not specified. 342 * @param state The state. Corresponds to the recommended 343 * {@code state} parameter. {@code null} if 344 * not specified. 345 * @param codeChallenge The code challenge for PKCE, {@code null} 346 * if not specified. 347 * @param codeChallengeMethod The code challenge method for PKCE, 348 * {@code null} if not specified. 349 */ 350 public AuthorizationRequest(final URI uri, 351 final ResponseType rt, 352 final ResponseMode rm, 353 final ClientID clientID, 354 final URI redirectURI, 355 final Scope scope, 356 final State state, 357 final CodeChallenge codeChallenge, 358 final CodeChallengeMethod codeChallengeMethod) { 359 360 super(uri); 361 362 if (rt == null) 363 throw new IllegalArgumentException("The response type must not be null"); 364 365 this.rt = rt; 366 367 this.rm = rm; 368 369 370 if (clientID == null) 371 throw new IllegalArgumentException("The client ID must not be null"); 372 373 this.clientID = clientID; 374 375 376 this.redirectURI = redirectURI; 377 this.scope = scope; 378 this.state = state; 379 380 this.codeChallenge = codeChallenge; 381 this.codeChallengeMethod = codeChallengeMethod; 382 } 383 384 385 /** 386 * Gets the response type. Corresponds to the {@code response_type} 387 * parameter. 388 * 389 * @return The response type. 390 */ 391 public ResponseType getResponseType() { 392 393 return rt; 394 } 395 396 397 /** 398 * Gets the optional response mode. Corresponds to the optional 399 * {@code response_mode} parameter. 400 * 401 * @return The response mode, {@code null} if not specified. 402 */ 403 public ResponseMode getResponseMode() { 404 405 return rm; 406 } 407 408 409 /** 410 * Returns the implied response mode, determined by the optional 411 * {@code response_mode} parameter, and if that isn't specified, by 412 * the {@code response_type}. 413 * 414 * @return The implied response mode. 415 */ 416 public ResponseMode impliedResponseMode() { 417 418 if (rm != null) { 419 return rm; 420 } else if (rt.impliesImplicitFlow()) { 421 return ResponseMode.FRAGMENT; 422 } else { 423 return ResponseMode.QUERY; 424 } 425 } 426 427 428 /** 429 * Gets the client identifier. Corresponds to the {@code client_id} 430 * parameter. 431 * 432 * @return The client identifier. 433 */ 434 public ClientID getClientID() { 435 436 return clientID; 437 } 438 439 440 /** 441 * Gets the redirection URI. Corresponds to the optional 442 * {@code redirection_uri} parameter. 443 * 444 * @return The redirection URI, {@code null} if not specified. 445 */ 446 public URI getRedirectionURI() { 447 448 return redirectURI; 449 } 450 451 452 /** 453 * Gets the scope. Corresponds to the optional {@code scope} parameter. 454 * 455 * @return The scope, {@code null} if not specified. 456 */ 457 public Scope getScope() { 458 459 return scope; 460 } 461 462 463 /** 464 * Gets the state. Corresponds to the recommended {@code state} 465 * parameter. 466 * 467 * @return The state, {@code null} if not specified. 468 */ 469 public State getState() { 470 471 return state; 472 } 473 474 475 /** 476 * Returns the code challenge for PKCE. 477 * 478 * @return The code challenge, {@code null} if not specified. 479 */ 480 public CodeChallenge getCodeChallenge() { 481 482 return codeChallenge; 483 } 484 485 486 /** 487 * Returns the code challenge method for PKCE. 488 * 489 * @return The code challenge method, {@code null} if not specified. 490 */ 491 public CodeChallengeMethod getCodeChallengeMethod() { 492 493 return codeChallengeMethod; 494 } 495 496 497 /** 498 * Returns the parameters for this authorisation request. 499 * 500 * <p>Example parameters: 501 * 502 * <pre> 503 * response_type = code 504 * client_id = s6BhdRkqt3 505 * state = xyz 506 * redirect_uri = https://client.example.com/cb 507 * </pre> 508 * 509 * @return The parameters. 510 */ 511 public Map<String,String> toParameters() { 512 513 Map <String,String> params = new LinkedHashMap<>(); 514 515 params.put("response_type", rt.toString()); 516 params.put("client_id", clientID.getValue()); 517 518 if (rm != null) { 519 params.put("response_mode", rm.getValue()); 520 } 521 522 if (redirectURI != null) 523 params.put("redirect_uri", redirectURI.toString()); 524 525 if (scope != null) 526 params.put("scope", scope.toString()); 527 528 if (state != null) 529 params.put("state", state.getValue()); 530 531 if (codeChallenge != null) { 532 params.put("code_challenge", codeChallenge.getValue()); 533 534 if (codeChallengeMethod != null) { 535 params.put("code_challenge_method", codeChallengeMethod.getValue()); 536 } 537 } 538 539 return params; 540 } 541 542 543 /** 544 * Returns the URI query string for this authorisation request. 545 * 546 * <p>Note that the '?' character preceding the query string in an URI 547 * is not included in the returned string. 548 * 549 * <p>Example URI query string: 550 * 551 * <pre> 552 * response_type=code 553 * &client_id=s6BhdRkqt3 554 * &state=xyz 555 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 556 * </pre> 557 * 558 * @return The URI query string. 559 */ 560 public String toQueryString() { 561 562 return URLUtils.serializeParameters(toParameters()); 563 } 564 565 566 /** 567 * Returns the complete URI representation for this authorisation 568 * request, consisting of the {@link #getEndpointURI authorization 569 * endpoint URI} with the {@link #toQueryString query string} appended. 570 * 571 * <p>Example URI: 572 * 573 * <pre> 574 * https://server.example.com/authorize? 575 * response_type=code 576 * &client_id=s6BhdRkqt3 577 * &state=xyz 578 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 579 * </pre> 580 * 581 * @return The URI representation. 582 */ 583 public URI toURI() { 584 585 if (getEndpointURI() == null) 586 throw new SerializeException("The authorization endpoint URI is not specified"); 587 588 StringBuilder sb = new StringBuilder(getEndpointURI().toString()); 589 sb.append('?'); 590 sb.append(toQueryString()); 591 try { 592 return new URI(sb.toString()); 593 } catch (URISyntaxException e) { 594 throw new SerializeException("Couldn't append query string: " + e.getMessage(), e); 595 } 596 } 597 598 599 /** 600 * Returns the matching HTTP request. 601 * 602 * @param method The HTTP request method which can be GET or POST. Must 603 * not be {@code null}. 604 * 605 * @return The HTTP request. 606 */ 607 public HTTPRequest toHTTPRequest(final HTTPRequest.Method method) { 608 609 if (getEndpointURI() == null) 610 throw new SerializeException("The endpoint URI is not specified"); 611 612 HTTPRequest httpRequest; 613 614 URL endpointURL; 615 616 try { 617 endpointURL = getEndpointURI().toURL(); 618 619 } catch (MalformedURLException e) { 620 621 throw new SerializeException(e.getMessage(), e); 622 } 623 624 if (method.equals(HTTPRequest.Method.GET)) { 625 626 httpRequest = new HTTPRequest(HTTPRequest.Method.GET, endpointURL); 627 628 } else if (method.equals(HTTPRequest.Method.POST)) { 629 630 httpRequest = new HTTPRequest(HTTPRequest.Method.POST, endpointURL); 631 632 } else { 633 634 throw new IllegalArgumentException("The HTTP request method must be GET or POST"); 635 } 636 637 httpRequest.setQuery(toQueryString()); 638 639 return httpRequest; 640 } 641 642 643 @Override 644 public HTTPRequest toHTTPRequest() { 645 646 return toHTTPRequest(HTTPRequest.Method.GET); 647 } 648 649 650 /** 651 * Parses an authorisation request from the specified parameters. 652 * 653 * <p>Example parameters: 654 * 655 * <pre> 656 * response_type = code 657 * client_id = s6BhdRkqt3 658 * state = xyz 659 * redirect_uri = https://client.example.com/cb 660 * </pre> 661 * 662 * @param params The parameters. Must not be {@code null}. 663 * 664 * @return The authorisation request. 665 * 666 * @throws ParseException If the parameters couldn't be parsed to an 667 * authorisation request. 668 */ 669 public static AuthorizationRequest parse(final Map<String,String> params) 670 throws ParseException { 671 672 return parse(null, params); 673 } 674 675 676 /** 677 * Parses an authorisation request from the specified parameters. 678 * 679 * <p>Example parameters: 680 * 681 * <pre> 682 * response_type = code 683 * client_id = s6BhdRkqt3 684 * state = xyz 685 * redirect_uri = https://client.example.com/cb 686 * </pre> 687 * 688 * @param uri The URI of the authorisation endpoint. May be 689 * {@code null} if the {@link #toHTTPRequest()} method 690 * will not be used. 691 * @param params The parameters. Must not be {@code null}. 692 * 693 * @return The authorisation request. 694 * 695 * @throws ParseException If the parameters couldn't be parsed to an 696 * authorisation request. 697 */ 698 public static AuthorizationRequest parse(final URI uri, final Map<String,String> params) 699 throws ParseException { 700 701 // Parse mandatory client ID first 702 String v = params.get("client_id"); 703 704 if (StringUtils.isBlank(v)) { 705 String msg = "Missing \"client_id\" parameter"; 706 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 707 } 708 709 ClientID clientID = new ClientID(v); 710 711 712 // Parse optional redirection URI second 713 v = params.get("redirect_uri"); 714 715 URI redirectURI = null; 716 717 if (StringUtils.isNotBlank(v)) { 718 719 try { 720 redirectURI = new URI(v); 721 722 } catch (URISyntaxException e) { 723 String msg = "Invalid \"redirect_uri\" parameter: " + e.getMessage(); 724 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 725 clientID, null, null, null, e); 726 } 727 } 728 729 730 // Parse optional state third 731 State state = State.parse(params.get("state")); 732 733 734 // Parse mandatory response type 735 v = params.get("response_type"); 736 737 ResponseType rt; 738 739 try { 740 rt = ResponseType.parse(v); 741 742 } catch (ParseException e) { 743 // Only cause 744 String msg = "Missing \"response_type\" parameter"; 745 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 746 clientID, redirectURI, null, state, e); 747 } 748 749 750 // Parse the optional response mode 751 v = params.get("response_mode"); 752 753 ResponseMode rm = null; 754 755 if (StringUtils.isNotBlank(v)) { 756 rm = new ResponseMode(v); 757 } 758 759 760 // Parse optional scope 761 v = params.get("scope"); 762 763 Scope scope = null; 764 765 if (StringUtils.isNotBlank(v)) 766 scope = Scope.parse(v); 767 768 769 // Parse optional code challenge and method for PKCE 770 CodeChallenge codeChallenge = null; 771 CodeChallengeMethod codeChallengeMethod = null; 772 773 v = params.get("code_challenge"); 774 775 if (StringUtils.isNotBlank(v)) 776 codeChallenge = new CodeChallenge(v); 777 778 if (codeChallenge != null) { 779 780 v = params.get("code_challenge_method"); 781 782 if (StringUtils.isNotBlank(v)) 783 codeChallengeMethod = CodeChallengeMethod.parse(v); 784 } 785 786 787 return new AuthorizationRequest(uri, rt, rm, clientID, redirectURI, scope, state, codeChallenge, codeChallengeMethod); 788 } 789 790 791 /** 792 * Parses an authorisation request from the specified URI query string. 793 * 794 * <p>Example URI query string: 795 * 796 * <pre> 797 * response_type=code 798 * &client_id=s6BhdRkqt3 799 * &state=xyz 800 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 801 * </pre> 802 * 803 * @param query The URI query string. Must not be {@code null}. 804 * 805 * @return The authorisation request. 806 * 807 * @throws ParseException If the query string couldn't be parsed to an 808 * authorisation request. 809 */ 810 public static AuthorizationRequest parse(final String query) 811 throws ParseException { 812 813 return parse(null, URLUtils.parseParameters(query)); 814 } 815 816 817 /** 818 * Parses an authorisation request from the specified URI query string. 819 * 820 * <p>Example URI query string: 821 * 822 * <pre> 823 * response_type=code 824 * &client_id=s6BhdRkqt3 825 * &state=xyz 826 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 827 * </pre> 828 * 829 * @param uri The URI of the authorisation endpoint. May be 830 * {@code null} if the {@link #toHTTPRequest()} method 831 * will not be used. 832 * @param query The URI query string. Must not be {@code null}. 833 * 834 * @return The authorisation request. 835 * 836 * @throws ParseException If the query string couldn't be parsed to an 837 * authorisation request. 838 */ 839 public static AuthorizationRequest parse(final URI uri, final String query) 840 throws ParseException { 841 842 return parse(uri, URLUtils.parseParameters(query)); 843 } 844 845 846 /** 847 * Parses an authorisation request from the specified URI. 848 * 849 * <p>Example URI: 850 * 851 * <pre> 852 * https://server.example.com/authorize? 853 * response_type=code 854 * &client_id=s6BhdRkqt3 855 * &state=xyz 856 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 857 * </pre> 858 * 859 * @param uri The URI. Must not be {@code null}. 860 * 861 * @return The authorisation request. 862 * 863 * @throws ParseException If the URI couldn't be parsed to an 864 * authorisation request. 865 */ 866 public static AuthorizationRequest parse(final URI uri) 867 throws ParseException { 868 869 return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery())); 870 } 871 872 873 /** 874 * Parses an authorisation request from the specified HTTP request. 875 * 876 * <p>Example HTTP request (GET): 877 * 878 * <pre> 879 * https://server.example.com/authorize? 880 * response_type=code 881 * &client_id=s6BhdRkqt3 882 * &state=xyz 883 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 884 * </pre> 885 * 886 * @param httpRequest The HTTP request. Must not be {@code null}. 887 * 888 * @return The authorisation request. 889 * 890 * @throws ParseException If the HTTP request couldn't be parsed to an 891 * authorisation request. 892 */ 893 public static AuthorizationRequest parse(final HTTPRequest httpRequest) 894 throws ParseException { 895 896 String query = httpRequest.getQuery(); 897 898 if (query == null) 899 throw new ParseException("Missing URI query string"); 900 901 try { 902 return parse(URIUtils.getBaseURI(httpRequest.getURL().toURI()), query); 903 904 } catch (URISyntaxException e) { 905 906 throw new ParseException(e.getMessage(), e); 907 } 908 } 909}