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