001package com.nimbusds.oauth2.sdk; 002 003 004import java.net.MalformedURLException; 005import java.net.URI; 006import java.net.URISyntaxException; 007import java.net.URL; 008import java.util.Collections; 009import java.util.HashMap; 010import java.util.Map; 011 012import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; 013import com.nimbusds.oauth2.sdk.http.CommonContentTypes; 014import com.nimbusds.oauth2.sdk.http.HTTPRequest; 015import com.nimbusds.oauth2.sdk.id.ClientID; 016import com.nimbusds.oauth2.sdk.util.URLUtils; 017import net.jcip.annotations.Immutable; 018import org.apache.commons.collections4.MapUtils; 019 020 021/** 022 * Token request. Used to obtain an 023 * {@link com.nimbusds.oauth2.sdk.token.AccessToken access token} and an 024 * optional {@link com.nimbusds.oauth2.sdk.token.RefreshToken refresh token} 025 * at the Token endpoint of the authorisation server. Supports custom request 026 * parameters. 027 * 028 * <p>Example token request with an authorisation code grant: 029 * 030 * <pre> 031 * POST /token HTTP/1.1 032 * Host: server.example.com 033 * Content-Type: application/x-www-form-URIencoded 034 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 035 * 036 * grant_type=authorization_code 037 * &code=SplxlOBeZQQYbYS6WxSbIA 038 * &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb 039 * </pre> 040 * 041 * <p>Related specifications: 042 * 043 * <ul> 044 * <li>OAuth 2.0 (RFC 6749), sections 4.1.3, 4.3.2, 4.4.2 and 6. 045 * </ul> 046 */ 047@Immutable 048public class TokenRequest extends AbstractOptionallyIdentifiedRequest { 049 050 051 /** 052 * The authorisation grant. 053 */ 054 private final AuthorizationGrant authzGrant; 055 056 057 /** 058 * The requested scope, {@code null} if not specified. 059 */ 060 private final Scope scope; 061 062 063 /** 064 * Additional custom request parameters. 065 */ 066 private final Map<String,String> customParams; 067 068 069 /** 070 * Creates a new token request with the specified client 071 * authentication. 072 * 073 * @param uri The URI of the token endpoint. May be 074 * {@code null} if the {@link #toHTTPRequest} method 075 * will not be used. 076 * @param clientAuth The client authentication. Must not be 077 * {@code null}. 078 * @param authzGrant The authorisation grant. Must not be {@code null}. 079 * @param scope The requested scope, {@code null} if not 080 * specified. 081 */ 082 public TokenRequest(final URI uri, 083 final ClientAuthentication clientAuth, 084 final AuthorizationGrant authzGrant, 085 final Scope scope) { 086 087 this(uri, clientAuth, authzGrant, scope, null); 088 } 089 090 091 /** 092 * Creates a new token request with the specified client 093 * authentication and additional custom parameters. 094 * 095 * @param uri The URI of the token endpoint. May be 096 * {@code null} if the {@link #toHTTPRequest} 097 * method will not be used. 098 * @param clientAuth The client authentication. Must not be 099 * {@code null}. 100 * @param authzGrant The authorisation grant. Must not be 101 * {@code null}. 102 * @param scope The requested scope, {@code null} if not 103 * specified. 104 * @param customParams Additional custom parameters to be included in 105 * the request body, empty map or {@code null} if 106 * none. 107 */ 108 public TokenRequest(final URI uri, 109 final ClientAuthentication clientAuth, 110 final AuthorizationGrant authzGrant, 111 final Scope scope, 112 final Map<String,String> customParams) { 113 114 super(uri, clientAuth); 115 116 if (clientAuth == null) 117 throw new IllegalArgumentException("The client authentication must not be null"); 118 119 this.authzGrant = authzGrant; 120 121 this.scope = scope; 122 123 if (MapUtils.isNotEmpty(customParams)) { 124 this.customParams = customParams; 125 } else { 126 this.customParams = Collections.emptyMap(); 127 } 128 } 129 130 131 /** 132 * Creates a new token request with the specified client 133 * authentication. 134 * 135 * @param uri The URI of the token endpoint. May be 136 * {@code null} if the {@link #toHTTPRequest} method 137 * will not be used. 138 * @param clientAuth The client authentication. Must not be 139 * {@code null}. 140 * @param authzGrant The authorisation grant. Must not be {@code null}. 141 */ 142 public TokenRequest(final URI uri, 143 final ClientAuthentication clientAuth, 144 final AuthorizationGrant authzGrant) { 145 146 this(uri, clientAuth, authzGrant, null); 147 } 148 149 150 /** 151 * Creates a new token request, with no explicit client authentication 152 * (may be present in the grant depending on its type). 153 * 154 * @param uri The URI of the token endpoint. May be 155 * {@code null} if the {@link #toHTTPRequest} method 156 * will not be used. 157 * @param clientID The client identifier, {@code null} if not 158 * specified. 159 * @param authzGrant The authorisation grant. Must not be {@code null}. 160 * @param scope The requested scope, {@code null} if not 161 * specified. 162 */ 163 public TokenRequest(final URI uri, 164 final ClientID clientID, 165 final AuthorizationGrant authzGrant, 166 final Scope scope) { 167 168 this(uri, clientID, authzGrant, scope, null); 169 } 170 171 172 /** 173 * Creates a new token request, with no explicit client authentication 174 * (may be present in the grant depending on its type) and additional 175 * custom parameters. 176 * 177 * @param uri The URI of the token endpoint. May be 178 * {@code null} if the {@link #toHTTPRequest} 179 * method will not be used. 180 * @param clientID The client identifier, {@code null} if not 181 * specified. 182 * @param authzGrant The authorisation grant. Must not be 183 * {@code null}. 184 * @param scope The requested scope, {@code null} if not 185 * specified. 186 * @param customParams Additional custom parameters to be included in 187 * the request body, empty map or {@code null} if 188 * none. 189 */ 190 public TokenRequest(final URI uri, 191 final ClientID clientID, 192 final AuthorizationGrant authzGrant, 193 final Scope scope, 194 final Map<String,String> customParams) { 195 196 super(uri, clientID); 197 198 if (authzGrant.getType().requiresClientAuthentication()) { 199 throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires client authentication"); 200 } 201 202 if (authzGrant.getType().requiresClientID() && clientID == null) { 203 throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires a \"client_id\" parameter"); 204 } 205 206 this.authzGrant = authzGrant; 207 208 this.scope = scope; 209 210 if (MapUtils.isNotEmpty(customParams)) { 211 this.customParams = customParams; 212 } else { 213 this.customParams = Collections.emptyMap(); 214 } 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). 221 * 222 * @param uri The URI of the token endpoint. May be 223 * {@code null} if the {@link #toHTTPRequest} method 224 * will not be used. 225 * @param clientID The client identifier, {@code null} if not 226 * specified. 227 * @param authzGrant The authorisation grant. Must not be {@code null}. 228 */ 229 public TokenRequest(final URI uri, 230 final ClientID clientID, 231 final AuthorizationGrant authzGrant) { 232 233 this(uri, clientID, authzGrant, null); 234 } 235 236 237 /** 238 * Creates a new token request, without client authentication and a 239 * specified client identifier. 240 * 241 * @param uri The URI of the token endpoint. May be 242 * {@code null} if the {@link #toHTTPRequest} method 243 * will not be used. 244 * @param authzGrant The authorisation grant. Must not be {@code null}. 245 * @param scope The requested scope, {@code null} if not 246 * specified. 247 */ 248 public TokenRequest(final URI uri, 249 final AuthorizationGrant authzGrant, 250 final Scope scope) { 251 252 this(uri, (ClientID)null, authzGrant, scope); 253 } 254 255 256 /** 257 * Creates a new token request, without client authentication and a 258 * specified client identifier. 259 * 260 * @param uri The URI of the token endpoint. May be 261 * {@code null} if the {@link #toHTTPRequest} method 262 * will not be used. 263 * @param authzGrant The authorisation grant. Must not be {@code null}. 264 */ 265 public TokenRequest(final URI uri, 266 final AuthorizationGrant authzGrant) { 267 268 this(uri, (ClientID)null, authzGrant, null); 269 } 270 271 272 /** 273 * Gets the authorisation grant. 274 * 275 * @return The authorisation grant. 276 */ 277 public AuthorizationGrant getAuthorizationGrant() { 278 279 return authzGrant; 280 } 281 282 283 /** 284 * Gets the requested scope. 285 * 286 * @return The requested scope, {@code null} if not specified. 287 */ 288 public Scope getScope() { 289 290 return scope; 291 } 292 293 294 /** 295 * Returns the additional custom parameters included in the request 296 * body. 297 * 298 * <p>Example: 299 * 300 * <pre> 301 * resource=http://xxxxxx/PartyOData 302 * </pre> 303 * 304 * @return The additional custom parameters as a unmodifiable map, 305 * empty map if none. 306 */ 307 public Map<String,String> getCustomParameters () { 308 309 return customParams; 310 } 311 312 313 /** 314 * Returns the specified custom parameter included in the request body. 315 * 316 * @param name The parameter name. Must not be {@code null}. 317 * 318 * @return The parameter value, {@code null} if not specified. 319 */ 320 public String getCustomParameter(final String name) { 321 322 return customParams.get(name); 323 } 324 325 326 @Override 327 public HTTPRequest toHTTPRequest() { 328 329 if (getEndpointURI() == null) 330 throw new SerializeException("The endpoint URI is not specified"); 331 332 URL url; 333 334 try { 335 url = getEndpointURI().toURL(); 336 337 } catch (MalformedURLException e) { 338 339 throw new SerializeException(e.getMessage(), e); 340 } 341 342 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, url); 343 httpRequest.setContentType(CommonContentTypes.APPLICATION_URLENCODED); 344 345 if (getClientAuthentication() != null) { 346 getClientAuthentication().applyTo(httpRequest); 347 } 348 349 Map<String,String> params = httpRequest.getQueryParameters(); 350 351 params.putAll(authzGrant.toParameters()); 352 353 if (scope != null && ! scope.isEmpty()) { 354 params.put("scope", scope.toString()); 355 } 356 357 if (getClientID() != null) { 358 params.put("client_id", getClientID().getValue()); 359 } 360 361 if (! getCustomParameters().isEmpty()) { 362 params.putAll(getCustomParameters()); 363 } 364 365 httpRequest.setQuery(URLUtils.serializeParameters(params)); 366 367 return httpRequest; 368 } 369 370 371 /** 372 * Parses a token request from the specified HTTP request. 373 * 374 * @param httpRequest The HTTP request. Must not be {@code null}. 375 * 376 * @return The token request. 377 * 378 * @throws ParseException If the HTTP request couldn't be parsed to a 379 * token request. 380 */ 381 public static TokenRequest parse(final HTTPRequest httpRequest) 382 throws ParseException { 383 384 // Only HTTP POST accepted 385 URI uri; 386 387 try { 388 uri = httpRequest.getURL().toURI(); 389 390 } catch (URISyntaxException e) { 391 392 throw new ParseException(e.getMessage(), e); 393 } 394 395 httpRequest.ensureMethod(HTTPRequest.Method.POST); 396 httpRequest.ensureContentType(CommonContentTypes.APPLICATION_URLENCODED); 397 398 // Parse client authentication, if any 399 ClientAuthentication clientAuth = ClientAuthentication.parse(httpRequest); 400 401 // No fragment! May use query component! 402 Map<String,String> params = httpRequest.getQueryParameters(); 403 404 // Parse grant 405 AuthorizationGrant grant = AuthorizationGrant.parse(params); 406 407 if (clientAuth == null && grant.getType().requiresClientAuthentication()) { 408 String msg = "Missing client authentication"; 409 throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg)); 410 } 411 412 // Parse client id 413 ClientID clientID = null; 414 415 if (clientAuth == null) { 416 417 // Parse optional client ID 418 String clientIDString = params.get("client_id"); 419 420 if (clientIDString != null && ! clientIDString.trim().isEmpty()) 421 clientID = new ClientID(clientIDString); 422 423 if (clientID == null && grant.getType().requiresClientID()) { 424 String msg = "Missing required \"client_id\" parameter"; 425 throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 426 } 427 } 428 429 // Parse optional scope 430 String scopeValue = params.get("scope"); 431 432 Scope scope = null; 433 434 if (scopeValue != null) { 435 scope = Scope.parse(scopeValue); 436 } 437 438 // Parse custom parameters 439 Map<String,String> customParams = new HashMap<>(); 440 441 for (Map.Entry<String,String> p: params.entrySet()) { 442 443 if (p.getKey().equalsIgnoreCase("grant_type")) { 444 continue; // skip 445 } 446 447 if (p.getKey().equalsIgnoreCase("client_id")) { 448 continue; // skip 449 } 450 451 if (p.getKey().equalsIgnoreCase("client_secret")) { 452 continue; // skip 453 } 454 455 if (p.getKey().equalsIgnoreCase("client_assertion_type")) { 456 continue; // skip 457 } 458 459 if (p.getKey().equalsIgnoreCase("client_assertion")) { 460 continue; // skip 461 } 462 463 if (p.getKey().equalsIgnoreCase("scope")) { 464 continue; // skip 465 } 466 467 if (! grant.getType().getRequestParameterNames().contains(p.getKey())) { 468 // We have a custom (non-registered) parameter 469 customParams.put(p.getKey(), p.getValue()); 470 } 471 } 472 473 if (clientAuth != null) { 474 return new TokenRequest(uri, clientAuth, grant, scope, customParams); 475 } else { 476 return new TokenRequest(uri, clientID, grant, scope, customParams); 477 } 478 } 479}