001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2023, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.oauth2.sdk; 019 020 021import com.nimbusds.common.contenttype.ContentType; 022import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; 023import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; 024import com.nimbusds.oauth2.sdk.http.HTTPRequest; 025import com.nimbusds.oauth2.sdk.id.ClientID; 026import com.nimbusds.oauth2.sdk.rar.AuthorizationDetail; 027import com.nimbusds.oauth2.sdk.token.RefreshToken; 028import com.nimbusds.oauth2.sdk.util.*; 029import net.jcip.annotations.Immutable; 030 031import java.net.URI; 032import java.net.URISyntaxException; 033import java.util.*; 034 035 036/** 037 * Token request. Used to obtain an 038 * {@link com.nimbusds.oauth2.sdk.token.AccessToken access token} and an 039 * optional {@link com.nimbusds.oauth2.sdk.token.RefreshToken refresh token} 040 * at the Token endpoint of the authorisation server. Supports custom request 041 * parameters. 042 * 043 * <p>Example token request with an authorisation code grant: 044 * 045 * <pre> 046 * POST /token HTTP/1.1 047 * Host: server.example.com 048 * Content-Type: application/x-www-form-urlencoded 049 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 050 * 051 * grant_type=authorization_code 052 * &code=SplxlOBeZQQYbYS6WxSbIA 053 * &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb 054 * </pre> 055 * 056 * <p>Related specifications: 057 * 058 * <ul> 059 * <li>OAuth 2.0 (RFC 6749), sections 4.1.3, 4.3.2, 4.4.2 and 6. 060 * <li>OAuth 2.0 Rich Authorization Requests (RFC 9396), section 6. 061 * <li>Resource Indicators for OAuth 2.0 (RFC 8707) 062 * <li>OAuth 2.0 Incremental Authorization 063 * (draft-ietf-oauth-incremental-authz-04) 064 * </ul> 065 */ 066@Immutable 067public class TokenRequest extends AbstractOptionallyIdentifiedRequest { 068 069 070 /** 071 * The authorisation grant. 072 */ 073 private final AuthorizationGrant authzGrant; 074 075 076 /** 077 * The requested scope, {@code null} if not specified. 078 */ 079 private final Scope scope; 080 081 082 /** 083 * The RAR details (optional). 084 */ 085 private final List<AuthorizationDetail> authorizationDetails; 086 087 088 /** 089 * The resource URI(s), {@code null} if not specified. 090 */ 091 private final List<URI> resources; 092 093 094 /** 095 * Existing refresh token for incremental authorisation of a public 096 * client, {@code null} if not specified. 097 */ 098 private final RefreshToken existingGrant; 099 100 101 /** 102 * Custom request parameters. 103 */ 104 private final Map<String,List<String>> customParams; 105 106 107 private static final Set<String> ALLOWED_REPEATED_PARAMS = new HashSet<>(Arrays.asList("resource", "audience")); 108 109 110 /** 111 * Creates a new token request with the specified client 112 * authentication. 113 * 114 * @param uri The URI of the token endpoint. May be 115 * {@code null} if the {@link #toHTTPRequest} method 116 * will not be used. 117 * @param clientAuth The client authentication. Must not be 118 * {@code null}. 119 * @param authzGrant The authorisation grant. Must not be {@code null}. 120 * @param scope The requested scope, {@code null} if not 121 * specified. 122 */ 123 public TokenRequest(final URI uri, 124 final ClientAuthentication clientAuth, 125 final AuthorizationGrant authzGrant, 126 final Scope scope) { 127 128 this(uri, clientAuth, authzGrant, scope, null, null); 129 } 130 131 132 /** 133 * Creates a new token request with the specified client 134 * authentication and extension and custom parameters. 135 * 136 * @param uri The URI of the token endpoint. May be 137 * {@code null} if the {@link #toHTTPRequest} 138 * method will not be used. 139 * @param clientAuth The client authentication. Must not be 140 * {@code null}. 141 * @param authzGrant The authorisation grant. Must not be 142 * {@code null}. 143 * @param scope The requested scope, {@code null} if not 144 * specified. 145 * @param resources The resource URI(s), {@code null} if not 146 * specified. 147 * @param customParams Custom parameters to be included in the request 148 * body, empty map or {@code null} if none. 149 */ 150 public TokenRequest(final URI uri, 151 final ClientAuthentication clientAuth, 152 final AuthorizationGrant authzGrant, 153 final Scope scope, 154 final List<URI> resources, 155 final Map<String,List<String>> customParams) { 156 157 this(uri, clientAuth, authzGrant, scope, null, resources, customParams); 158 } 159 160 161 /** 162 * Creates a new token request with the specified client 163 * authentication and extension and custom parameters. 164 * 165 * @param uri The URI of the token endpoint. May be 166 * {@code null} if the 167 * {@link #toHTTPRequest} method will not 168 * be used. 169 * @param clientAuth The client authentication. Must not be 170 * {@code null}. 171 * @param authzGrant The authorisation grant. Must not be 172 * {@code null}. 173 * @param scope The requested scope, {@code null} if not 174 * specified. 175 * @param authorizationDetails The Rich Authorisation Request (RAR) 176 * details, {@code null} if not specified. 177 * @param resources The resource URI(s), {@code null} if not 178 * specified. 179 * @param customParams Custom parameters to be included in the 180 * request body, empty map or {@code null} 181 * if none. 182 */ 183 public TokenRequest(final URI uri, 184 final ClientAuthentication clientAuth, 185 final AuthorizationGrant authzGrant, 186 final Scope scope, 187 final List<AuthorizationDetail> authorizationDetails, 188 final List<URI> resources, 189 final Map<String,List<String>> customParams) { 190 191 super(uri, clientAuth); 192 193 if (clientAuth == null) 194 throw new IllegalArgumentException("The client authentication must not be null"); 195 196 this.authzGrant = authzGrant; 197 198 this.scope = scope; 199 200 if (resources != null) { 201 for (URI resourceURI: resources) { 202 if (! ResourceUtils.isLegalResourceURI(resourceURI)) 203 throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI); 204 } 205 } 206 207 this.authorizationDetails = authorizationDetails; 208 209 this.resources = resources; 210 211 this.existingGrant = null; // only for confidential client 212 213 if (MapUtils.isNotEmpty(customParams)) { 214 this.customParams = customParams; 215 } else { 216 this.customParams = Collections.emptyMap(); 217 } 218 } 219 220 221 /** 222 * Creates a new token request with the specified client 223 * authentication. 224 * 225 * @param uri The URI of the token endpoint. May be 226 * {@code null} if the {@link #toHTTPRequest} method 227 * will not be used. 228 * @param clientAuth The client authentication. Must not be 229 * {@code null}. 230 * @param authzGrant The authorisation grant. Must not be {@code null}. 231 */ 232 public TokenRequest(final URI uri, 233 final ClientAuthentication clientAuth, 234 final AuthorizationGrant authzGrant) { 235 236 this(uri, clientAuth, authzGrant, null); 237 } 238 239 240 /** 241 * Creates a new token request, with no explicit client authentication 242 * (maybe present in the grant depending on its type). 243 * 244 * @param uri The URI of the token endpoint. May be 245 * {@code null} if the {@link #toHTTPRequest} method 246 * will not be used. 247 * @param clientID The client identifier, {@code null} if not 248 * specified. 249 * @param authzGrant The authorisation grant. Must not be {@code null}. 250 * @param scope The requested scope, {@code null} if not 251 * specified. 252 */ 253 public TokenRequest(final URI uri, 254 final ClientID clientID, 255 final AuthorizationGrant authzGrant, 256 final Scope scope) { 257 258 this(uri, clientID, authzGrant, scope, null, null,null); 259 } 260 261 262 /** 263 * Creates a new token request, with no explicit client authentication 264 * (maybe present in the grant depending on its type) and extension 265 * and custom parameters. 266 * 267 * @param uri The URI of the token endpoint. May be 268 * {@code null} if the {@link #toHTTPRequest} 269 * method will not be used. 270 * @param clientID The client identifier, {@code null} if not 271 * specified. 272 * @param authzGrant The authorisation grant. Must not be 273 * {@code null}. 274 * @param scope The requested scope, {@code null} if not 275 * specified. 276 * @param resources The resource URI(s), {@code null} if not 277 * specified. 278 * @param existingGrant Existing refresh token for incremental 279 * authorisation of a public client, {@code null} 280 * if not specified. 281 * @param customParams Custom parameters to be included in the request 282 * body, empty map or {@code null} if none. 283 */ 284 public TokenRequest(final URI uri, 285 final ClientID clientID, 286 final AuthorizationGrant authzGrant, 287 final Scope scope, 288 final List<URI> resources, 289 final RefreshToken existingGrant, 290 final Map<String,List<String>> customParams) { 291 292 this(uri, clientID, authzGrant, scope, null, resources, existingGrant, customParams); 293 } 294 295 296 /** 297 * Creates a new token request, with no explicit client authentication 298 * (maybe present in the grant depending on its type) and extension 299 * and custom parameters. 300 * 301 * @param uri The URI of the token endpoint. May be 302 * {@code null} if the 303 * {@link #toHTTPRequest} 304 * method will not be used. 305 * @param clientID The client identifier, {@code null} if 306 * not specified. 307 * @param authzGrant The authorisation grant. Must not be 308 * {@code null}. 309 * @param scope The requested scope, {@code null} if not 310 * specified. 311 * @param authorizationDetails The Rich Authorisation Request (RAR) 312 * details, {@code null} if not specified. 313 * @param resources The resource URI(s), {@code null} if not 314 * specified. 315 * @param existingGrant Existing refresh token for incremental 316 * authorisation of a public client, 317 * {@code null} if not specified. 318 * @param customParams Custom parameters to be included in the 319 * request body, empty map or {@code null} 320 * if none. 321 */ 322 public TokenRequest(final URI uri, 323 final ClientID clientID, 324 final AuthorizationGrant authzGrant, 325 final Scope scope, 326 final List<AuthorizationDetail> authorizationDetails, 327 final List<URI> resources, 328 final RefreshToken existingGrant, 329 final Map<String,List<String>> customParams) { 330 331 super(uri, clientID); 332 333 if (authzGrant.getType().requiresClientAuthentication()) { 334 throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires client authentication"); 335 } 336 337 if (authzGrant.getType().requiresClientID() && clientID == null) { 338 throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires a \"client_id\" parameter"); 339 } 340 341 this.authzGrant = authzGrant; 342 343 this.scope = scope; 344 345 this.authorizationDetails = authorizationDetails; 346 347 if (resources != null) { 348 for (URI resourceURI: resources) { 349 if (! ResourceUtils.isLegalResourceURI(resourceURI)) 350 throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI); 351 } 352 } 353 354 this.resources = resources; 355 356 this.existingGrant = existingGrant; 357 358 if (MapUtils.isNotEmpty(customParams)) { 359 this.customParams = customParams; 360 } else { 361 this.customParams = Collections.emptyMap(); 362 } 363 } 364 365 366 /** 367 * Creates a new token request, with no explicit client authentication 368 * (maybe present in the grant depending on its type). 369 * 370 * @param uri The URI of the token endpoint. May be 371 * {@code null} if the {@link #toHTTPRequest} method 372 * will not be used. 373 * @param clientID The client identifier, {@code null} if not 374 * specified. 375 * @param authzGrant The authorisation grant. Must not be {@code null}. 376 */ 377 public TokenRequest(final URI uri, 378 final ClientID clientID, 379 final AuthorizationGrant authzGrant) { 380 381 this(uri, clientID, authzGrant, null); 382 } 383 384 385 /** 386 * Creates a new token request, without client authentication and a 387 * specified client identifier. 388 * 389 * @param uri The URI of the token endpoint. May be 390 * {@code null} if the {@link #toHTTPRequest} method 391 * will not be used. 392 * @param authzGrant The authorisation grant. Must not be {@code null}. 393 * @param scope The requested scope, {@code null} if not 394 * specified. 395 */ 396 public TokenRequest(final URI uri, 397 final AuthorizationGrant authzGrant, 398 final Scope scope) { 399 400 this(uri, (ClientID)null, authzGrant, scope); 401 } 402 403 404 /** 405 * Creates a new token request, without client authentication and a 406 * specified client identifier. 407 * 408 * @param uri The URI of the token endpoint. May be 409 * {@code null} if the {@link #toHTTPRequest} method 410 * will not be used. 411 * @param authzGrant The authorisation grant. Must not be {@code null}. 412 */ 413 public TokenRequest(final URI uri, 414 final AuthorizationGrant authzGrant) { 415 416 this(uri, (ClientID)null, authzGrant, null); 417 } 418 419 420 /** 421 * Returns the authorisation grant. 422 * 423 * @return The authorisation grant. 424 */ 425 public AuthorizationGrant getAuthorizationGrant() { 426 427 return authzGrant; 428 } 429 430 431 /** 432 * Returns the requested scope. 433 * 434 * @return The requested scope, {@code null} if not specified. 435 */ 436 public Scope getScope() { 437 438 return scope; 439 } 440 441 442 /** 443 * Returns the Rich Authorisation Request (RAR) details. 444 * 445 * @return The authorisation details, {@code null} if not specified. 446 */ 447 public List<AuthorizationDetail> getAuthorizationDetails() { 448 449 return authorizationDetails; 450 } 451 452 453 /** 454 * Returns the resource server URI. 455 * 456 * @return The resource URI(s), {@code null} if not specified. 457 */ 458 public List<URI> getResources() { 459 460 return resources; 461 } 462 463 464 /** 465 * Returns the existing refresh token for incremental authorisation of 466 * a public client, {@code null} if not specified. 467 * 468 * @return The existing grant, {@code null} if not specified. 469 */ 470 public RefreshToken getExistingGrant() { 471 472 return existingGrant; 473 } 474 475 476 /** 477 * Returns the additional custom parameters included in the request 478 * body. 479 * 480 * <p>Example: 481 * 482 * <pre> 483 * resource=http://xxxxxx/PartyOData 484 * </pre> 485 * 486 * @return The additional custom parameters as an unmodifiable map, 487 * empty map if none. 488 */ 489 public Map<String,List<String>> getCustomParameters () { 490 491 return Collections.unmodifiableMap(customParams); 492 } 493 494 495 /** 496 * Returns the specified custom parameter included in the request body. 497 * 498 * @param name The parameter name. Must not be {@code null}. 499 * 500 * @return The parameter value(s), {@code null} if not specified. 501 */ 502 public List<String> getCustomParameter(final String name) { 503 504 return customParams.get(name); 505 } 506 507 508 @Override 509 public HTTPRequest toHTTPRequest() { 510 511 if (getEndpointURI() == null) 512 throw new SerializeException("The endpoint URI is not specified"); 513 514 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI()); 515 httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED); 516 517 if (getClientAuthentication() != null) { 518 getClientAuthentication().applyTo(httpRequest); 519 } 520 521 Map<String, List<String>> params; 522 try { 523 params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters()); 524 } catch (ParseException e) { 525 throw new SerializeException(e.getMessage(), e); 526 } 527 params.putAll(authzGrant.toParameters()); 528 529 switch (authzGrant.getType().getScopeRequirementInTokenRequest()) { 530 case REQUIRED: 531 if (CollectionUtils.isEmpty(scope)) { 532 throw new SerializeException("Scope is required for the " + authzGrant.getType() + " grant"); 533 } 534 params.put("scope", Collections.singletonList(scope.toString())); 535 break; 536 case OPTIONAL: 537 if (CollectionUtils.isNotEmpty(scope)) { 538 params.put("scope", Collections.singletonList(scope.toString())); 539 } 540 break; 541 case NOT_ALLOWED: 542 default: 543 break; 544 } 545 546 if (getClientID() != null) { 547 params.put("client_id", Collections.singletonList(getClientID().getValue())); 548 } 549 550 if (getAuthorizationDetails() != null) { 551 params.put("authorization_details", Collections.singletonList(AuthorizationDetail.toJSONString(getAuthorizationDetails()))); 552 } 553 554 if (getResources() != null) { 555 List<String> values = new LinkedList<>(); 556 for (URI uri: resources) { 557 if (uri == null) 558 continue; 559 values.add(uri.toString()); 560 } 561 params.put("resource", values); 562 } 563 564 if (getExistingGrant() != null) { 565 params.put("existing_grant", Collections.singletonList(existingGrant.getValue())); 566 } 567 568 if (! getCustomParameters().isEmpty()) { 569 params.putAll(getCustomParameters()); 570 } 571 572 httpRequest.setBody(URLUtils.serializeParameters(params)); 573 574 return httpRequest; 575 } 576 577 578 /** 579 * Parses a token request from the specified HTTP request. 580 * 581 * @param httpRequest The HTTP request. Must not be {@code null}. 582 * 583 * @return The token request. 584 * 585 * @throws ParseException If the HTTP request couldn't be parsed to a 586 * token request. 587 */ 588 public static TokenRequest parse(final HTTPRequest httpRequest) 589 throws ParseException { 590 591 // Only HTTP POST accepted 592 URI uri = httpRequest.getURI(); 593 httpRequest.ensureMethod(HTTPRequest.Method.POST); 594 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 595 596 // Parse client authentication, if any 597 ClientAuthentication clientAuth; 598 599 try { 600 clientAuth = ClientAuthentication.parse(httpRequest); 601 } catch (ParseException e) { 602 throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage())); 603 } 604 605 // No fragment! May use query component! 606 Map<String,List<String>> params = httpRequest.getBodyAsFormParameters(); 607 608 Set<String> repeatParams = MultivaluedMapUtils.getKeysWithMoreThanOneValue(params, ALLOWED_REPEATED_PARAMS); 609 if (! repeatParams.isEmpty()) { 610 String msg = "Parameter(s) present more than once: " + repeatParams; 611 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.setDescription(msg)); 612 } 613 614 // Multiple conflicting client auth methods (issue #203)? 615 if (clientAuth instanceof ClientSecretBasic) { 616 if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) { 617 String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion"; 618 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 619 } 620 } 621 622 // Parse grant 623 AuthorizationGrant grant = AuthorizationGrant.parse(params); 624 625 if (clientAuth == null && grant.getType().requiresClientAuthentication()) { 626 String msg = "Missing client authentication"; 627 throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg)); 628 } 629 630 // Parse client id 631 ClientID clientID = null; 632 633 if (clientAuth == null) { 634 635 // Parse optional client ID 636 String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id"); 637 638 if (clientIDString != null && ! clientIDString.trim().isEmpty()) 639 clientID = new ClientID(clientIDString); 640 641 if (clientID == null && grant.getType().requiresClientID()) { 642 String msg = "Missing required client_id parameter"; 643 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 644 } 645 } 646 647 // Parse optional scope 648 String scopeValue = MultivaluedMapUtils.getFirstValue(params, "scope"); 649 650 ParameterRequirement scopeRequirement = grant.getType().getScopeRequirementInTokenRequest(); 651 652 Scope scope = null; 653 654 if (scopeValue != null && (ParameterRequirement.REQUIRED.equals(scopeRequirement) || ParameterRequirement.OPTIONAL.equals(scopeRequirement))) { 655 scope = Scope.parse(scopeValue); 656 } 657 658 // Parse optional RAR 659 String json = MultivaluedMapUtils.getFirstValue(params, "authorization_details"); 660 661 List<AuthorizationDetail> authorizationDetails = null; 662 663 if (json != null) { 664 authorizationDetails = AuthorizationDetail.parseList(json); 665 } 666 667 // Parse optional resource URIs 668 List<URI> resources = null; 669 670 List<String> vList = params.get("resource"); 671 672 if (vList != null) { 673 674 resources = new LinkedList<>(); 675 676 for (String uriValue: vList) { 677 678 if (uriValue == null) 679 continue; 680 681 String errMsg = "Illegal resource parameter: Must be an absolute URI without a fragment: " + uriValue; 682 683 URI resourceURI; 684 try { 685 resourceURI = new URI(uriValue); 686 } catch (URISyntaxException e) { 687 throw new ParseException(errMsg, OAuth2Error.INVALID_TARGET.setDescription(errMsg)); 688 } 689 690 if (! ResourceUtils.isLegalResourceURI(resourceURI)) { 691 throw new ParseException(errMsg, OAuth2Error.INVALID_TARGET.setDescription(errMsg)); 692 } 693 694 resources.add(resourceURI); 695 } 696 } 697 698 String rt = MultivaluedMapUtils.getFirstValue(params, "existing_grant"); 699 RefreshToken existingGrant = StringUtils.isNotBlank(rt) ? new RefreshToken(rt) : null; 700 701 // Parse custom parameters 702 Map<String,List<String>> customParams = new HashMap<>(); 703 704 for (Map.Entry<String,List<String>> p: params.entrySet()) { 705 706 if (p.getKey().equalsIgnoreCase("grant_type")) { 707 continue; // skip 708 } 709 710 if (p.getKey().equalsIgnoreCase("client_id")) { 711 continue; // skip 712 } 713 714 if (p.getKey().equalsIgnoreCase("client_secret")) { 715 continue; // skip 716 } 717 718 if (p.getKey().equalsIgnoreCase("client_assertion_type")) { 719 continue; // skip 720 } 721 722 if (p.getKey().equalsIgnoreCase("client_assertion")) { 723 continue; // skip 724 } 725 726 if (p.getKey().equalsIgnoreCase("scope")) { 727 continue; // skip 728 } 729 730 if (p.getKey().equalsIgnoreCase("authorization_details")) { 731 continue; // skip 732 } 733 734 if (p.getKey().equalsIgnoreCase("resource")) { 735 continue; // skip 736 } 737 738 if (p.getKey().equalsIgnoreCase("existing_grant")) 739 continue; // skip 740 741 if (! grant.getType().getRequestParameterNames().contains(p.getKey())) { 742 // We have a custom (non-registered) parameter 743 customParams.put(p.getKey(), p.getValue()); 744 } 745 } 746 747 if (clientAuth != null) { 748 return new TokenRequest(uri, clientAuth, grant, scope, authorizationDetails, resources, customParams); 749 } else { 750 // public client 751 return new TokenRequest(uri, clientID, grant, scope, authorizationDetails, resources, existingGrant, customParams); 752 } 753 } 754}