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