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