001/* 002 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the 010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 011 * express or implied. See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package net.openid.appauth; 016 017import static net.openid.appauth.AdditionalParamsProcessor.checkAdditionalParams; 018import static net.openid.appauth.Preconditions.checkNotEmpty; 019import static net.openid.appauth.Preconditions.checkNotNull; 020import static net.openid.appauth.Preconditions.checkNullOrNotEmpty; 021 022import android.net.Uri; 023import android.text.TextUtils; 024import androidx.annotation.NonNull; 025import androidx.annotation.Nullable; 026import androidx.annotation.VisibleForTesting; 027 028import org.json.JSONException; 029import org.json.JSONObject; 030 031import java.util.Arrays; 032import java.util.Collections; 033import java.util.HashMap; 034import java.util.HashSet; 035import java.util.LinkedHashMap; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.Set; 039 040/** 041 * An OAuth2 token request. These are used to exchange codes for tokens, or exchange a refresh 042 * token for updated tokens. 043 * 044 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3 045 * <https://tools.ietf.org/html/rfc6749#section-4.1.3>" 046 */ 047public class TokenRequest { 048 049 @VisibleForTesting 050 static final String KEY_CONFIGURATION = "configuration"; 051 @VisibleForTesting 052 static final String KEY_CLIENT_ID = "clientId"; 053 @VisibleForTesting 054 static final String KEY_NONCE = "nonce"; 055 @VisibleForTesting 056 static final String KEY_GRANT_TYPE = "grantType"; 057 @VisibleForTesting 058 static final String KEY_REDIRECT_URI = "redirectUri"; 059 @VisibleForTesting 060 static final String KEY_SCOPE = "scope"; 061 @VisibleForTesting 062 static final String KEY_AUTHORIZATION_CODE = "authorizationCode"; 063 @VisibleForTesting 064 static final String KEY_REFRESH_TOKEN = "refreshToken"; 065 @VisibleForTesting 066 static final String KEY_CODE_VERIFIER = "codeVerifier"; 067 @VisibleForTesting 068 static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; 069 070 public static final String PARAM_CLIENT_ID = "client_id"; 071 072 @VisibleForTesting 073 static final String PARAM_CODE = "code"; 074 075 @VisibleForTesting 076 static final String PARAM_CODE_VERIFIER = "code_verifier"; 077 078 @VisibleForTesting 079 static final String PARAM_GRANT_TYPE = "grant_type"; 080 081 @VisibleForTesting 082 static final String PARAM_REDIRECT_URI = "redirect_uri"; 083 084 @VisibleForTesting 085 static final String PARAM_REFRESH_TOKEN = "refresh_token"; 086 087 @VisibleForTesting 088 static final String PARAM_SCOPE = "scope"; 089 090 private static final Set<String> BUILT_IN_PARAMS = Collections.unmodifiableSet( 091 new HashSet<>(Arrays.asList( 092 PARAM_CLIENT_ID, 093 PARAM_CODE, 094 PARAM_CODE_VERIFIER, 095 PARAM_GRANT_TYPE, 096 PARAM_REDIRECT_URI, 097 PARAM_REFRESH_TOKEN, 098 PARAM_SCOPE))); 099 100 101 /** 102 * The grant type used when requesting an access token using a username and password. 103 * This grant type is not directly supported by this library. 104 * 105 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.3.2 106 * <https://tools.ietf.org/html/rfc6749#section-4.3.2>" 107 */ 108 public static final String GRANT_TYPE_PASSWORD = "password"; 109 110 /** 111 * The grant type used when requesting an access token using client credentials, typically 112 * TLS client certificates. This grant type is not directly supported by this library. 113 * 114 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.4.2 115 * <https://tools.ietf.org/html/rfc6749#section-4.4.2>" 116 */ 117 public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; 118 119 /** 120 * The service's {@link AuthorizationServiceConfiguration configuration}. 121 * This configuration specifies how to connect to a particular OAuth provider. 122 * Configurations may be 123 * {@link 124 * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri) 125 * created manually}, or 126 * {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, 127 * AuthorizationServiceConfiguration.RetrieveConfigurationCallback) 128 * via an OpenID Connect Discovery Document}. 129 */ 130 @NonNull 131 public final AuthorizationServiceConfiguration configuration; 132 133 /** 134 * The (optional) nonce associated with the current session. 135 */ 136 @Nullable 137 public final String nonce; 138 139 /** 140 * The client identifier. 141 * 142 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4 143 * <https://tools.ietf.org/html/rfc6749#section-4>" 144 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1 145 * <https://tools.ietf.org/html/rfc6749#section-4.1.1>" 146 */ 147 @NonNull 148 public final String clientId; 149 150 /** 151 * The type of token being sent to the token endpoint. 152 * 153 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3 154 * <https://tools.ietf.org/html/rfc6749#section-4.1.3>" 155 */ 156 @NonNull 157 public final String grantType; 158 159 /** 160 * The client's redirect URI. Required if this token request is to exchange an authorization 161 * code for one or more tokens, and must be identical to the value specified in the original 162 * authorization request. 163 * 164 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.1.2 165 * <https://tools.ietf.org/html/rfc6749#section-3.1.2>" 166 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3 167 * <https://tools.ietf.org/html/rfc6749#section-4.1.3>" 168 */ 169 @Nullable 170 public final Uri redirectUri; 171 172 /** 173 * An authorization code to be exchanged for one or more tokens. 174 * 175 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3 176 * <https://tools.ietf.org/html/rfc6749#section-4.1.3>" 177 */ 178 @Nullable 179 public final String authorizationCode; 180 181 /** 182 * A space-delimited set of scopes used to determine the scope of any returned tokens. 183 * 184 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 185 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 186 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6 187 * <https://tools.ietf.org/html/rfc6749#section-6>" 188 */ 189 @Nullable 190 public final String scope; 191 192 /** 193 * A refresh token to be exchanged for a new token. 194 * 195 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6 196 * <https://tools.ietf.org/html/rfc6749#section-6>" 197 */ 198 @Nullable 199 public final String refreshToken; 200 201 /** 202 * The code verifier that was used to generate the challenge in the original authorization 203 * request, if one was used. 204 * 205 * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636), Section 4 206 * <https://tools.ietf.org/html/rfc7636#section-4>" 207 */ 208 @Nullable 209 public final String codeVerifier; 210 211 /** 212 * Additional parameters to be passed as part of the request. 213 */ 214 @NonNull 215 public final Map<String, String> additionalParameters; 216 217 /** 218 * Creates instances of {@link TokenRequest}. 219 */ 220 public static final class Builder { 221 222 @NonNull 223 private AuthorizationServiceConfiguration mConfiguration; 224 225 @NonNull 226 private String mClientId; 227 228 @Nullable 229 private String mNonce; 230 231 @Nullable 232 private String mGrantType; 233 234 @Nullable 235 private Uri mRedirectUri; 236 237 @Nullable 238 private String mScope; 239 240 @Nullable 241 private String mAuthorizationCode; 242 243 @Nullable 244 private String mRefreshToken; 245 246 @Nullable 247 private String mCodeVerifier; 248 249 @NonNull 250 private Map<String, String> mAdditionalParameters; 251 252 /** 253 * Creates a token request builder with the specified mandatory properties. 254 */ 255 public Builder( 256 @NonNull AuthorizationServiceConfiguration configuration, 257 @NonNull String clientId) { 258 setConfiguration(configuration); 259 setClientId(clientId); 260 mAdditionalParameters = new LinkedHashMap<>(); 261 } 262 263 /** 264 * Specifies the authorization service configuration for the request, which must not 265 * be null or empty. 266 */ 267 @NonNull 268 public Builder setConfiguration(@NonNull AuthorizationServiceConfiguration configuration) { 269 mConfiguration = checkNotNull(configuration); 270 return this; 271 } 272 273 /** 274 * Specifies the client ID for the token request, which must not be null or empty. 275 */ 276 @NonNull 277 public Builder setClientId(@NonNull String clientId) { 278 mClientId = checkNotEmpty(clientId, "clientId cannot be null or empty"); 279 return this; 280 } 281 282 /** 283 * Specifies the (optional) nonce for the current session. 284 */ 285 @NonNull 286 public Builder setNonce(@Nullable String nonce) { 287 if (TextUtils.isEmpty(nonce)) { 288 mNonce = null; 289 } else { 290 this.mNonce = nonce; 291 } 292 return this; 293 } 294 295 /** 296 * Specifies the grant type for the request, which must not be null or empty. 297 */ 298 @NonNull 299 public Builder setGrantType(@NonNull String grantType) { 300 mGrantType = checkNotEmpty(grantType, "grantType cannot be null or empty"); 301 return this; 302 } 303 304 /** 305 * Specifies the redirect URI for the request. This is required for authorization code 306 * exchanges, but otherwise optional. If specified, the redirect URI must have a scheme. 307 */ 308 @NonNull 309 public Builder setRedirectUri(@Nullable Uri redirectUri) { 310 if (redirectUri != null) { 311 checkNotNull(redirectUri.getScheme(), "redirectUri must have a scheme"); 312 } 313 mRedirectUri = redirectUri; 314 return this; 315 } 316 317 /** 318 * Specifies the encoded scope string, which is a space-delimited set of 319 * case-sensitive scope identifiers. Replaces any previously specified scope. 320 * 321 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 322 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 323 */ 324 @NonNull 325 public Builder setScope(@Nullable String scope) { 326 if (TextUtils.isEmpty(scope)) { 327 mScope = null; 328 } else { 329 setScopes(scope.split(" +")); 330 } 331 return this; 332 } 333 334 /** 335 * Specifies the set of case-sensitive scopes. Replaces any previously specified set of 336 * scopes. Individual scope strings cannot be null or empty. 337 * 338 * Scopes specified here are used to obtain a "down-scoped" access token, where the 339 * set of scopes specified _must_ be a subset of those already granted in 340 * previous requests. 341 * 342 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 343 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 344 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6 345 * <https://tools.ietf.org/html/rfc6749#section-6>" 346 */ 347 @NonNull 348 public Builder setScopes(String... scopes) { 349 if (scopes == null) { 350 scopes = new String[0]; 351 } 352 setScopes(Arrays.asList(scopes)); 353 return this; 354 } 355 356 /** 357 * Specifies the set of case-sensitive scopes. Replaces any previously specified set of 358 * scopes. Individual scope strings cannot be null or empty. 359 * 360 * Scopes specified here are used to obtain a "down-scoped" access token, where the 361 * set of scopes specified _must_ be a subset of those already granted in 362 * previous requests. 363 * 364 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 365 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 366 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6 367 * <https://tools.ietf.org/html/rfc6749#section-6>" 368 */ 369 @NonNull 370 public Builder setScopes(@Nullable Iterable<String> scopes) { 371 mScope = AsciiStringListUtil.iterableToString(scopes); 372 return this; 373 } 374 375 /** 376 * Specifies the authorization code for the request. If provided, the authorization code 377 * must not be empty. 378 * 379 * Specifying an authorization code normally implies that this is a request to exchange 380 * this authorization code for one or more tokens. If this is not intended, the grant type 381 * should be explicitly set. 382 */ 383 @NonNull 384 public Builder setAuthorizationCode(@Nullable String authorizationCode) { 385 checkNullOrNotEmpty(authorizationCode, "authorization code must not be empty"); 386 mAuthorizationCode = authorizationCode; 387 return this; 388 } 389 390 /** 391 * Specifies the refresh token for the request. If a non-null value is provided, it must 392 * not be empty. 393 * 394 * Specifying a refresh token normally implies that this is a request to exchange the 395 * refresh token for a new token. If this is not intended, the grant type should be 396 * explicit set. 397 */ 398 @NonNull 399 public Builder setRefreshToken(@Nullable String refreshToken) { 400 if (refreshToken != null) { 401 checkNotEmpty(refreshToken, "refresh token cannot be empty if defined"); 402 } 403 mRefreshToken = refreshToken; 404 return this; 405 } 406 407 /** 408 * Specifies the code verifier for an authorization code exchange request. This must match 409 * the code verifier that was used to generate the challenge sent in the request that 410 * produced the authorization code. 411 */ 412 public Builder setCodeVerifier(@Nullable String codeVerifier) { 413 if (codeVerifier != null) { 414 CodeVerifierUtil.checkCodeVerifier(codeVerifier); 415 } 416 417 mCodeVerifier = codeVerifier; 418 return this; 419 } 420 421 /** 422 * Specifies an additional set of parameters to be sent as part of the request. 423 */ 424 @NonNull 425 public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) { 426 mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS); 427 return this; 428 } 429 430 /** 431 * Produces a {@link TokenRequest} instance, if all necessary values have been provided. 432 */ 433 @NonNull 434 public TokenRequest build() { 435 String grantType = inferGrantType(); 436 437 if (GrantTypeValues.AUTHORIZATION_CODE.equals(grantType)) { 438 checkNotNull(mAuthorizationCode, 439 "authorization code must be specified for grant_type = " 440 + GrantTypeValues.AUTHORIZATION_CODE); 441 } 442 443 if (GrantTypeValues.REFRESH_TOKEN.equals(grantType)) { 444 checkNotNull(mRefreshToken, 445 "refresh token must be specified for grant_type = " 446 + GrantTypeValues.REFRESH_TOKEN); 447 } 448 449 450 if (grantType.equals(GrantTypeValues.AUTHORIZATION_CODE) && mRedirectUri == null) { 451 throw new IllegalStateException( 452 "no redirect URI specified on token request for code exchange"); 453 } 454 455 return new TokenRequest( 456 mConfiguration, 457 mClientId, 458 mNonce, 459 grantType, 460 mRedirectUri, 461 mScope, 462 mAuthorizationCode, 463 mRefreshToken, 464 mCodeVerifier, 465 Collections.unmodifiableMap(mAdditionalParameters)); 466 } 467 468 private String inferGrantType() { 469 if (mGrantType != null) { 470 return mGrantType; 471 } else if (mAuthorizationCode != null) { 472 return GrantTypeValues.AUTHORIZATION_CODE; 473 } else if (mRefreshToken != null) { 474 return GrantTypeValues.REFRESH_TOKEN; 475 } else { 476 throw new IllegalStateException("grant type not specified and cannot be inferred"); 477 } 478 } 479 } 480 481 private TokenRequest( 482 @NonNull AuthorizationServiceConfiguration configuration, 483 @NonNull String clientId, 484 @Nullable String nonce, 485 @NonNull String grantType, 486 @Nullable Uri redirectUri, 487 @Nullable String scope, 488 @Nullable String authorizationCode, 489 @Nullable String refreshToken, 490 @Nullable String codeVerifier, 491 @NonNull Map<String, String> additionalParameters) { 492 this.configuration = configuration; 493 this.clientId = clientId; 494 this.nonce = nonce; 495 this.grantType = grantType; 496 this.redirectUri = redirectUri; 497 this.scope = scope; 498 this.authorizationCode = authorizationCode; 499 this.refreshToken = refreshToken; 500 this.codeVerifier = codeVerifier; 501 this.additionalParameters = additionalParameters; 502 } 503 504 /** 505 * Derives the set of scopes from the consolidated, space-delimited scopes in the 506 * {@link #scope} field. If no scopes were specified for this request, the method will 507 * return `null`. 508 */ 509 @Nullable 510 public Set<String> getScopeSet() { 511 return AsciiStringListUtil.stringToSet(scope); 512 } 513 514 /** 515 * Produces the set of request parameters for this query, which can be further 516 * processed into a request body. 517 */ 518 @NonNull 519 public Map<String, String> getRequestParameters() { 520 Map<String, String> params = new HashMap<>(); 521 params.put(PARAM_GRANT_TYPE, grantType); 522 putIfNotNull(params, PARAM_REDIRECT_URI, redirectUri); 523 putIfNotNull(params, PARAM_CODE, authorizationCode); 524 putIfNotNull(params, PARAM_REFRESH_TOKEN, refreshToken); 525 putIfNotNull(params, PARAM_CODE_VERIFIER, codeVerifier); 526 putIfNotNull(params, PARAM_SCOPE, scope); 527 528 for (Entry<String, String> param : additionalParameters.entrySet()) { 529 params.put(param.getKey(), param.getValue()); 530 } 531 532 return params; 533 } 534 535 private void putIfNotNull(Map<String, String> map, String key, Object value) { 536 if (value != null) { 537 map.put(key, value.toString()); 538 } 539 } 540 541 /** 542 * Produces a JSON string representation of the token request for persistent storage or 543 * local transmission (e.g. between activities). 544 */ 545 @NonNull 546 public JSONObject jsonSerialize() { 547 JSONObject json = new JSONObject(); 548 JsonUtil.put(json, KEY_CONFIGURATION, configuration.toJson()); 549 JsonUtil.put(json, KEY_CLIENT_ID, clientId); 550 JsonUtil.putIfNotNull(json, KEY_NONCE, nonce); 551 JsonUtil.put(json, KEY_GRANT_TYPE, grantType); 552 JsonUtil.putIfNotNull(json, KEY_REDIRECT_URI, redirectUri); 553 JsonUtil.putIfNotNull(json, KEY_SCOPE, scope); 554 JsonUtil.putIfNotNull(json, KEY_AUTHORIZATION_CODE, authorizationCode); 555 JsonUtil.putIfNotNull(json, KEY_REFRESH_TOKEN, refreshToken); 556 JsonUtil.putIfNotNull(json, KEY_CODE_VERIFIER, codeVerifier); 557 JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, 558 JsonUtil.mapToJsonObject(additionalParameters)); 559 return json; 560 } 561 562 /** 563 * Produces a JSON string representation of the token request for persistent storage or 564 * local transmission (e.g. between activities). This method is just a convenience wrapper 565 * for {@link #jsonSerialize()}, converting the JSON object to its string form. 566 */ 567 @NonNull 568 public String jsonSerializeString() { 569 return jsonSerialize().toString(); 570 } 571 572 /** 573 * Reads a token request from a JSON string representation produced by 574 * {@link #jsonSerialize()}. 575 * @throws JSONException if the provided JSON does not match the expected structure. 576 */ 577 @NonNull 578 public static TokenRequest jsonDeserialize(JSONObject json) throws JSONException { 579 checkNotNull(json, "json object cannot be null"); 580 581 return new TokenRequest( 582 AuthorizationServiceConfiguration.fromJson(json.getJSONObject(KEY_CONFIGURATION)), 583 JsonUtil.getString(json, KEY_CLIENT_ID), 584 JsonUtil.getStringIfDefined(json, KEY_NONCE), 585 JsonUtil.getString(json, KEY_GRANT_TYPE), 586 JsonUtil.getUriIfDefined(json, KEY_REDIRECT_URI), 587 JsonUtil.getStringIfDefined(json, KEY_SCOPE), 588 JsonUtil.getStringIfDefined(json, KEY_AUTHORIZATION_CODE), 589 JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN), 590 JsonUtil.getStringIfDefined(json, KEY_CODE_VERIFIER), 591 JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS)); 592 } 593 594 /** 595 * Reads a token request from a JSON string representation produced by 596 * {@link #jsonSerializeString()}. This method is just a convenience wrapper for 597 * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form. 598 * @throws JSONException if the provided JSON does not match the expected structure. 599 */ 600 @NonNull 601 public static TokenRequest jsonDeserialize(@NonNull String json) throws JSONException { 602 checkNotNull(json, "json string cannot be null"); 603 return jsonDeserialize(new JSONObject(json)); 604 } 605}