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