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