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