001package com.nimbusds.openid.connect.sdk; 002 003 004import java.net.URI; 005import java.net.URISyntaxException; 006import java.util.*; 007 008import com.nimbusds.jwt.JWT; 009import com.nimbusds.jwt.JWTParser; 010import com.nimbusds.langtag.LangTag; 011import com.nimbusds.langtag.LangTagException; 012import com.nimbusds.oauth2.sdk.*; 013import com.nimbusds.oauth2.sdk.http.HTTPRequest; 014import com.nimbusds.oauth2.sdk.id.ClientID; 015import com.nimbusds.oauth2.sdk.id.State; 016import com.nimbusds.oauth2.sdk.pkce.CodeChallenge; 017import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod; 018import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 019import com.nimbusds.oauth2.sdk.util.URIUtils; 020import com.nimbusds.oauth2.sdk.util.URLUtils; 021import com.nimbusds.openid.connect.sdk.claims.ACR; 022import net.jcip.annotations.Immutable; 023import net.minidev.json.JSONObject; 024import org.apache.commons.lang3.StringUtils; 025 026 027/** 028 * OpenID Connect authentication request. Intended to authenticate an end-user 029 * and request the end-user's authorisation to release information to the 030 * client. 031 * 032 * <p>Example HTTP request (code flow): 033 * 034 * <pre> 035 * https://server.example.com/op/authorize? 036 * response_type=code%20id_token 037 * &client_id=s6BhdRkqt3 038 * &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb 039 * &scope=openid 040 * &nonce=n-0S6_WzA2Mj 041 * &state=af0ifjsldkj 042 * </pre> 043 * 044 * <p>Related specifications: 045 * 046 * <ul> 047 * <li>OpenID Connect Core 1.0, section 3.1.2.1. 048 * <li>Proof Key for Code Exchange by OAuth Public Clients (RFC 7636). 049 * </ul> 050 */ 051@Immutable 052public class AuthenticationRequest extends AuthorizationRequest { 053 054 055 /** 056 * The nonce (required for implicit flow, optional for code flow). 057 */ 058 private final Nonce nonce; 059 060 061 /** 062 * The requested display type (optional). 063 */ 064 private final Display display; 065 066 067 /** 068 * The requested prompt (optional). 069 */ 070 private final Prompt prompt; 071 072 073 /** 074 * The required maximum authentication age, in seconds, 0 if not 075 * specified (optional). 076 */ 077 private final int maxAge; 078 079 080 /** 081 * The end-user's preferred languages and scripts for the user 082 * interface (optional). 083 */ 084 private final List<LangTag> uiLocales; 085 086 087 /** 088 * The end-user's preferred languages and scripts for claims being 089 * returned (optional). 090 */ 091 private final List<LangTag> claimsLocales; 092 093 094 /** 095 * Previously issued ID Token passed to the authorisation server as a 096 * hint about the end-user's current or past authenticated session with 097 * the client (optional). Should be present when {@code prompt=none} is 098 * used. 099 */ 100 private final JWT idTokenHint; 101 102 103 /** 104 * Hint to the authorisation server about the login identifier the 105 * end-user may use to log in (optional). 106 */ 107 private final String loginHint; 108 109 110 /** 111 * Requested Authentication Context Class Reference values (optional). 112 */ 113 private final List<ACR> acrValues; 114 115 116 /** 117 * Individual claims to be returned (optional). 118 */ 119 private final ClaimsRequest claims; 120 121 122 /** 123 * Request object (optional). 124 */ 125 private final JWT requestObject; 126 127 128 /** 129 * Request object URI (optional). 130 */ 131 private final URI requestURI; 132 133 134 /** 135 * Builder for constructing OpenID Connect authentication requests. 136 */ 137 public static class Builder { 138 139 140 /** 141 * The endpoint URI (optional). 142 */ 143 private URI uri; 144 145 146 /** 147 * The response type (required). 148 */ 149 private final ResponseType rt; 150 151 152 /** 153 * The client identifier (required). 154 */ 155 private final ClientID clientID; 156 157 158 /** 159 * The redirection URI where the response will be sent 160 * (required). 161 */ 162 private final URI redirectURI; 163 164 165 /** 166 * The scope (required). 167 */ 168 private final Scope scope; 169 170 171 /** 172 * The opaque value to maintain state between the request and 173 * the callback (recommended). 174 */ 175 private State state; 176 177 178 /** 179 * The nonce (required for implicit flow, optional for code 180 * flow). 181 */ 182 private Nonce nonce; 183 184 185 /** 186 * The requested display type (optional). 187 */ 188 private Display display; 189 190 191 /** 192 * The requested prompt (optional). 193 */ 194 private Prompt prompt; 195 196 197 /** 198 * The required maximum authentication age, in seconds, 0 if 199 * not specified (optional). 200 */ 201 private int maxAge; 202 203 204 /** 205 * The end-user's preferred languages and scripts for the user 206 * interface (optional). 207 */ 208 private List<LangTag> uiLocales; 209 210 211 /** 212 * The end-user's preferred languages and scripts for claims 213 * being returned (optional). 214 */ 215 private List<LangTag> claimsLocales; 216 217 218 /** 219 * Previously issued ID Token passed to the authorisation 220 * server as a hint about the end-user's current or past 221 * authenticated session with the client (optional). Should be 222 * present when {@code prompt=none} is used. 223 */ 224 private JWT idTokenHint; 225 226 227 /** 228 * Hint to the authorisation server about the login identifier 229 * the end-user may use to log in (optional). 230 */ 231 private String loginHint; 232 233 234 /** 235 * Requested Authentication Context Class Reference values 236 * (optional). 237 */ 238 private List<ACR> acrValues; 239 240 241 /** 242 * Individual claims to be returned (optional). 243 */ 244 private ClaimsRequest claims; 245 246 247 /** 248 * Request object (optional). 249 */ 250 private JWT requestObject; 251 252 253 /** 254 * Request object URI (optional). 255 */ 256 private URI requestURI; 257 258 259 /** 260 * The response mode (optional). 261 */ 262 private ResponseMode rm; 263 264 265 /** 266 * The authorisation code challenge for PKCE (optional). 267 */ 268 private CodeChallenge codeChallenge; 269 270 271 /** 272 * The authorisation code challenge method for PKCE (optional). 273 */ 274 private CodeChallengeMethod codeChallengeMethod; 275 276 277 /** 278 * Creates a new OpenID Connect authentication request builder. 279 * 280 * @param rt The response type. Corresponds to the 281 * {@code response_type} parameter. Must 282 * specify a valid OpenID Connect response 283 * type. Must not be {@code null}. 284 * @param scope The request scope. Corresponds to the 285 * {@code scope} parameter. Must contain an 286 * {@link OIDCScopeValue#OPENID openid 287 * value}. Must not be {@code null}. 288 * @param clientID The client identifier. Corresponds to the 289 * {@code client_id} parameter. Must not be 290 * {@code null}. 291 * @param redirectURI The redirection URI. Corresponds to the 292 * {@code redirect_uri} parameter. Must not 293 * be {@code null} unless set by means of 294 * the optional {@code request_object} / 295 * {@code request_uri} parameter. 296 */ 297 public Builder(final ResponseType rt, 298 final Scope scope, 299 final ClientID clientID, 300 final URI redirectURI) { 301 302 if (rt == null) 303 throw new IllegalArgumentException("The response type must not be null"); 304 305 OIDCResponseTypeValidator.validate(rt); 306 307 this.rt = rt; 308 309 if (scope == null) 310 throw new IllegalArgumentException("The scope must not be null"); 311 312 if (! scope.contains(OIDCScopeValue.OPENID)) 313 throw new IllegalArgumentException("The scope must include an \"openid\" value"); 314 315 this.scope = scope; 316 317 if (clientID == null) 318 throw new IllegalArgumentException("The client ID must not be null"); 319 320 this.clientID = clientID; 321 322 // Check presence at build time 323 this.redirectURI = redirectURI; 324 } 325 326 327 /** 328 * Sets the state. Corresponds to the recommended {@code state} 329 * parameter. 330 * 331 * @param state The state, {@code null} if not specified. 332 * 333 * @return This builder. 334 */ 335 public Builder state(final State state) { 336 337 this.state = state; 338 return this; 339 } 340 341 342 /** 343 * Sets the URI of the endpoint (HTTP or HTTPS) for which the 344 * request is intended. 345 * 346 * @param uri The endpoint URI, {@code null} if not specified. 347 * 348 * @return This builder. 349 */ 350 public Builder endpointURI(final URI uri) { 351 352 this.uri = uri; 353 return this; 354 } 355 356 357 /** 358 * Sets the nonce. Corresponds to the conditionally optional 359 * {@code nonce} parameter. 360 * 361 * @param nonce The nonce, {@code null} if not specified. 362 */ 363 public Builder nonce(final Nonce nonce) { 364 365 this.nonce = nonce; 366 return this; 367 } 368 369 370 /** 371 * Sets the requested display type. Corresponds to the optional 372 * {@code display} parameter. 373 * 374 * @param display The requested display type, {@code null} if 375 * not specified. 376 */ 377 public Builder display(final Display display) { 378 379 this.display = display; 380 return this; 381 } 382 383 384 /** 385 * Sets the requested prompt. Corresponds to the optional 386 * {@code prompt} parameter. 387 * 388 * @param prompt The requested prompt, {@code null} if not 389 * specified. 390 */ 391 public Builder prompt(final Prompt prompt) { 392 393 this.prompt = prompt; 394 return this; 395 } 396 397 398 /** 399 * Sets the required maximum authentication age. Corresponds to 400 * the optional {@code max_age} parameter. 401 * 402 * @param maxAge The maximum authentication age, in seconds; 0 403 * if not specified. 404 */ 405 public Builder maxAge(final int maxAge) { 406 407 this.maxAge = maxAge; 408 return this; 409 } 410 411 412 /** 413 * Sets the end-user's preferred languages and scripts for the 414 * user interface, ordered by preference. Corresponds to the 415 * optional {@code ui_locales} parameter. 416 * 417 * @param uiLocales The preferred UI locales, {@code null} if 418 * not specified. 419 */ 420 public Builder uiLocales(final List<LangTag> uiLocales) { 421 422 this.uiLocales = uiLocales; 423 return this; 424 } 425 426 427 /** 428 * Sets the end-user's preferred languages and scripts for the 429 * claims being returned, ordered by preference. Corresponds to 430 * the optional {@code claims_locales} parameter. 431 * 432 * @param claimsLocales The preferred claims locales, 433 * {@code null} if not specified. 434 */ 435 public Builder claimsLocales(final List<LangTag> claimsLocales) { 436 437 this.claimsLocales = claimsLocales; 438 return this; 439 } 440 441 442 /** 443 * Sets the ID Token hint. Corresponds to the conditionally 444 * optional {@code id_token_hint} parameter. 445 * 446 * @param idTokenHint The ID Token hint, {@code null} if not 447 * specified. 448 */ 449 public Builder idTokenHint(final JWT idTokenHint) { 450 451 this.idTokenHint = idTokenHint; 452 return this; 453 } 454 455 456 /** 457 * Sets the login hint. Corresponds to the optional 458 * {@code login_hint} parameter. 459 * 460 * @param loginHint The login hint, {@code null} if not 461 * specified. 462 */ 463 public Builder loginHint(final String loginHint) { 464 465 this.loginHint = loginHint; 466 return this; 467 } 468 469 470 /** 471 * Sets the requested Authentication Context Class Reference 472 * values. Corresponds to the optional {@code acr_values} 473 * parameter. 474 * 475 * @param acrValues The requested ACR values, {@code null} if 476 * not specified. 477 */ 478 public Builder acrValues(final List<ACR> acrValues) { 479 480 this.acrValues = acrValues; 481 return this; 482 } 483 484 485 /** 486 * Sets the individual claims to be returned. Corresponds to 487 * the optional {@code claims} parameter. 488 * 489 * @param claims The individual claims to be returned, 490 * {@code null} if not specified. 491 */ 492 public Builder claims(final ClaimsRequest claims) { 493 494 this.claims = claims; 495 return this; 496 } 497 498 499 /** 500 * Sets the request object. Corresponds to the optional 501 * {@code request} parameter. Must not be specified together 502 * with a request object URI. 503 * 504 * @return The request object, {@code null} if not specified. 505 */ 506 public Builder requestObject(final JWT requestObject) { 507 508 this.requestObject = requestObject; 509 return this; 510 } 511 512 513 /** 514 * Sets the request object URI. Corresponds to the optional 515 * {@code request_uri} parameter. Must not be specified 516 * together with a request object. 517 * 518 * @param requestURI The request object URI, {@code null} if 519 * not specified. 520 */ 521 public Builder requestURI(final URI requestURI) { 522 523 this.requestURI = requestURI; 524 return this; 525 } 526 527 528 /** 529 * Sets the response mode. Corresponds to the optional 530 * {@code response_mode} parameter. Use of this parameter is 531 * not recommended unless a non-default response mode is 532 * requested (e.g. form_post). 533 * 534 * @param rm The response mode, {@code null} if not specified. 535 * 536 * @return This builder. 537 */ 538 public Builder responseMode(final ResponseMode rm) { 539 540 this.rm = rm; 541 return this; 542 } 543 544 545 /** 546 * Sets the code challenge for Proof Key for Code Exchange 547 * (PKCE) by public OAuth clients. 548 * 549 * @param codeChallenge The code challenge, {@code null} 550 * if not specified. 551 * @param codeChallengeMethod The code challenge method, 552 * {@code null} if not specified. 553 * 554 * @return This builder. 555 */ 556 public Builder codeChallenge(final CodeChallenge codeChallenge, final CodeChallengeMethod codeChallengeMethod) { 557 558 this.codeChallenge = codeChallenge; 559 this.codeChallengeMethod = codeChallengeMethod; 560 return this; 561 } 562 563 564 /** 565 * Builds a new authentication request. 566 * 567 * @return The authentication request. 568 */ 569 public AuthenticationRequest build() { 570 571 try { 572 return new AuthenticationRequest( 573 uri, rt, rm, scope, clientID, redirectURI, state, nonce, 574 display, prompt, maxAge, uiLocales, claimsLocales, 575 idTokenHint, loginHint, acrValues, claims, 576 requestObject, requestURI, 577 codeChallenge, codeChallengeMethod); 578 579 } catch (IllegalArgumentException e) { 580 581 throw new IllegalStateException(e.getMessage(), e); 582 } 583 } 584 } 585 586 587 /** 588 * Creates a new minimal OpenID Connect authentication request. 589 * 590 * @param uri The URI of the OAuth 2.0 authorisation endpoint. 591 * May be {@code null} if the {@link #toHTTPRequest} 592 * method will not be used. 593 * @param rt The response type. Corresponds to the 594 * {@code response_type} parameter. Must specify a 595 * valid OpenID Connect response type. Must not be 596 * {@code null}. 597 * @param scope The request scope. Corresponds to the 598 * {@code scope} parameter. Must contain an 599 * {@link OIDCScopeValue#OPENID openid value}. Must 600 * not be {@code null}. 601 * @param clientID The client identifier. Corresponds to the 602 * {@code client_id} parameter. Must not be 603 * {@code null}. 604 * @param redirectURI The redirection URI. Corresponds to the 605 * {@code redirect_uri} parameter. Must not be 606 * {@code null}. 607 * @param state The state. Corresponds to the {@code state} 608 * parameter. May be {@code null}. 609 * @param nonce The nonce. Corresponds to the {@code nonce} 610 * parameter. May be {@code null} for code flow. 611 */ 612 public AuthenticationRequest(final URI uri, 613 final ResponseType rt, 614 final Scope scope, 615 final ClientID clientID, 616 final URI redirectURI, 617 final State state, 618 final Nonce nonce) { 619 620 // Not specified: display, prompt, maxAge, uiLocales, claimsLocales, 621 // idTokenHint, loginHint, acrValues, claims 622 // codeChallenge, codeChallengeMethod 623 this(uri, rt, null, scope, clientID, redirectURI, state, nonce, 624 null, null, 0, null, null, 625 null, null, null, null, null, null, 626 null, null); 627 } 628 629 630 /** 631 * Creates a new OpenID Connect authentication request. 632 * 633 * @param uri The URI of the OAuth 2.0 authorisation 634 * endpoint. May be {@code null} if the 635 * {@link #toHTTPRequest} method will not be 636 * used. 637 * @param rt The response type set. Corresponds to the 638 * {@code response_type} parameter. Must 639 * specify a valid OpenID Connect response 640 * type. Must not be {@code null}. 641 * @param rm The response mode. Corresponds to the 642 * optional {@code response_mode} parameter. 643 * Use of this parameter is not recommended 644 * unless a non-default response mode is 645 * requested (e.g. form_post). 646 * @param scope The request scope. Corresponds to the 647 * {@code scope} parameter. Must contain an 648 * {@link OIDCScopeValue#OPENID openid value}. 649 * Must not be {@code null}. 650 * @param clientID The client identifier. Corresponds to the 651 * {@code client_id} parameter. Must not be 652 * {@code null}. 653 * @param redirectURI The redirection URI. Corresponds to the 654 * {@code redirect_uri} parameter. Must not 655 * be {@code null} unless set by means of 656 * the optional {@code request_object} / 657 * {@code request_uri} parameter. 658 * @param state The state. Corresponds to the recommended 659 * {@code state} parameter. {@code null} if 660 * not specified. 661 * @param nonce The nonce. Corresponds to the 662 * {@code nonce} parameter. May be 663 * {@code null} for code flow. 664 * @param display The requested display type. Corresponds 665 * to the optional {@code display} 666 * parameter. 667 * {@code null} if not specified. 668 * @param prompt The requested prompt. Corresponds to the 669 * optional {@code prompt} parameter. 670 * {@code null} if not specified. 671 * @param maxAge The required maximum authentication age, 672 * in seconds. Corresponds to the optional 673 * {@code max_age} parameter. Zero if not 674 * specified. 675 * @param uiLocales The preferred languages and scripts for 676 * the user interface. Corresponds to the 677 * optional {@code ui_locales} parameter. 678 * {@code null} if not specified. 679 * @param claimsLocales The preferred languages and scripts for 680 * claims being returned. Corresponds to the 681 * optional {@code claims_locales} 682 * parameter. {@code null} if not specified. 683 * @param idTokenHint The ID Token hint. Corresponds to the 684 * optional {@code id_token_hint} parameter. 685 * {@code null} if not specified. 686 * @param loginHint The login hint. Corresponds to the 687 * optional {@code login_hint} parameter. 688 * {@code null} if not specified. 689 * @param acrValues The requested Authentication Context 690 * Class Reference values. Corresponds to 691 * the optional {@code acr_values} 692 * parameter. {@code null} if not specified. 693 * @param claims The individual claims to be returned. 694 * Corresponds to the optional 695 * {@code claims} parameter. {@code null} if 696 * not specified. 697 * @param requestObject The request object. Corresponds to the 698 * optional {@code request} parameter. Must 699 * not be specified together with a request 700 * object URI. {@code null} if not 701 * specified. 702 * @param requestURI The request object URI. Corresponds to 703 * the optional {@code request_uri} 704 * parameter. Must not be specified together 705 * with a request object. {@code null} if 706 * not specified. 707 * @param codeChallenge The code challenge for PKCE, {@code null} 708 * if not specified. 709 * @param codeChallengeMethod The code challenge method for PKCE, 710 * {@code null} if not specified. 711 */ 712 public AuthenticationRequest(final URI uri, 713 final ResponseType rt, 714 final ResponseMode rm, 715 final Scope scope, 716 final ClientID clientID, 717 final URI redirectURI, 718 final State state, 719 final Nonce nonce, 720 final Display display, 721 final Prompt prompt, 722 final int maxAge, 723 final List<LangTag> uiLocales, 724 final List<LangTag> claimsLocales, 725 final JWT idTokenHint, 726 final String loginHint, 727 final List<ACR> acrValues, 728 final ClaimsRequest claims, 729 final JWT requestObject, 730 final URI requestURI, 731 final CodeChallenge codeChallenge, 732 final CodeChallengeMethod codeChallengeMethod) { 733 734 super(uri, rt, rm, clientID, redirectURI, scope, state, codeChallenge, codeChallengeMethod); 735 736 // Redirect URI required unless set in request_object / request_uri 737 if (redirectURI == null && requestObject == null && requestURI == null) 738 throw new IllegalArgumentException("The redirection URI must not be null"); 739 740 OIDCResponseTypeValidator.validate(rt); 741 742 if (scope == null) 743 throw new IllegalArgumentException("The scope must not be null"); 744 745 if (! scope.contains(OIDCScopeValue.OPENID)) 746 throw new IllegalArgumentException("The scope must include an \"openid\" token"); 747 748 749 // Nonce required for implicit protocol flow 750 if (rt.impliesImplicitFlow() && nonce == null) 751 throw new IllegalArgumentException("Nonce is required in implicit / hybrid protocol flow"); 752 753 this.nonce = nonce; 754 755 // Optional parameters 756 this.display = display; 757 this.prompt = prompt; 758 this.maxAge = maxAge; 759 760 if (uiLocales != null) 761 this.uiLocales = Collections.unmodifiableList(uiLocales); 762 else 763 this.uiLocales = null; 764 765 if (claimsLocales != null) 766 this.claimsLocales = Collections.unmodifiableList(claimsLocales); 767 else 768 this.claimsLocales = null; 769 770 this.idTokenHint = idTokenHint; 771 this.loginHint = loginHint; 772 773 if (acrValues != null) 774 this.acrValues = Collections.unmodifiableList(acrValues); 775 else 776 this.acrValues = null; 777 778 this.claims = claims; 779 780 if (requestObject != null && requestURI != null) 781 throw new IllegalArgumentException("Either a request object or a request URI must be specified, but not both"); 782 783 this.requestObject = requestObject; 784 this.requestURI = requestURI; 785 } 786 787 788 /** 789 * Gets the nonce. Corresponds to the conditionally optional 790 * {@code nonce} parameter. 791 * 792 * @return The nonce, {@code null} if not specified. 793 */ 794 public Nonce getNonce() { 795 796 return nonce; 797 } 798 799 800 /** 801 * Gets the requested display type. Corresponds to the optional 802 * {@code display} parameter. 803 * 804 * @return The requested display type, {@code null} if not specified. 805 */ 806 public Display getDisplay() { 807 808 return display; 809 } 810 811 812 /** 813 * Gets the requested prompt. Corresponds to the optional 814 * {@code prompt} parameter. 815 * 816 * @return The requested prompt, {@code null} if not specified. 817 */ 818 public Prompt getPrompt() { 819 820 return prompt; 821 } 822 823 824 /** 825 * Gets the required maximum authentication age. Corresponds to the 826 * optional {@code max_age} parameter. 827 * 828 * @return The maximum authentication age, in seconds; 0 if not 829 * specified. 830 */ 831 public int getMaxAge() { 832 833 return maxAge; 834 } 835 836 837 /** 838 * Gets the end-user's preferred languages and scripts for the user 839 * interface, ordered by preference. Corresponds to the optional 840 * {@code ui_locales} parameter. 841 * 842 * @return The preferred UI locales, {@code null} if not specified. 843 */ 844 public List<LangTag> getUILocales() { 845 846 return uiLocales; 847 } 848 849 850 /** 851 * Gets the end-user's preferred languages and scripts for the claims 852 * being returned, ordered by preference. Corresponds to the optional 853 * {@code claims_locales} parameter. 854 * 855 * @return The preferred claims locales, {@code null} if not specified. 856 */ 857 public List<LangTag> getClaimsLocales() { 858 859 return claimsLocales; 860 } 861 862 863 /** 864 * Gets the ID Token hint. Corresponds to the conditionally optional 865 * {@code id_token_hint} parameter. 866 * 867 * @return The ID Token hint, {@code null} if not specified. 868 */ 869 public JWT getIDTokenHint() { 870 871 return idTokenHint; 872 } 873 874 875 /** 876 * Gets the login hint. Corresponds to the optional {@code login_hint} 877 * parameter. 878 * 879 * @return The login hint, {@code null} if not specified. 880 */ 881 public String getLoginHint() { 882 883 return loginHint; 884 } 885 886 887 /** 888 * Gets the requested Authentication Context Class Reference values. 889 * Corresponds to the optional {@code acr_values} parameter. 890 * 891 * @return The requested ACR values, {@code null} if not specified. 892 */ 893 public List<ACR> getACRValues() { 894 895 return acrValues; 896 } 897 898 899 /** 900 * Gets the individual claims to be returned. Corresponds to the 901 * optional {@code claims} parameter. 902 * 903 * @return The individual claims to be returned, {@code null} if not 904 * specified. 905 */ 906 public ClaimsRequest getClaims() { 907 908 return claims; 909 } 910 911 912 /** 913 * Gets the request object. Corresponds to the optional {@code request} 914 * parameter. 915 * 916 * @return The request object, {@code null} if not specified. 917 */ 918 public JWT getRequestObject() { 919 920 return requestObject; 921 } 922 923 924 /** 925 * Gets the request object URI. Corresponds to the optional 926 * {@code request_uri} parameter. 927 * 928 * @return The request object URI, {@code null} if not specified. 929 */ 930 public URI getRequestURI() { 931 932 return requestURI; 933 } 934 935 936 /** 937 * Returns {@code true} if this authentication request specifies an 938 * OpenID Connect request object (directly through the {@code request} 939 * parameter or by reference through the {@code request_uri} parameter). 940 * 941 * @return {@code true} if a request object is specified, else 942 * {@code false}. 943 */ 944 public boolean specifiesRequestObject() { 945 946 return requestObject != null || requestURI != null; 947 } 948 949 950 @Override 951 public Map<String,String> toParameters() { 952 953 Map <String,String> params = super.toParameters(); 954 955 if (nonce != null) 956 params.put("nonce", nonce.toString()); 957 958 if (display != null) 959 params.put("display", display.toString()); 960 961 if (prompt != null) 962 params.put("prompt", prompt.toString()); 963 964 if (maxAge > 0) 965 params.put("max_age", "" + maxAge); 966 967 if (uiLocales != null) { 968 969 StringBuilder sb = new StringBuilder(); 970 971 for (LangTag locale: uiLocales) { 972 973 if (sb.length() > 0) 974 sb.append(' '); 975 976 sb.append(locale.toString()); 977 } 978 979 params.put("ui_locales", sb.toString()); 980 } 981 982 if (claimsLocales != null) { 983 984 StringBuilder sb = new StringBuilder(); 985 986 for (LangTag locale: claimsLocales) { 987 988 if (sb.length() > 0) 989 sb.append(' '); 990 991 sb.append(locale.toString()); 992 } 993 994 params.put("claims_locales", sb.toString()); 995 } 996 997 if (idTokenHint != null) { 998 999 try { 1000 params.put("id_token_hint", idTokenHint.serialize()); 1001 1002 } catch (IllegalStateException e) { 1003 1004 throw new SerializeException("Couldn't serialize ID token hint: " + e.getMessage(), e); 1005 } 1006 } 1007 1008 if (loginHint != null) 1009 params.put("login_hint", loginHint); 1010 1011 if (acrValues != null) { 1012 1013 StringBuilder sb = new StringBuilder(); 1014 1015 for (ACR acr: acrValues) { 1016 1017 if (sb.length() > 0) 1018 sb.append(' '); 1019 1020 sb.append(acr.toString()); 1021 } 1022 1023 params.put("acr_values", sb.toString()); 1024 } 1025 1026 1027 if (claims != null) 1028 params.put("claims", claims.toJSONObject().toString()); 1029 1030 if (requestObject != null) { 1031 1032 try { 1033 params.put("request", requestObject.serialize()); 1034 1035 } catch (IllegalStateException e) { 1036 1037 throw new SerializeException("Couldn't serialize request object to JWT: " + e.getMessage(), e); 1038 } 1039 } 1040 1041 if (requestURI != null) 1042 params.put("request_uri", requestURI.toString()); 1043 1044 return params; 1045 } 1046 1047 1048 /** 1049 * Parses an OpenID Connect authentication request from the specified 1050 * parameters. 1051 * 1052 * <p>Example parameters: 1053 * 1054 * <pre> 1055 * response_type = token id_token 1056 * client_id = s6BhdRkqt3 1057 * redirect_uri = https://client.example.com/cb 1058 * scope = openid profile 1059 * state = af0ifjsldkj 1060 * nonce = -0S6_WzA2Mj 1061 * </pre> 1062 * 1063 * @param params The parameters. Must not be {@code null}. 1064 * 1065 * @return The OpenID Connect authentication request. 1066 * 1067 * @throws ParseException If the parameters couldn't be parsed to an 1068 * OpenID Connect authentication request. 1069 */ 1070 public static AuthenticationRequest parse(final Map<String,String> params) 1071 throws ParseException { 1072 1073 return parse(null, params); 1074 } 1075 1076 1077 /** 1078 * Parses an OpenID Connect authentication request from the specified 1079 * parameters. 1080 * 1081 * <p>Example parameters: 1082 * 1083 * <pre> 1084 * response_type = token id_token 1085 * client_id = s6BhdRkqt3 1086 * redirect_uri = https://client.example.com/cb 1087 * scope = openid profile 1088 * state = af0ifjsldkj 1089 * nonce = -0S6_WzA2Mj 1090 * </pre> 1091 * 1092 * @param uri The URI of the OAuth 2.0 authorisation endpoint. May 1093 * be {@code null} if the {@link #toHTTPRequest} method 1094 * will not be used. 1095 * @param params The parameters. Must not be {@code null}. 1096 * 1097 * @return The OpenID Connect authentication request. 1098 * 1099 * @throws ParseException If the parameters couldn't be parsed to an 1100 * OpenID Connect authentication request. 1101 */ 1102 public static AuthenticationRequest parse(final URI uri, final Map<String,String> params) 1103 throws ParseException { 1104 1105 // Parse and validate the core OAuth 2.0 autz request params in 1106 // the context of OIDC 1107 AuthorizationRequest ar = AuthorizationRequest.parse(uri, params); 1108 1109 ClientID clientID = ar.getClientID(); 1110 State state = ar.getState(); 1111 ResponseMode rm = ar.getResponseMode(); 1112 1113 // Required in OIDC, check later after optional request_object / request_uri is parsed 1114 URI redirectURI = ar.getRedirectionURI(); 1115 1116 ResponseType rt = ar.getResponseType(); 1117 1118 try { 1119 OIDCResponseTypeValidator.validate(rt); 1120 1121 } catch (IllegalArgumentException e) { 1122 String msg = "Unsupported \"response_type\" parameter: " + e.getMessage(); 1123 throw new ParseException(msg, OAuth2Error.UNSUPPORTED_RESPONSE_TYPE.appendDescription(": " + msg), 1124 clientID, redirectURI, ar.impliedResponseMode(), state); 1125 } 1126 1127 // Required in OIDC, must include "openid" parameter 1128 Scope scope = ar.getScope(); 1129 1130 if (scope == null) { 1131 String msg = "Missing \"scope\" parameter"; 1132 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1133 clientID, redirectURI, ar.impliedResponseMode(), state); 1134 } 1135 1136 if (! scope.contains(OIDCScopeValue.OPENID)) { 1137 String msg = "The scope must include an \"openid\" value"; 1138 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1139 clientID, redirectURI, ar.impliedResponseMode(), state); 1140 } 1141 1142 1143 // Parse the remaining OIDC parameters 1144 Nonce nonce = Nonce.parse(params.get("nonce")); 1145 1146 // Nonce required in implicit flow 1147 if (rt.impliesImplicitFlow() && nonce == null) { 1148 String msg = "Missing \"nonce\" parameter: Required in implicit flow"; 1149 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1150 clientID, redirectURI, ar.impliedResponseMode(), state); 1151 } 1152 1153 Display display = null; 1154 1155 if (params.containsKey("display")) { 1156 try { 1157 display = Display.parse(params.get("display")); 1158 1159 } catch (ParseException e) { 1160 String msg = "Invalid \"display\" parameter: " + e.getMessage(); 1161 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1162 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1163 } 1164 } 1165 1166 1167 Prompt prompt; 1168 1169 try { 1170 prompt = Prompt.parse(params.get("prompt")); 1171 1172 } catch (ParseException e) { 1173 String msg = "Invalid \"prompt\" parameter: " + e.getMessage(); 1174 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1175 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1176 } 1177 1178 1179 String v = params.get("max_age"); 1180 1181 int maxAge = 0; 1182 1183 if (StringUtils.isNotBlank(v)) { 1184 1185 try { 1186 maxAge = Integer.parseInt(v); 1187 1188 } catch (NumberFormatException e) { 1189 String msg = "Invalid \"max_age\" parameter: " + v; 1190 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1191 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1192 } 1193 } 1194 1195 1196 v = params.get("ui_locales"); 1197 1198 List<LangTag> uiLocales = null; 1199 1200 if (StringUtils.isNotBlank(v)) { 1201 1202 uiLocales = new LinkedList<>(); 1203 1204 StringTokenizer st = new StringTokenizer(v, " "); 1205 1206 while (st.hasMoreTokens()) { 1207 1208 try { 1209 uiLocales.add(LangTag.parse(st.nextToken())); 1210 1211 } catch (LangTagException e) { 1212 String msg = "Invalid \"ui_locales\" parameter: " + e.getMessage(); 1213 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1214 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1215 } 1216 } 1217 } 1218 1219 1220 v = params.get("claims_locales"); 1221 1222 List<LangTag> claimsLocales = null; 1223 1224 if (StringUtils.isNotBlank(v)) { 1225 1226 claimsLocales = new LinkedList<>(); 1227 1228 StringTokenizer st = new StringTokenizer(v, " "); 1229 1230 while (st.hasMoreTokens()) { 1231 1232 try { 1233 claimsLocales.add(LangTag.parse(st.nextToken())); 1234 1235 } catch (LangTagException e) { 1236 String msg = "Invalid \"claims_locales\" parameter: " + e.getMessage(); 1237 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1238 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1239 } 1240 } 1241 } 1242 1243 1244 v = params.get("id_token_hint"); 1245 1246 JWT idTokenHint = null; 1247 1248 if (StringUtils.isNotBlank(v)) { 1249 1250 try { 1251 idTokenHint = JWTParser.parse(v); 1252 1253 } catch (java.text.ParseException e) { 1254 String msg = "Invalid \"id_token_hint\" parameter: " + e.getMessage(); 1255 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1256 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1257 } 1258 } 1259 1260 String loginHint = params.get("login_hint"); 1261 1262 1263 v = params.get("acr_values"); 1264 1265 List<ACR> acrValues = null; 1266 1267 if (StringUtils.isNotBlank(v)) { 1268 1269 acrValues = new LinkedList<>(); 1270 1271 StringTokenizer st = new StringTokenizer(v, " "); 1272 1273 while (st.hasMoreTokens()) { 1274 1275 acrValues.add(new ACR(st.nextToken())); 1276 } 1277 } 1278 1279 1280 v = params.get("claims"); 1281 1282 ClaimsRequest claims = null; 1283 1284 if (StringUtils.isNotBlank(v)) { 1285 1286 JSONObject jsonObject; 1287 1288 try { 1289 jsonObject = JSONObjectUtils.parse(v); 1290 1291 } catch (ParseException e) { 1292 String msg = "Invalid \"claims\" parameter: " + e.getMessage(); 1293 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1294 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1295 } 1296 1297 // Parse exceptions silently ignored 1298 claims = ClaimsRequest.parse(jsonObject); 1299 } 1300 1301 1302 v = params.get("request_uri"); 1303 1304 URI requestURI = null; 1305 1306 if (StringUtils.isNotBlank(v)) { 1307 1308 try { 1309 requestURI = new URI(v); 1310 1311 } catch (URISyntaxException e) { 1312 String msg = "Invalid \"request_uri\" parameter: " + e.getMessage(); 1313 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1314 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1315 } 1316 } 1317 1318 v = params.get("request"); 1319 1320 JWT requestObject = null; 1321 1322 if (StringUtils.isNotBlank(v)) { 1323 1324 // request_object and request_uri must not be defined at the same time 1325 if (requestURI != null) { 1326 String msg = "Invalid request: Found mutually exclusive \"request\" and \"request_uri\" parameters"; 1327 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1328 clientID, redirectURI, ar.impliedResponseMode(), state, null); 1329 } 1330 1331 try { 1332 requestObject = JWTParser.parse(v); 1333 1334 } catch (java.text.ParseException e) { 1335 String msg = "Invalid \"request_object\" parameter: " + e.getMessage(); 1336 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1337 clientID, redirectURI, ar.impliedResponseMode(), state, e); 1338 } 1339 } 1340 1341 1342 // Redirect URI required unless request_object / request_uri present 1343 if (redirectURI == null && requestObject == null && requestURI == null) { 1344 String msg = "Missing \"redirect_uri\" parameter"; 1345 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg), 1346 clientID, null, ar.impliedResponseMode(), state); 1347 } 1348 1349 1350 return new AuthenticationRequest( 1351 uri, rt, rm, scope, clientID, redirectURI, state, nonce, 1352 display, prompt, maxAge, uiLocales, claimsLocales, 1353 idTokenHint, loginHint, acrValues, claims, requestObject, requestURI, 1354 ar.getCodeChallenge(), ar.getCodeChallengeMethod()); 1355 } 1356 1357 1358 /** 1359 * Parses an OpenID Connect authentication request from the specified 1360 * URI query string. 1361 * 1362 * <p>Example URI query string: 1363 * 1364 * <pre> 1365 * response_type=token%20id_token 1366 * &client_id=s6BhdRkqt3 1367 * &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb 1368 * &scope=openid%20profile 1369 * &state=af0ifjsldkj 1370 * &nonce=n-0S6_WzA2Mj 1371 * </pre> 1372 * 1373 * @param query The URI query string. Must not be {@code null}. 1374 * 1375 * @return The OpenID Connect authentication request. 1376 * 1377 * @throws ParseException If the query string couldn't be parsed to an 1378 * OpenID Connect authentication request. 1379 */ 1380 public static AuthenticationRequest parse(final String query) 1381 throws ParseException { 1382 1383 return parse(null, URLUtils.parseParameters(query)); 1384 } 1385 1386 1387 /** 1388 * Parses an OpenID Connect authentication request from the specified 1389 * URI query string. 1390 * 1391 * <p>Example URI query string: 1392 * 1393 * <pre> 1394 * response_type=token%20id_token 1395 * &client_id=s6BhdRkqt3 1396 * &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb 1397 * &scope=openid%20profile 1398 * &state=af0ifjsldkj 1399 * &nonce=n-0S6_WzA2Mj 1400 * </pre> 1401 * 1402 * @param uri The URI of the OAuth 2.0 authorisation endpoint. May be 1403 * {@code null} if the {@link #toHTTPRequest} method will 1404 * not be used. 1405 * @param query The URI query string. Must not be {@code null}. 1406 * 1407 * @return The OpenID Connect authentication request. 1408 * 1409 * @throws ParseException If the query string couldn't be parsed to an 1410 * OpenID Connect authentication request. 1411 */ 1412 public static AuthenticationRequest parse(final URI uri, final String query) 1413 throws ParseException { 1414 1415 return parse(uri, URLUtils.parseParameters(query)); 1416 } 1417 1418 1419 /** 1420 * Parses an OpenID Connect authentication request from the specified 1421 * URI. 1422 * 1423 * <p>Example URI: 1424 * 1425 * <pre> 1426 * https://server.example.com/authorize? 1427 * response_type=token%20id_token 1428 * &client_id=s6BhdRkqt3 1429 * &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb 1430 * &scope=openid%20profile 1431 * &state=af0ifjsldkj 1432 * &nonce=n-0S6_WzA2Mj 1433 * </pre> 1434 * 1435 * @param uri The URI. Must not be {@code null}. 1436 * 1437 * @return The OpenID Connect authentication request. 1438 * 1439 * @throws ParseException If the query string couldn't be parsed to an 1440 * OpenID Connect authentication request. 1441 */ 1442 public static AuthenticationRequest parse(final URI uri) 1443 throws ParseException { 1444 1445 return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery())); 1446 } 1447 1448 1449 /** 1450 * Parses an authentication request from the specified HTTP GET or HTTP 1451 * POST request. 1452 * 1453 * <p>Example HTTP request (GET): 1454 * 1455 * <pre> 1456 * https://server.example.com/op/authorize? 1457 * response_type=code%20id_token 1458 * &client_id=s6BhdRkqt3 1459 * &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb 1460 * &scope=openid 1461 * &nonce=n-0S6_WzA2Mj 1462 * &state=af0ifjsldkj 1463 * </pre> 1464 * 1465 * @param httpRequest The HTTP request. Must not be {@code null}. 1466 * 1467 * @return The OpenID Connect authentication request. 1468 * 1469 * @throws ParseException If the HTTP request couldn't be parsed to an 1470 * OpenID Connect authentication request. 1471 */ 1472 public static AuthenticationRequest parse(final HTTPRequest httpRequest) 1473 throws ParseException { 1474 1475 String query = httpRequest.getQuery(); 1476 1477 if (query == null) 1478 throw new ParseException("Missing URI query string"); 1479 1480 URI endpointURI; 1481 1482 try { 1483 endpointURI = httpRequest.getURL().toURI(); 1484 1485 } catch (URISyntaxException e) { 1486 1487 throw new ParseException(e.getMessage(), e); 1488 } 1489 1490 return parse(endpointURI, query); 1491 } 1492}