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 com.nimbusds.oauth2.sdk.auth.ClientAuthentication; 028import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; 029import com.nimbusds.oauth2.sdk.http.CommonContentTypes; 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.*; 034import net.jcip.annotations.Immutable; 035 036 037/** 038 * Token request. Used to obtain an 039 * {@link com.nimbusds.oauth2.sdk.token.AccessToken access token} and an 040 * optional {@link com.nimbusds.oauth2.sdk.token.RefreshToken refresh token} 041 * at the Token endpoint of the authorisation server. Supports custom request 042 * parameters. 043 * 044 * <p>Example token request with an authorisation code grant: 045 * 046 * <pre> 047 * POST /token HTTP/1.1 048 * Host: server.example.com 049 * Content-Type: application/x-www-form-URIencoded 050 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 051 * 052 * grant_type=authorization_code 053 * &code=SplxlOBeZQQYbYS6WxSbIA 054 * &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb 055 * </pre> 056 * 057 * <p>Related specifications: 058 * 059 * <ul> 060 * <li>OAuth 2.0 (RFC 6749), sections 4.1.3, 4.3.2, 4.4.2 and 6. 061 * <li>Resource Indicators for OAuth 2.0 062 * (draft-ietf-oauth-resource-indicators-00) 063 * <li>OAuth 2.0 Incremental Authorization 064 * (draft-ietf-oauth-incremental-authz-00) 065 * </ul> 066 */ 067@Immutable 068public class TokenRequest extends AbstractOptionallyIdentifiedRequest { 069 070 071 /** 072 * The authorisation grant. 073 */ 074 private final AuthorizationGrant authzGrant; 075 076 077 /** 078 * The requested scope, {@code null} if not specified. 079 */ 080 private final Scope scope; 081 082 083 /** 084 * The resource URI(s), {@code null} if not specified. 085 */ 086 private final List<URI> resources; 087 088 089 /** 090 * Existing refresh token for incremental authorisation of a public 091 * client, {@code null} if not specified. 092 */ 093 private final RefreshToken existingGrant; 094 095 096 /** 097 * Custom request parameters. 098 */ 099 private final Map<String,List<String>> customParams; 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.isValidResourceURI(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.isValidResourceURI(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 a 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 URL url; 419 420 try { 421 url = getEndpointURI().toURL(); 422 423 } catch (MalformedURLException e) { 424 425 throw new SerializeException(e.getMessage(), e); 426 } 427 428 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, url); 429 httpRequest.setContentType(CommonContentTypes.APPLICATION_URLENCODED); 430 431 if (getClientAuthentication() != null) { 432 getClientAuthentication().applyTo(httpRequest); 433 } 434 435 Map<String,List<String>> params = httpRequest.getQueryParameters(); 436 437 params.putAll(authzGrant.toParameters()); 438 439 if (scope != null && ! scope.isEmpty()) { 440 params.put("scope", Collections.singletonList(scope.toString())); 441 } 442 443 if (getClientID() != null) { 444 params.put("client_id", Collections.singletonList(getClientID().getValue())); 445 } 446 447 if (getResources() != null) { 448 List<String> values = new LinkedList<>(); 449 for (URI uri: resources) { 450 if (uri == null) 451 continue; 452 values.add(uri.toString()); 453 } 454 params.put("resource", values); 455 } 456 457 if (getExistingGrant() != null) { 458 params.put("existing_grant", Collections.singletonList(existingGrant.getValue())); 459 } 460 461 if (! getCustomParameters().isEmpty()) { 462 params.putAll(getCustomParameters()); 463 } 464 465 httpRequest.setQuery(URLUtils.serializeParameters(params)); 466 467 return httpRequest; 468 } 469 470 471 /** 472 * Parses a token request from the specified HTTP request. 473 * 474 * @param httpRequest The HTTP request. Must not be {@code null}. 475 * 476 * @return The token request. 477 * 478 * @throws ParseException If the HTTP request couldn't be parsed to a 479 * token request. 480 */ 481 public static TokenRequest parse(final HTTPRequest httpRequest) 482 throws ParseException { 483 484 // Only HTTP POST accepted 485 URI uri; 486 487 try { 488 uri = httpRequest.getURL().toURI(); 489 490 } catch (URISyntaxException e) { 491 492 throw new ParseException(e.getMessage(), e); 493 } 494 495 httpRequest.ensureMethod(HTTPRequest.Method.POST); 496 httpRequest.ensureContentType(CommonContentTypes.APPLICATION_URLENCODED); 497 498 // Parse client authentication, if any 499 ClientAuthentication clientAuth; 500 501 try { 502 clientAuth = ClientAuthentication.parse(httpRequest); 503 } catch (ParseException e) { 504 throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage())); 505 } 506 507 // No fragment! May use query component! 508 Map<String,List<String>> params = httpRequest.getQueryParameters(); 509 510 // Multiple conflicting client auth methods (issue #203)? 511 if (clientAuth instanceof ClientSecretBasic) { 512 if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) { 513 String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion"; 514 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 515 } 516 } 517 518 // Parse grant 519 AuthorizationGrant grant = AuthorizationGrant.parse(params); 520 521 if (clientAuth == null && grant.getType().requiresClientAuthentication()) { 522 String msg = "Missing client authentication"; 523 throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg)); 524 } 525 526 // Parse client id 527 ClientID clientID = null; 528 529 if (clientAuth == null) { 530 531 // Parse optional client ID 532 String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id"); 533 534 if (clientIDString != null && ! clientIDString.trim().isEmpty()) 535 clientID = new ClientID(clientIDString); 536 537 if (clientID == null && grant.getType().requiresClientID()) { 538 String msg = "Missing required \"client_id\" parameter"; 539 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 540 } 541 } 542 543 // Parse optional scope 544 String scopeValue = MultivaluedMapUtils.getFirstValue(params, "scope"); 545 546 Scope scope = null; 547 548 if (scopeValue != null) { 549 scope = Scope.parse(scopeValue); 550 } 551 552 // Parse resource URIs 553 List<URI> resources = null; 554 555 List<String> vList = params.get("resource"); 556 557 if (vList != null) { 558 559 resources = new LinkedList<>(); 560 561 for (String uriValue: vList) { 562 563 if (uriValue == null) 564 continue; 565 566 String errMsg = "Invalid \"resource\" parameter: Must be an absolute URI and with no query or fragment: " + uriValue; 567 568 URI resourceURI; 569 try { 570 resourceURI = new URI(uriValue); 571 } catch (URISyntaxException e) { 572 throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg)); 573 } 574 575 if (! ResourceUtils.isValidResourceURI(resourceURI)) { 576 throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg)); 577 } 578 579 resources.add(resourceURI); 580 } 581 } 582 583 String rt = MultivaluedMapUtils.getFirstValue(params, "existing_grant"); 584 RefreshToken existingGrant = StringUtils.isNotBlank(rt) ? new RefreshToken(rt) : null; 585 586 // Parse custom parameters 587 Map<String,List<String>> customParams = new HashMap<>(); 588 589 for (Map.Entry<String,List<String>> p: params.entrySet()) { 590 591 if (p.getKey().equalsIgnoreCase("grant_type")) { 592 continue; // skip 593 } 594 595 if (p.getKey().equalsIgnoreCase("client_id")) { 596 continue; // skip 597 } 598 599 if (p.getKey().equalsIgnoreCase("client_secret")) { 600 continue; // skip 601 } 602 603 if (p.getKey().equalsIgnoreCase("client_assertion_type")) { 604 continue; // skip 605 } 606 607 if (p.getKey().equalsIgnoreCase("client_assertion")) { 608 continue; // skip 609 } 610 611 if (p.getKey().equalsIgnoreCase("scope")) { 612 continue; // skip 613 } 614 615 if (p.getKey().equalsIgnoreCase("resource")) { 616 continue; // skip 617 } 618 619 if (p.getKey().equalsIgnoreCase("existing_grant")) 620 continue; // skip 621 622 if (! grant.getType().getRequestParameterNames().contains(p.getKey())) { 623 // We have a custom (non-registered) parameter 624 customParams.put(p.getKey(), p.getValue()); 625 } 626 } 627 628 if (clientAuth != null) { 629 return new TokenRequest(uri, clientAuth, grant, scope, resources, customParams); 630 } else { 631 // public client 632 return new TokenRequest(uri, clientID, grant, scope, resources, existingGrant, customParams); 633 } 634 } 635}