/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook; import android.annotation.SuppressLint; import android.content.Intent; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.text.TextUtils; import com.facebook.internal.Utility; import com.facebook.internal.Validate; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This class represents an immutable access token for using Facebook APIs. It also includes * associated metadata such as expiration date and permissions. * <p/> * For more information on access tokens, see * <a href="https://developers.facebook.com/docs/facebook-login/access-tokens/">Access Tokens</a>. */ public final class AccessToken implements Parcelable { public static final String ACCESS_TOKEN_KEY = "access_token"; public static final String EXPIRES_IN_KEY = "expires_in"; public static final String USER_ID_KEY = "user_id"; private static final Date MAX_DATE = new Date(Long.MAX_VALUE); private static final Date DEFAULT_EXPIRATION_TIME = MAX_DATE; private static final Date DEFAULT_LAST_REFRESH_TIME = new Date(); private static final AccessTokenSource DEFAULT_ACCESS_TOKEN_SOURCE = AccessTokenSource.FACEBOOK_APPLICATION_WEB; // Constants related to JSON serialization. private static final int CURRENT_JSON_FORMAT = 1; private static final String VERSION_KEY = "version"; private static final String EXPIRES_AT_KEY = "expires_at"; private static final String PERMISSIONS_KEY = "permissions"; private static final String DECLINED_PERMISSIONS_KEY = "declined_permissions"; private static final String TOKEN_KEY = "token"; private static final String SOURCE_KEY = "source"; private static final String LAST_REFRESH_KEY = "last_refresh"; private static final String APPLICATION_ID_KEY = "application_id"; private final Date expires; private final Set<String> permissions; private final Set<String> declinedPermissions; private final String token; private final AccessTokenSource source; private final Date lastRefresh; private final String applicationId; private final String userId; /** * Creates a new AccessToken using the supplied information from a previously-obtained access * token (for instance, from an already-cached access token obtained prior to integration with * the Facebook SDK). Note that the caller is asserting that all parameters provided are correct * with respect to the access token string; no validation is done to verify they are correct. * * @param accessToken the access token string obtained from Facebook * @param applicationId the ID of the Facebook Application associated with this access * token * @param userId the id of the user * @param permissions the permissions that were requested when the token was obtained * (or when it was last reauthorized); may be null if permission set * is unknown * @param declinedPermissions the permissions that were declined when the token was obtained; * may be null if permission set is unknown * @param accessTokenSource an enum indicating how the token was originally obtained (in most * cases, this will be either AccessTokenSource.FACEBOOK_APPLICATION * or AccessTokenSource.WEB_VIEW); if null, FACEBOOK_APPLICATION is * assumed. * @param expirationTime the expiration date associated with the token; if null, an * infinite expiration time is assumed (but will become correct when * the token is refreshed) * @param lastRefreshTime the last time the token was refreshed (or when it was first * obtained); if null, the current time is used. */ public AccessToken( final String accessToken, final String applicationId, final String userId, @Nullable final Collection<String> permissions, @Nullable final Collection<String> declinedPermissions, @Nullable final AccessTokenSource accessTokenSource, @Nullable final Date expirationTime, @Nullable final Date lastRefreshTime ) { Validate.notNullOrEmpty(accessToken, "accessToken"); Validate.notNullOrEmpty(applicationId, "applicationId"); Validate.notNullOrEmpty(userId, "userId"); this.expires = expirationTime != null ? expirationTime : DEFAULT_EXPIRATION_TIME; this.permissions = Collections.unmodifiableSet( permissions != null ? new HashSet<String>(permissions) : new HashSet<String>()); this.declinedPermissions = Collections.unmodifiableSet( declinedPermissions != null ? new HashSet<String>(declinedPermissions) : new HashSet<String>()); this.token = accessToken; this.source = accessTokenSource != null ? accessTokenSource : DEFAULT_ACCESS_TOKEN_SOURCE; this.lastRefresh = lastRefreshTime != null ? lastRefreshTime : DEFAULT_LAST_REFRESH_TIME; this.applicationId = applicationId; this.userId = userId; } /** * Getter for the access token that is current for the application. * * @return The access token that is current for the application. */ public static AccessToken getCurrentAccessToken() { return AccessTokenManager.getInstance().getCurrentAccessToken(); } /** * Setter for the access token that is current for the application. * * @param accessToken The access token to set. */ public static void setCurrentAccessToken(AccessToken accessToken) { AccessTokenManager.getInstance().setCurrentAccessToken(accessToken); } /** * Updates the current access token with up to date permissions, * and extends the expiration date, if extension is possible. */ public static void refreshCurrentAccessTokenAsync() { AccessTokenManager.getInstance().refreshCurrentAccessToken(null); } /** * Updates the current access token with up to date permissions, * and extends the expiration date, if extension is possible. * @param callback */ public static void refreshCurrentAccessTokenAsync(AccessTokenRefreshCallback callback) { AccessTokenManager.getInstance().refreshCurrentAccessToken(callback); } public interface AccessTokenRefreshCallback { void OnTokenRefreshed(AccessToken accessToken); void OnTokenRefreshFailed(FacebookException exception); } /** * Gets the string representing the access token. * * @return the string representing the access token */ public String getToken() { return this.token; } /** * Gets the date at which the access token expires. * * @return the expiration date of the token */ public Date getExpires() { return this.expires; } /** * Gets the list of permissions associated with this access token. Note that the most up-to-date * list of permissions is maintained by Facebook, so this list may be outdated if * permissions have been added or removed since the time the AccessToken object was created. For * more information on permissions, see * https://developers.facebook.com/docs/reference/login/#permissions. * * @return a read-only list of strings representing the permissions granted via this access * token */ public Set<String> getPermissions() { return this.permissions; } /** * Gets the list of permissions declined by the user with this access token. It represents the * entire set of permissions that have been requested and declined. Note that the most * up-to-date list of permissions is maintained by Facebook, so this list may be * outdated if permissions have been granted or declined since the last time an AccessToken * object was created. * * @return a read-only list of strings representing the permissions declined by the user */ public Set<String> getDeclinedPermissions() { return this.declinedPermissions; } /** * Gets the {@link AccessTokenSource} indicating how this access token was obtained. * * @return the enum indicating how the access token was obtained */ public AccessTokenSource getSource() { return source; } /** * Gets the date at which the token was last refreshed. Since tokens expire, the Facebook SDK * will attempt to renew them periodically. * * @return the date at which this token was last refreshed */ public Date getLastRefresh() { return this.lastRefresh; } /** * Gets the ID of the Facebook Application associated with this access token. * * @return the application ID */ public String getApplicationId() { return applicationId; } /** * Returns the user id for this access token. * * @return The user id for this access token. */ public String getUserId() { return userId; } /** * A callback for creating an access token from a NativeLinkingIntent */ public interface AccessTokenCreationCallback { /** * The method called on a successful creation of an AccessToken. * * @param token the access token created from the native link intent. */ public void onSuccess(AccessToken token); public void onError(FacebookException error); } /** * Creates a new AccessToken using the information contained in an Intent populated by the * Facebook application in order to launch a native link. For more information on native * linking, please see https://developers.facebook.com/docs/mobile/android/deep_linking/. * * @param intent the Intent that was used to start an Activity; must not be null * @param applicationId the ID of the Facebook Application associated with this access token */ public static void createFromNativeLinkingIntent( Intent intent, final String applicationId, final AccessTokenCreationCallback accessTokenCallback) { Validate.notNull(intent, "intent"); if (intent.getExtras() == null) { accessTokenCallback.onError( new FacebookException("No extras found on intent")); return; } final Bundle extras = new Bundle(intent.getExtras()); String accessToken = extras.getString(ACCESS_TOKEN_KEY); if (accessToken == null || accessToken.isEmpty()) { accessTokenCallback.onError(new FacebookException("No access token found on intent")); return; } String userId = extras.getString(USER_ID_KEY); // Old versions of facebook for android don't provide the UserId. Obtain the id if missing if (userId == null || userId.isEmpty()) { Utility.getGraphMeRequestWithCacheAsync(accessToken, new Utility.GraphMeRequestWithCacheCallback() { @Override public void onSuccess(JSONObject userInfo) { try { String userId = userInfo.getString("id"); extras.putString(USER_ID_KEY, userId); accessTokenCallback.onSuccess(createFromBundle( null, extras, AccessTokenSource.FACEBOOK_APPLICATION_WEB, new Date(), applicationId)); } catch (JSONException ex) { accessTokenCallback.onError( new FacebookException( "Unable to generate access token due to missing user id")); } } @Override public void onFailure(FacebookException error) { accessTokenCallback.onError(error); } }); } else { accessTokenCallback.onSuccess(createFromBundle( null, extras, AccessTokenSource.FACEBOOK_APPLICATION_WEB, new Date(), applicationId)); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("{AccessToken"); builder.append(" token:").append(tokenToString()); appendPermissions(builder); builder.append("}"); return builder.toString(); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof AccessToken)) { return false; } AccessToken o = (AccessToken) other; return expires.equals(o.expires) && permissions.equals(o.permissions) && declinedPermissions.equals(o.declinedPermissions) && token.equals(o.token) && source == o.source && lastRefresh.equals(o.lastRefresh) && (applicationId == null ? o.applicationId == null : applicationId.equals(o.applicationId)) && userId.equals(o.userId); } @Override public int hashCode() { int result = 17; result = result * 31 + expires.hashCode(); result = result * 31 + permissions.hashCode(); result = result * 31 + declinedPermissions.hashCode(); result = result * 31 + token.hashCode(); result = result * 31 + source.hashCode(); result = result * 31 + lastRefresh.hashCode(); result = result * 31 + (applicationId == null ? 0 : applicationId.hashCode()); result = result * 31 + userId.hashCode(); return result; } @SuppressLint("FieldGetter") static AccessToken createFromRefresh(AccessToken current, Bundle bundle) { // Only tokens obtained via SSO support refresh. Token refresh returns the expiration date // in seconds from the epoch rather than seconds from now. if (current.source != AccessTokenSource.FACEBOOK_APPLICATION_WEB && current.source != AccessTokenSource.FACEBOOK_APPLICATION_NATIVE && current.source != AccessTokenSource.FACEBOOK_APPLICATION_SERVICE) { throw new FacebookException("Invalid token source: " + current.source); } Date expires = Utility.getBundleLongAsDate(bundle, EXPIRES_IN_KEY, new Date(0)); String token = bundle.getString(ACCESS_TOKEN_KEY); if (Utility.isNullOrEmpty(token)) { return null; } return new AccessToken( token, current.applicationId, current.getUserId(), current.getPermissions(), current.getDeclinedPermissions(), current.source, expires, new Date()); } static AccessToken createFromLegacyCache(Bundle bundle) { List<String> permissions = getPermissionsFromBundle( bundle, LegacyTokenHelper.PERMISSIONS_KEY); List<String> declinedPermissions = getPermissionsFromBundle( bundle, LegacyTokenHelper.DECLINED_PERMISSIONS_KEY); String applicationId = LegacyTokenHelper.getApplicationId(bundle); if (Utility.isNullOrEmpty(applicationId)) { applicationId = FacebookSdk.getApplicationId(); } String tokenString = LegacyTokenHelper.getToken(bundle); String userId; JSONObject userInfo = Utility.awaitGetGraphMeRequestWithCache(tokenString); try { userId = userInfo.getString("id"); } catch (JSONException ex) { // This code is only used by AccessTokenCache. If we for any reason fail to get the // user id just return null. return null; } return new AccessToken( tokenString, applicationId, userId, permissions, declinedPermissions, LegacyTokenHelper.getSource(bundle), LegacyTokenHelper.getDate( bundle, LegacyTokenHelper.EXPIRATION_DATE_KEY), LegacyTokenHelper.getDate( bundle, LegacyTokenHelper.LAST_REFRESH_DATE_KEY) ); } static List<String> getPermissionsFromBundle(Bundle bundle, String key) { // Copy the list so we can guarantee immutable List<String> originalPermissions = bundle.getStringArrayList(key); List<String> permissions; if (originalPermissions == null) { permissions = Collections.emptyList(); } else { permissions = Collections.unmodifiableList(new ArrayList<String>(originalPermissions)); } return permissions; } /** * Shows if the token is expired. * * @return true if the token is expired. */ public boolean isExpired() { return new Date().after(this.expires); } JSONObject toJSONObject() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put(VERSION_KEY, CURRENT_JSON_FORMAT); jsonObject.put(TOKEN_KEY, token); jsonObject.put(EXPIRES_AT_KEY, expires.getTime()); JSONArray permissionsArray = new JSONArray(permissions); jsonObject.put(PERMISSIONS_KEY, permissionsArray); JSONArray declinedPermissionsArray = new JSONArray(declinedPermissions); jsonObject.put(DECLINED_PERMISSIONS_KEY, declinedPermissionsArray); jsonObject.put(LAST_REFRESH_KEY, lastRefresh.getTime()); jsonObject.put(SOURCE_KEY, source.name()); jsonObject.put(APPLICATION_ID_KEY, applicationId); jsonObject.put(USER_ID_KEY, userId); return jsonObject; } static AccessToken createFromJSONObject(JSONObject jsonObject) throws JSONException { int version = jsonObject.getInt(VERSION_KEY); if (version > CURRENT_JSON_FORMAT) { throw new FacebookException("Unknown AccessToken serialization format."); } String token = jsonObject.getString(TOKEN_KEY); Date expiresAt = new Date(jsonObject.getLong(EXPIRES_AT_KEY)); JSONArray permissionsArray = jsonObject.getJSONArray(PERMISSIONS_KEY); JSONArray declinedPermissionsArray = jsonObject.getJSONArray(DECLINED_PERMISSIONS_KEY); Date lastRefresh = new Date(jsonObject.getLong(LAST_REFRESH_KEY)); AccessTokenSource source = AccessTokenSource.valueOf(jsonObject.getString(SOURCE_KEY)); String applicationId = jsonObject.getString(APPLICATION_ID_KEY); String userId = jsonObject.getString(USER_ID_KEY); return new AccessToken( token, applicationId, userId, Utility.jsonArrayToStringList(permissionsArray), Utility.jsonArrayToStringList(declinedPermissionsArray), source, expiresAt, lastRefresh); } private static AccessToken createFromBundle( List<String> requestedPermissions, Bundle bundle, AccessTokenSource source, Date expirationBase, String applicationId) { String token = bundle.getString(ACCESS_TOKEN_KEY); Date expires = Utility.getBundleLongAsDate(bundle, EXPIRES_IN_KEY, expirationBase); String userId = bundle.getString(USER_ID_KEY); if (Utility.isNullOrEmpty(token) || (expires == null)) { return null; } return new AccessToken( token, applicationId, userId, requestedPermissions, null, source, expires, new Date()); } private String tokenToString() { if (this.token == null) { return "null"; } else if (FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.INCLUDE_ACCESS_TOKENS)) { return this.token; } else { return "ACCESS_TOKEN_REMOVED"; } } private void appendPermissions(StringBuilder builder) { builder.append(" permissions:"); if (this.permissions == null) { builder.append("null"); } else { builder.append("["); builder.append(TextUtils.join(", ", permissions)); builder.append("]"); } } AccessToken(Parcel parcel) { this.expires = new Date(parcel.readLong()); ArrayList<String> permissionsList = new ArrayList<>(); parcel.readStringList(permissionsList); this.permissions = Collections.unmodifiableSet(new HashSet<String>(permissionsList)); permissionsList.clear(); parcel.readStringList(permissionsList); this.declinedPermissions = Collections.unmodifiableSet( new HashSet<String>(permissionsList)); this.token = parcel.readString(); this.source = AccessTokenSource.valueOf(parcel.readString()); this.lastRefresh = new Date(parcel.readLong()); this.applicationId = parcel.readString(); this.userId = parcel.readString(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(expires.getTime()); dest.writeStringList(new ArrayList<String>(permissions)); dest.writeStringList(new ArrayList<String>(declinedPermissions)); dest.writeString(token); dest.writeString(source.name()); dest.writeLong(lastRefresh.getTime()); dest.writeString(applicationId); dest.writeString(userId); } public static final Parcelable.Creator<AccessToken> CREATOR = new Parcelable.Creator() { @Override public AccessToken createFromParcel(Parcel source) { return new AccessToken(source); } @Override public AccessToken[] newArray(int size) { return new AccessToken[size]; } }; }