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.checkNotNull; 020import static net.openid.appauth.Preconditions.checkNullOrNotEmpty; 021 022import android.content.Intent; 023import android.net.Uri; 024import android.text.TextUtils; 025import androidx.annotation.NonNull; 026import androidx.annotation.Nullable; 027import androidx.annotation.VisibleForTesting; 028 029import net.openid.appauth.internal.UriUtil; 030import org.json.JSONException; 031import org.json.JSONObject; 032 033import java.util.Arrays; 034import java.util.Collections; 035import java.util.HashSet; 036import java.util.LinkedHashMap; 037import java.util.Map; 038import java.util.Set; 039import java.util.concurrent.TimeUnit; 040 041/** 042 * A response to an authorization request. 043 * 044 * @see AuthorizationRequest 045 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2 046 * <https://tools.ietf.org/html/rfc6749#section-4.1.2>" 047 */ 048public class AuthorizationResponse extends AuthorizationManagementResponse { 049 050 /** 051 * The extra string used to store an {@link AuthorizationResponse} in an intent by 052 * {@link #toIntent()}. 053 */ 054 public static final String EXTRA_RESPONSE = "net.openid.appauth.AuthorizationResponse"; 055 056 /** 057 * Indicates that a provided access token is a bearer token. 058 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 7.1 <https://tools.ietf.org/html/rfc6749#section-7.1>" 059 */ 060 public static final String TOKEN_TYPE_BEARER = "bearer"; 061 062 @VisibleForTesting 063 static final String KEY_REQUEST = "request"; 064 065 @VisibleForTesting 066 static final String KEY_ADDITIONAL_PARAMETERS = "additional_parameters"; 067 068 @VisibleForTesting 069 static final String KEY_EXPIRES_AT = "expires_at"; 070 071 // TODO: rename all KEY_* below to PARAM_* - they are standard OAuth2 parameters 072 @VisibleForTesting 073 static final String KEY_STATE = "state"; 074 @VisibleForTesting 075 static final String KEY_TOKEN_TYPE = "token_type"; 076 @VisibleForTesting 077 static final String KEY_AUTHORIZATION_CODE = "code"; 078 @VisibleForTesting 079 static final String KEY_ACCESS_TOKEN = "access_token"; 080 @VisibleForTesting 081 static final String KEY_EXPIRES_IN = "expires_in"; 082 @VisibleForTesting 083 static final String KEY_ID_TOKEN = "id_token"; 084 @VisibleForTesting 085 static final String KEY_SCOPE = "scope"; 086 087 private static final Set<String> BUILT_IN_PARAMS = Collections.unmodifiableSet( 088 new HashSet<>(Arrays.asList( 089 KEY_TOKEN_TYPE, 090 KEY_STATE, 091 KEY_AUTHORIZATION_CODE, 092 KEY_ACCESS_TOKEN, 093 KEY_EXPIRES_IN, 094 KEY_ID_TOKEN, 095 KEY_SCOPE))); 096 097 /** 098 * The authorization request associated with this response. 099 */ 100 @NonNull 101 public final AuthorizationRequest request; 102 103 /** 104 * The returned state parameter, which must match the value specified in the request. 105 * AppAuth for Android ensures that this is the case. 106 */ 107 @Nullable 108 public final String state; 109 110 /** 111 * The type of the retrieved token. Typically this is "Bearer" when present. Otherwise, 112 * another token_type value that the Client has negotiated with the Authorization Server. 113 * 114 * @see "OpenID Connect Core 1.0, Section 3.2.2.5 115 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>" 116 */ 117 @Nullable 118 public final String tokenType; 119 120 /** 121 * The authorization code generated by the authorization server. 122 * Set when the response_type requested includes 'code'. 123 */ 124 @Nullable 125 public final String authorizationCode; 126 127 /** 128 * The access token retrieved as part of the authorization flow. 129 * This is available when the {@link AuthorizationRequest#responseType response_type} 130 * of the request included 'token'. 131 * 132 * @see "OpenID Connect Core 1.0, Section 3.2.2.5 133 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>" 134 */ 135 @Nullable 136 public final String accessToken; 137 138 /** 139 * The approximate expiration time of the access token, as milliseconds from the UNIX epoch. 140 * Set when the requested {@link AuthorizationRequest#responseType response_type} 141 * included 'token'. 142 * 143 * @see "OpenID Connect Core 1.0, Section 3.2.2.5 144 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>" 145 */ 146 @Nullable 147 public final Long accessTokenExpirationTime; 148 149 /** 150 * The id token retrieved as part of the authorization flow. 151 * This is available when the {@link AuthorizationRequest#responseType response_type} 152 * of the request included 'id_token'. 153 * 154 * @see "OpenID Connect Core 1.0, Section 2 155 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2>" 156 * @see "OpenID Connect Core 1.0, Section 3.2.2.5 157 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>" 158 */ 159 @Nullable 160 public final String idToken; 161 162 /** 163 * The scope of the returned access token. If this is not specified, the scope is assumed 164 * to be the same as what was originally requested. 165 */ 166 @Nullable 167 public final String scope; 168 169 /** 170 * The additional, non-standard parameters in the response. 171 */ 172 @NonNull 173 public final Map<String, String> additionalParameters; 174 175 /** 176 * Creates instances of {@link AuthorizationResponse}. 177 */ 178 public static final class Builder { 179 180 @NonNull 181 private AuthorizationRequest mRequest; 182 183 @Nullable 184 private String mState; 185 186 @Nullable 187 private String mTokenType; 188 189 @Nullable 190 private String mAuthorizationCode; 191 192 @Nullable 193 private String mAccessToken; 194 195 @Nullable 196 private Long mAccessTokenExpirationTime; 197 198 @Nullable 199 private String mIdToken; 200 201 @Nullable 202 private String mScope; 203 204 @NonNull 205 private Map<String, String> mAdditionalParameters; 206 207 /** 208 * Creates an authorization builder with the specified mandatory properties. 209 */ 210 public Builder(@NonNull AuthorizationRequest request) { 211 mRequest = checkNotNull(request, "authorization request cannot be null"); 212 mAdditionalParameters = new LinkedHashMap<>(); 213 } 214 215 /** 216 * Extracts authorization response parameters from the query portion of a redirect URI. 217 */ 218 @NonNull 219 public Builder fromUri(@NonNull Uri uri) { 220 return fromUri(uri, SystemClock.INSTANCE); 221 } 222 223 @NonNull 224 @VisibleForTesting 225 Builder fromUri(@NonNull Uri uri, @NonNull Clock clock) { 226 setState(uri.getQueryParameter(KEY_STATE)); 227 setTokenType(uri.getQueryParameter(KEY_TOKEN_TYPE)); 228 setAuthorizationCode(uri.getQueryParameter(KEY_AUTHORIZATION_CODE)); 229 setAccessToken(uri.getQueryParameter(KEY_ACCESS_TOKEN)); 230 setAccessTokenExpiresIn(UriUtil.getLongQueryParameter(uri, KEY_EXPIRES_IN), clock); 231 setIdToken(uri.getQueryParameter(KEY_ID_TOKEN)); 232 setScope(uri.getQueryParameter(KEY_SCOPE)); 233 setAdditionalParameters(extractAdditionalParams(uri, BUILT_IN_PARAMS)); 234 return this; 235 } 236 237 /** 238 * Specifies the OAuth 2 state. 239 */ 240 @NonNull 241 public Builder setState(@Nullable String state) { 242 checkNullOrNotEmpty(state, "state must not be empty"); 243 mState = state; 244 return this; 245 } 246 247 /** 248 * Specifies the OAuth 2 token type. 249 */ 250 @NonNull 251 public Builder setTokenType(@Nullable String tokenType) { 252 checkNullOrNotEmpty(tokenType, "tokenType must not be empty"); 253 mTokenType = tokenType; 254 return this; 255 } 256 257 /** 258 * Specifies the OAuth 2 authorization code. 259 */ 260 @NonNull 261 public Builder setAuthorizationCode(@Nullable String authorizationCode) { 262 checkNullOrNotEmpty(authorizationCode, "authorizationCode must not be empty"); 263 mAuthorizationCode = authorizationCode; 264 return this; 265 } 266 267 /** 268 * Specifies the OAuth 2 access token. 269 */ 270 @NonNull 271 public Builder setAccessToken(@Nullable String accessToken) { 272 checkNullOrNotEmpty(accessToken, "accessToken must not be empty"); 273 mAccessToken = accessToken; 274 return this; 275 } 276 277 /** 278 * Specifies the expiration period of the OAuth 2 access token. 279 */ 280 @NonNull 281 public Builder setAccessTokenExpiresIn(@Nullable Long expiresIn) { 282 return setAccessTokenExpiresIn(expiresIn, SystemClock.INSTANCE); 283 } 284 285 /** 286 * Specifies the relative expiration time of the access token, in seconds, using the 287 * provided clock as the source of the current time. 288 */ 289 @NonNull 290 @VisibleForTesting 291 public Builder setAccessTokenExpiresIn(@Nullable Long expiresIn, @NonNull Clock clock) { 292 if (expiresIn == null) { 293 mAccessTokenExpirationTime = null; 294 } else { 295 mAccessTokenExpirationTime = clock.getCurrentTimeMillis() 296 + TimeUnit.SECONDS.toMillis(expiresIn); 297 } 298 return this; 299 } 300 301 /** 302 * Specifies the expiration time of the OAuth 2 access token. 303 */ 304 @NonNull 305 public Builder setAccessTokenExpirationTime(@Nullable Long expirationTime) { 306 mAccessTokenExpirationTime = expirationTime; 307 return this; 308 } 309 310 /** 311 * Specifies the OAuth 2 Id token. 312 */ 313 @NonNull 314 public Builder setIdToken(@Nullable String idToken) { 315 checkNullOrNotEmpty(idToken, "idToken cannot be empty"); 316 mIdToken = idToken; 317 return this; 318 } 319 320 /** 321 * Specifies the encoded scope string, which is a space-delimited set of 322 * case-sensitive scope identifiers. Replaces any previously specified scope. 323 * 324 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 325 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 326 */ 327 @NonNull 328 public Builder setScope(@Nullable String scope) { 329 if (TextUtils.isEmpty(scope)) { 330 mScope = null; 331 } else { 332 setScopes(scope.split(" +")); 333 } 334 return this; 335 } 336 337 /** 338 * Specifies the set of case-sensitive scopes. Replaces any previously specified set of 339 * scopes. Individual scope strings cannot be null or empty. 340 * 341 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 342 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 343 */ 344 @NonNull 345 public Builder setScopes(String... scopes) { 346 if (scopes == null) { 347 mScope = null; 348 } else { 349 setScopes(Arrays.asList(scopes)); 350 } 351 return this; 352 } 353 354 /** 355 * Specifies the set of case-sensitive scopes. Replaces any previously specified set of 356 * scopes. Individual scope strings cannot be null or empty. 357 * 358 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3 359 * <https://tools.ietf.org/html/rfc6749#section-3.3>" 360 */ 361 @NonNull 362 public Builder setScopes(@Nullable Iterable<String> scopes) { 363 mScope = AsciiStringListUtil.iterableToString(scopes); 364 return this; 365 } 366 367 /** 368 * Specifies the additional set of parameters received as part of the response. 369 */ 370 @NonNull 371 public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) { 372 mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS); 373 return this; 374 } 375 376 /** 377 * Builds the Authorization object. 378 */ 379 @NonNull 380 public AuthorizationResponse build() { 381 return new AuthorizationResponse( 382 mRequest, 383 mState, 384 mTokenType, 385 mAuthorizationCode, 386 mAccessToken, 387 mAccessTokenExpirationTime, 388 mIdToken, 389 mScope, 390 Collections.unmodifiableMap(mAdditionalParameters)); 391 } 392 } 393 394 private AuthorizationResponse( 395 @NonNull AuthorizationRequest request, 396 @Nullable String state, 397 @Nullable String tokenType, 398 @Nullable String authorizationCode, 399 @Nullable String accessToken, 400 @Nullable Long accessTokenExpirationTime, 401 @Nullable String idToken, 402 @Nullable String scope, 403 @NonNull Map<String, String> additionalParameters) { 404 this.request = request; 405 this.state = state; 406 this.tokenType = tokenType; 407 this.authorizationCode = authorizationCode; 408 this.accessToken = accessToken; 409 this.accessTokenExpirationTime = accessTokenExpirationTime; 410 this.idToken = idToken; 411 this.scope = scope; 412 this.additionalParameters = additionalParameters; 413 } 414 415 /** 416 * Determines whether the returned access token has expired. 417 */ 418 public boolean hasAccessTokenExpired() { 419 return hasAccessTokenExpired(SystemClock.INSTANCE); 420 } 421 422 @VisibleForTesting 423 boolean hasAccessTokenExpired(@NonNull Clock clock) { 424 return accessTokenExpirationTime != null 425 && checkNotNull(clock).getCurrentTimeMillis() > accessTokenExpirationTime; 426 } 427 428 /** 429 * Derives the set of scopes from the consolidated, space-delimited scopes in the 430 * {@link #scope} field. If no scopes were specified on this response, the method will 431 * return `null`. 432 */ 433 @Nullable 434 public Set<String> getScopeSet() { 435 return AsciiStringListUtil.stringToSet(scope); 436 } 437 438 /** 439 * Creates a follow-up request to exchange a received authorization code for tokens. 440 */ 441 @NonNull 442 public TokenRequest createTokenExchangeRequest() { 443 return createTokenExchangeRequest(Collections.<String, String>emptyMap()); 444 } 445 446 /** 447 * Creates a follow-up request to exchange a received authorization code for tokens, including 448 * the provided additional parameters. 449 */ 450 @NonNull 451 public TokenRequest createTokenExchangeRequest( 452 @NonNull Map<String, String> additionalExchangeParameters) { 453 checkNotNull(additionalExchangeParameters, 454 "additionalExchangeParameters cannot be null"); 455 456 if (authorizationCode == null) { 457 throw new IllegalStateException("authorizationCode not available for exchange request"); 458 } 459 460 return new TokenRequest.Builder( 461 request.configuration, 462 request.clientId) 463 .setGrantType(GrantTypeValues.AUTHORIZATION_CODE) 464 .setRedirectUri(request.redirectUri) 465 .setCodeVerifier(request.codeVerifier) 466 .setAuthorizationCode(authorizationCode) 467 .setAdditionalParameters(additionalExchangeParameters) 468 .setNonce(request.nonce) 469 .build(); 470 } 471 472 @Override 473 @Nullable 474 public String getState() { 475 return state; 476 } 477 478 /** 479 * Produces a JSON representation of the authorization response for persistent storage or local 480 * transmission (e.g. between activities). 481 */ 482 @Override 483 @NonNull 484 public JSONObject jsonSerialize() { 485 JSONObject json = new JSONObject(); 486 JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize()); 487 JsonUtil.putIfNotNull(json, KEY_STATE, state); 488 JsonUtil.putIfNotNull(json, KEY_TOKEN_TYPE, tokenType); 489 JsonUtil.putIfNotNull(json, KEY_AUTHORIZATION_CODE, authorizationCode); 490 JsonUtil.putIfNotNull(json, KEY_ACCESS_TOKEN, accessToken); 491 JsonUtil.putIfNotNull(json, KEY_EXPIRES_AT, accessTokenExpirationTime); 492 JsonUtil.putIfNotNull(json, KEY_ID_TOKEN, idToken); 493 JsonUtil.putIfNotNull(json, KEY_SCOPE, scope); 494 JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, 495 JsonUtil.mapToJsonObject(additionalParameters)); 496 return json; 497 } 498 499 /** 500 * Reads an authorization response from a JSON string representation produced by 501 * {@link #jsonSerialize()}. 502 * 503 * @throws JSONException if the provided JSON does not match the expected structure. 504 */ 505 @NonNull 506 public static AuthorizationResponse jsonDeserialize(@NonNull JSONObject json) 507 throws JSONException { 508 if (!json.has(KEY_REQUEST)) { 509 throw new IllegalArgumentException( 510 "authorization request not provided and not found in JSON"); 511 } 512 513 return new AuthorizationResponse( 514 AuthorizationRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)), 515 JsonUtil.getStringIfDefined(json, KEY_STATE), 516 JsonUtil.getStringIfDefined(json, KEY_TOKEN_TYPE), 517 JsonUtil.getStringIfDefined(json, KEY_AUTHORIZATION_CODE), 518 JsonUtil.getStringIfDefined(json, KEY_ACCESS_TOKEN), 519 JsonUtil.getLongIfDefined(json, KEY_EXPIRES_AT), 520 JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN), 521 JsonUtil.getStringIfDefined(json, KEY_SCOPE), 522 JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS)); 523 } 524 525 /** 526 * Reads an authorization request from a JSON string representation produced by 527 * {@link #jsonSerializeString()}. This method is just a convenience wrapper for 528 * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form. 529 * 530 * @throws JSONException if the provided JSON does not match the expected structure. 531 */ 532 @NonNull 533 public static AuthorizationResponse jsonDeserialize(@NonNull String jsonStr) 534 throws JSONException { 535 return jsonDeserialize(new JSONObject(jsonStr)); 536 } 537 538 /** 539 * Produces an intent containing this authorization response. This is used to deliver the 540 * authorization response to the registered handler after a call to 541 * {@link AuthorizationService#performAuthorizationRequest}. 542 */ 543 @Override 544 @NonNull 545 public Intent toIntent() { 546 Intent data = new Intent(); 547 data.putExtra(EXTRA_RESPONSE, this.jsonSerializeString()); 548 return data; 549 } 550 551 /** 552 * Extracts an authorization response from an intent produced by {@link #toIntent()}. This is 553 * used to extract the response from the intent data passed to an activity registered as the 554 * handler for {@link AuthorizationService#performAuthorizationRequest}. 555 */ 556 @Nullable 557 public static AuthorizationResponse fromIntent(@NonNull Intent dataIntent) { 558 checkNotNull(dataIntent, "dataIntent must not be null"); 559 if (!dataIntent.hasExtra(EXTRA_RESPONSE)) { 560 return null; 561 } 562 563 try { 564 return AuthorizationResponse.jsonDeserialize(dataIntent.getStringExtra(EXTRA_RESPONSE)); 565 } catch (JSONException ex) { 566 throw new IllegalArgumentException("Intent contains malformed auth response", ex); 567 } 568 } 569 570 static boolean containsAuthorizationResponse(@NonNull Intent intent) { 571 return intent.hasExtra(EXTRA_RESPONSE); 572 } 573}