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.AdditionalParamsProcessor.extractAdditionalParams; 019import static net.openid.appauth.Preconditions.checkNotEmpty; 020import static net.openid.appauth.Preconditions.checkNotNull; 021import static net.openid.appauth.Preconditions.checkNullOrNotEmpty; 022 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.HashSet; 034import java.util.Map; 035import java.util.Set; 036import java.util.concurrent.TimeUnit; 037 038/** 039 * A response to a token request. 040 * 041 * @see TokenRequest 042 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.4 043 * <https://tools.ietf.org/html/rfc6749#section-4.1.4>" 044 */ 045public class TokenResponse { 046 047 /** 048 * Indicates that a provided access token is a bearer token. 049 * 050 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 7.1 051 * <https://tools.ietf.org/html/rfc6749#section-7.1>" 052 */ 053 public static final String TOKEN_TYPE_BEARER = "Bearer"; 054 055 @VisibleForTesting 056 static final String KEY_REQUEST = "request"; 057 058 @VisibleForTesting 059 static final String KEY_EXPIRES_AT = "expires_at"; 060 061 // TODO: rename all KEY_* below to PARAM_* 062 @VisibleForTesting 063 static final String KEY_TOKEN_TYPE = "token_type"; 064 065 @VisibleForTesting 066 static final String KEY_ACCESS_TOKEN = "access_token"; 067 068 @VisibleForTesting 069 static final String KEY_EXPIRES_IN = "expires_in"; 070 071 @VisibleForTesting 072 static final String KEY_REFRESH_TOKEN = "refresh_token"; 073 074 @VisibleForTesting 075 static final String KEY_ID_TOKEN = "id_token"; 076 077 @VisibleForTesting 078 static final String KEY_SCOPE = "scope"; 079 080 @VisibleForTesting 081 static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; 082 083 private static final Set<String> BUILT_IN_PARAMS = new HashSet<>(Arrays.asList( 084 KEY_TOKEN_TYPE, 085 KEY_ACCESS_TOKEN, 086 KEY_EXPIRES_IN, 087 KEY_REFRESH_TOKEN, 088 KEY_ID_TOKEN, 089 KEY_SCOPE 090 )); 091 092 /** 093 * The token request associated with this response. 094 */ 095 @NonNull 096 public final TokenRequest request; 097 098 /** 099 * The type of the token returned. Typically this is {@link #TOKEN_TYPE_BEARER}, or some 100 * other token type that the client has negotiated with the authorization service. 101 * 102 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.4 103 * <https://tools.ietf.org/html/rfc6749#section-4.1.4>" 104 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1 105 * <https://tools.ietf.org/html/rfc6749#section-5.1>" 106 */ 107 @Nullable 108 public final String tokenType; 109 110 /** 111 * The access token, if provided. 112 * 113 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1 114 * <https://tools.ietf.org/html/rfc6749#section-5.1>" 115 */ 116 @Nullable 117 public final String accessToken; 118 119 /** 120 * The expiration time of the access token, if provided. If an access token is provided but the 121 * expiration time is not, then the expiration time is typically some default value specified 122 * by the identity provider through some other means, such as documentation or an additional 123 * non-standard field. 124 */ 125 @Nullable 126 public final Long accessTokenExpirationTime; 127 128 /** 129 * The ID token describing the authenticated user, if provided. 130 * 131 * @see "OpenID Connect Core 1.0, Section 2 132 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2>" 133 */ 134 @Nullable 135 public final String idToken; 136 137 /** 138 * The refresh token, if provided. 139 * 140 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1 141 * <https://tools.ietf.org/html/rfc6749#section-5.1>" 142 */ 143 @Nullable 144 public final String refreshToken; 145 146 /** 147 * The scope of the access token. If the scope is identical to that originally 148 * requested, then this value is optional. 149 * 150 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1 151 * <https://tools.ietf.org/html/rfc6749#section-5.1>" 152 */ 153 @Nullable 154 public final String scope; 155 156 /** 157 * Additional, non-standard parameters in the response. 158 */ 159 @NonNull 160 public final Map<String, String> additionalParameters; 161 162 /** 163 * Creates instances of {@link TokenResponse}. 164 */ 165 public static final class Builder { 166 @NonNull 167 private TokenRequest mRequest; 168 169 @Nullable 170 private String mTokenType; 171 172 @Nullable 173 private String mAccessToken; 174 175 @Nullable 176 private Long mAccessTokenExpirationTime; 177 178 @Nullable 179 private String mIdToken; 180 181 @Nullable 182 private String mRefreshToken; 183 184 @Nullable 185 private String mScope; 186 187 @NonNull 188 private Map<String, String> mAdditionalParameters; 189 190 /** 191 * Creates a token response associated with the specified request. 192 */ 193 public Builder(@NonNull TokenRequest request) { 194 setRequest(request); 195 mAdditionalParameters = Collections.emptyMap(); 196 } 197 198 /** 199 * Extracts token response fields from a JSON string. 200 * 201 * @throws JSONException if the JSON is malformed or has incorrect value types for fields. 202 */ 203 @NonNull 204 public Builder fromResponseJsonString(@NonNull String jsonStr) throws JSONException { 205 checkNotEmpty(jsonStr, "json cannot be null or empty"); 206 return fromResponseJson(new JSONObject(jsonStr)); 207 } 208 209 /** 210 * Extracts token response fields from a JSON object. 211 * 212 * @throws JSONException if the JSON is malformed or has incorrect value types for fields. 213 */ 214 @NonNull 215 public Builder fromResponseJson(@NonNull JSONObject json) throws JSONException { 216 setTokenType(JsonUtil.getString(json, KEY_TOKEN_TYPE)); 217 setAccessToken(JsonUtil.getStringIfDefined(json, KEY_ACCESS_TOKEN)); 218 setAccessTokenExpirationTime(JsonUtil.getLongIfDefined(json, KEY_EXPIRES_AT)); 219 if (json.has(KEY_EXPIRES_IN)) { 220 setAccessTokenExpiresIn(json.getLong(KEY_EXPIRES_IN)); 221 } 222 setRefreshToken(JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN)); 223 setIdToken(JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN)); 224 setScope(JsonUtil.getStringIfDefined(json, KEY_SCOPE)); 225 setAdditionalParameters(extractAdditionalParams(json, BUILT_IN_PARAMS)); 226 227 return this; 228 } 229 230 /** 231 * Specifies the request associated with this response. Must not be null. 232 */ 233 @NonNull 234 public Builder setRequest(@NonNull TokenRequest request) { 235 mRequest = checkNotNull(request, "request cannot be null"); 236 return this; 237 } 238 239 /** 240 * Specifies the token type of the access token in this response. If not null, the value 241 * must be non-empty. 242 */ 243 @NonNull 244 public Builder setTokenType(@Nullable String tokenType) { 245 mTokenType = checkNullOrNotEmpty(tokenType, "token type must not be empty if defined"); 246 return this; 247 } 248 249 /** 250 * Specifies the access token. If not null, the value must be non-empty. 251 */ 252 @NonNull 253 public Builder setAccessToken(@Nullable String accessToken) { 254 mAccessToken = checkNullOrNotEmpty(accessToken, 255 "access token cannot be empty if specified"); 256 return this; 257 } 258 259 /** 260 * Sets the relative expiration time of the access token, in seconds, using the default 261 * system clock as the source of the current time. 262 */ 263 @NonNull 264 public Builder setAccessTokenExpiresIn(@NonNull Long expiresIn) { 265 return setAccessTokenExpiresIn(expiresIn, SystemClock.INSTANCE); 266 } 267 268 /** 269 * Sets the relative expiration time of the access token, in seconds, using the provided 270 * clock as the source of the current time. 271 */ 272 @NonNull 273 @VisibleForTesting 274 Builder setAccessTokenExpiresIn(@Nullable Long expiresIn, @NonNull Clock clock) { 275 if (expiresIn == null) { 276 mAccessTokenExpirationTime = null; 277 } else { 278 mAccessTokenExpirationTime = clock.getCurrentTimeMillis() 279 + TimeUnit.SECONDS.toMillis(expiresIn); 280 } 281 return this; 282 } 283 284 /** 285 * Sets the exact expiration time of the access token, in milliseconds since the UNIX epoch. 286 */ 287 @NonNull 288 public Builder setAccessTokenExpirationTime(@Nullable Long expiresAt) { 289 mAccessTokenExpirationTime = expiresAt; 290 return this; 291 } 292 293 /** 294 * Specifies the ID token. If not null, the value must be non-empty. 295 */ 296 public Builder setIdToken(@Nullable String idToken) { 297 mIdToken = checkNullOrNotEmpty(idToken, "id token must not be empty if defined"); 298 return this; 299 } 300 301 /** 302 * Specifies the refresh token. If not null, the value must be non-empty. 303 */ 304 public Builder setRefreshToken(@Nullable String refreshToken) { 305 mRefreshToken = checkNullOrNotEmpty(refreshToken, 306 "refresh token must not be empty if defined"); 307 return this; 308 } 309 310 /** 311 * Specifies the encoded scope string, which is a space-delimited set of 312 * case-sensitive scope identifiers. Replaces any previously specified scope. 313 * 314 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 315 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 316 */ 317 @NonNull 318 public Builder setScope(@Nullable String scope) { 319 if (TextUtils.isEmpty(scope)) { 320 mScope = null; 321 } else { 322 setScopes(scope.split(" +")); 323 } 324 return this; 325 } 326 327 /** 328 * Specifies the set of case-sensitive scopes. Replaces any previously specified set of 329 * scopes. Individual scope strings cannot be null or empty. 330 * 331 * <p>Scopes specified here are used to obtain a "down-scoped" access token, where the 332 * set of scopes specified _must_ be a subset of those already granted in 333 * previous requests. 334 * 335 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 336 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 337 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6 338 * <https://tools.ietf.org/html/rfc6749#section-6>" 339 */ 340 @NonNull 341 public Builder setScopes(String... scopes) { 342 if (scopes == null) { 343 scopes = new String[0]; 344 } 345 setScopes(Arrays.asList(scopes)); 346 return this; 347 } 348 349 /** 350 * Specifies the set of case-sensitive scopes. Replaces any previously specified set of 351 * scopes. Individual scope strings cannot be null or empty. 352 * 353 * <p>Scopes specified here are used to obtain a "down-scoped" access token, where the 354 * set of scopes specified _must_ be a subset of those already granted in 355 * previous requests. 356 * 357 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 358 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 359 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6 360 * <https://tools.ietf.org/html/rfc6749#section-6>" 361 */ 362 @NonNull 363 public Builder setScopes(@Nullable Iterable<String> scopes) { 364 mScope = AsciiStringListUtil.iterableToString(scopes); 365 return this; 366 } 367 368 /** 369 * Specifies the additional, non-standard parameters received as part of the response. 370 */ 371 @NonNull 372 public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) { 373 mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS); 374 return this; 375 } 376 377 /** 378 * Creates the token response instance. 379 */ 380 public TokenResponse build() { 381 return new TokenResponse( 382 mRequest, 383 mTokenType, 384 mAccessToken, 385 mAccessTokenExpirationTime, 386 mIdToken, 387 mRefreshToken, 388 mScope, 389 mAdditionalParameters); 390 } 391 } 392 393 TokenResponse( 394 @NonNull TokenRequest request, 395 @Nullable String tokenType, 396 @Nullable String accessToken, 397 @Nullable Long accessTokenExpirationTime, 398 @Nullable String idToken, 399 @Nullable String refreshToken, 400 @Nullable String scope, 401 @NonNull Map<String, String> additionalParameters) { 402 this.request = request; 403 this.tokenType = tokenType; 404 this.accessToken = accessToken; 405 this.accessTokenExpirationTime = accessTokenExpirationTime; 406 this.idToken = idToken; 407 this.refreshToken = refreshToken; 408 this.scope = scope; 409 this.additionalParameters = additionalParameters; 410 } 411 412 /** 413 * Derives the set of scopes from the consolidated, space-delimited scopes in the 414 * {@link #scope} field. If no scopes were specified on this response, the method will 415 * return `null`. 416 */ 417 @Nullable 418 public Set<String> getScopeSet() { 419 return AsciiStringListUtil.stringToSet(scope); 420 } 421 422 /** 423 * Produces a JSON string representation of the token response for persistent storage or 424 * local transmission (e.g. between activities). 425 */ 426 public JSONObject jsonSerialize() { 427 JSONObject json = new JSONObject(); 428 JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize()); 429 JsonUtil.putIfNotNull(json, KEY_TOKEN_TYPE, tokenType); 430 JsonUtil.putIfNotNull(json, KEY_ACCESS_TOKEN, accessToken); 431 JsonUtil.putIfNotNull(json, KEY_EXPIRES_AT, accessTokenExpirationTime); 432 JsonUtil.putIfNotNull(json, KEY_ID_TOKEN, idToken); 433 JsonUtil.putIfNotNull(json, KEY_REFRESH_TOKEN, refreshToken); 434 JsonUtil.putIfNotNull(json, KEY_SCOPE, scope); 435 JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, 436 JsonUtil.mapToJsonObject(additionalParameters)); 437 return json; 438 } 439 440 /** 441 * Produces a JSON string representation of the token response for persistent storage or 442 * local transmission (e.g. between activities). This method is just a convenience wrapper 443 * for {@link #jsonSerialize()}, converting the JSON object to its string form. 444 */ 445 public String jsonSerializeString() { 446 return jsonSerialize().toString(); 447 } 448 449 /** 450 * Reads a token response from a JSON string, and associates it with the provided request. 451 * If a request is not provided, its serialized form is expected to be found in the JSON 452 * (as if produced by a prior call to {@link #jsonSerialize()}. 453 * @throws JSONException if the JSON is malformed or missing required fields. 454 */ 455 @NonNull 456 public static TokenResponse jsonDeserialize(@NonNull JSONObject json) throws JSONException { 457 if (!json.has(KEY_REQUEST)) { 458 throw new IllegalArgumentException( 459 "token request not provided and not found in JSON"); 460 } 461 return new TokenResponse( 462 TokenRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)), 463 JsonUtil.getStringIfDefined(json, KEY_TOKEN_TYPE), 464 JsonUtil.getStringIfDefined(json, KEY_ACCESS_TOKEN), 465 JsonUtil.getLongIfDefined(json, KEY_EXPIRES_AT), 466 JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN), 467 JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN), 468 JsonUtil.getStringIfDefined(json, KEY_SCOPE), 469 JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS)); 470 } 471 472 /** 473 * Reads a token response from a JSON string, and associates it with the provided request. 474 * If a request is not provided, its serialized form is expected to be found in the JSON 475 * (as if produced by a prior call to {@link #jsonSerialize()}. 476 * @throws JSONException if the JSON is malformed or missing required fields. 477 */ 478 @NonNull 479 public static TokenResponse jsonDeserialize(@NonNull String jsonStr) throws JSONException { 480 checkNotEmpty(jsonStr, "jsonStr cannot be null or empty"); 481 return jsonDeserialize(new JSONObject(jsonStr)); 482 } 483}