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