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