[Auth:Last Credentials Timing] : Introducing API
Storing last successful sign-in/authentication timings, and providing that
information as extra's in updateCredentials and confirmCredentials.
Also, adding a new api: AccountManager#accountAuthenticated(Account).
Change-Id: Icd0dac35b13d61bc28a2e045b96caefffeb353be
diff --git a/api/current.txt b/api/current.txt
index c7898e4..afa2137 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2719,6 +2719,7 @@
}
public class AccountManager {
+ method public boolean accountAuthenticated(android.accounts.Account);
method public android.accounts.AccountManagerFuture<android.os.Bundle> addAccount(java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
method public boolean addAccountExplicitly(android.accounts.Account, java.lang.String, android.os.Bundle);
method public void addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean);
@@ -2779,6 +2780,7 @@
field public static final java.lang.String KEY_ERROR_CODE = "errorCode";
field public static final java.lang.String KEY_ERROR_MESSAGE = "errorMessage";
field public static final java.lang.String KEY_INTENT = "intent";
+ field public static final java.lang.String KEY_LAST_AUTHENTICATE_TIME_MILLIS_EPOCH = "lastAuthenticatedTimeMillisEpoch";
field public static final java.lang.String KEY_PASSWORD = "password";
field public static final java.lang.String KEY_USERDATA = "userdata";
field public static final java.lang.String LOGIN_ACCOUNTS_CHANGED_ACTION = "android.accounts.LOGIN_ACCOUNTS_CHANGED";
diff --git a/api/system-current.txt b/api/system-current.txt
index f274a0d..bf3a5eb 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2798,6 +2798,7 @@
}
public class AccountManager {
+ method public boolean accountAuthenticated(android.accounts.Account);
method public android.accounts.AccountManagerFuture<android.os.Bundle> addAccount(java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
method public boolean addAccountExplicitly(android.accounts.Account, java.lang.String, android.os.Bundle);
method public void addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean);
@@ -2858,6 +2859,7 @@
field public static final java.lang.String KEY_ERROR_CODE = "errorCode";
field public static final java.lang.String KEY_ERROR_MESSAGE = "errorMessage";
field public static final java.lang.String KEY_INTENT = "intent";
+ field public static final java.lang.String KEY_LAST_AUTHENTICATE_TIME_MILLIS_EPOCH = "lastAuthenticatedTimeMillisEpoch";
field public static final java.lang.String KEY_PASSWORD = "password";
field public static final java.lang.String KEY_USERDATA = "userdata";
field public static final java.lang.String LOGIN_ACCOUNTS_CHANGED_ACTION = "android.accounts.LOGIN_ACCOUNTS_CHANGED";
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index 6957435c..480d171 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -203,6 +203,14 @@
public static final String KEY_USERDATA = "userdata";
/**
+ * Bundle key used to supply the last time the credentials of the account
+ * were authenticated successfully. Time is specified in milliseconds since
+ * epoch.
+ */
+ public static final String KEY_LAST_AUTHENTICATE_TIME_MILLIS_EPOCH =
+ "lastAuthenticatedTimeMillisEpoch";
+
+ /**
* Authenticators using 'customTokens' option will also get the UID of the
* caller
*/
@@ -663,6 +671,31 @@
}
/**
+ * Informs the system that the account has been authenticated recently. This
+ * recency may be used by other applications to verify the account. This
+ * should be called only when the user has entered correct credentials for
+ * the account.
+ * <p>
+ * It is not safe to call this method from the main thread. As such, call it
+ * from another thread.
+ * <p>
+ * This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and should be
+ * called from the account's authenticator.
+ *
+ * @param account The {@link Account} to be updated.
+ */
+ public boolean accountAuthenticated(Account account) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ try {
+ return mService.accountAuthenticated(account);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
* Rename the specified {@link Account}. This is equivalent to removing
* the existing account and adding a new renamed account with the old
* account's user data.
@@ -1544,15 +1577,20 @@
* with these fields if activity or password was supplied and
* the account was successfully verified:
* <ul>
- * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account created
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account verified
* <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
* <li> {@link #KEY_BOOLEAN_RESULT} - true to indicate success
* </ul>
*
* If no activity or password was specified, the returned Bundle contains
- * only {@link #KEY_INTENT} with the {@link Intent} needed to launch the
- * password prompt. If an error occurred,
- * {@link AccountManagerFuture#getResult()} throws:
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * password prompt.
+ *
+ * <p>Also the returning Bundle may contain {@link
+ * #KEY_LAST_AUTHENTICATE_TIME_MILLIS_EPOCH} indicating the last time the
+ * credential was validated/created.
+ *
+ * If an error occurred,{@link AccountManagerFuture#getResult()} throws:
* <ul>
* <li> {@link AuthenticatorException} if the authenticator failed to respond
* <li> {@link OperationCanceledException} if the operation was canceled for
@@ -1625,9 +1663,9 @@
* <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
* </ul>
*
- * If no activity was specified, the returned Bundle contains only
+ * If no activity was specified, the returned Bundle contains
* {@link #KEY_INTENT} with the {@link Intent} needed to launch the
- * password prompt. If an error occurred,
+ * password prompt. If an error occurred,
* {@link AccountManagerFuture#getResult()} throws:
* <ul>
* <li> {@link AuthenticatorException} if the authenticator failed to respond
diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl
index aa41161..04b3c88 100644
--- a/core/java/android/accounts/IAccountManager.aidl
+++ b/core/java/android/accounts/IAccountManager.aidl
@@ -67,6 +67,7 @@
boolean expectActivityLaunch);
void confirmCredentialsAsUser(in IAccountManagerResponse response, in Account account,
in Bundle options, boolean expectActivityLaunch, int userId);
+ boolean accountAuthenticated(in Account account);
void getAuthTokenLabel(in IAccountManagerResponse response, String accountType,
String authTokenType);
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 9339b35..90ef0a7 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -109,7 +109,7 @@
private static final int TIMEOUT_DELAY_MS = 1000 * 60;
private static final String DATABASE_NAME = "accounts.db";
- private static final int DATABASE_VERSION = 6;
+ private static final int DATABASE_VERSION = 7;
private final Context mContext;
@@ -131,6 +131,8 @@
private static final String ACCOUNTS_TYPE_COUNT = "count(type)";
private static final String ACCOUNTS_PASSWORD = "password";
private static final String ACCOUNTS_PREVIOUS_NAME = "previous_name";
+ private static final String ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS =
+ "last_password_entry_time_millis_epoch";
private static final String TABLE_AUTHTOKENS = "authtokens";
private static final String AUTHTOKENS_ID = "_id";
@@ -697,7 +699,8 @@
long identityToken = clearCallingIdentity();
try {
new Session(fromAccounts, response, account.type, false,
- false /* stripAuthTokenFromResult */) {
+ false /* stripAuthTokenFromResult */, account.name,
+ false /* authDetailsRequired */) {
@Override
protected String toDebugString(long now) {
return super.toDebugString(now) + ", getAccountCredentialsForClone"
@@ -725,12 +728,43 @@
}
}
+ @Override
+ public boolean accountAuthenticated(final Account account) {
+ if (account == null) {
+ throw new IllegalArgumentException("account is null");
+ }
+ checkAuthenticateAccountsPermission(account);
+
+ final UserAccounts accounts = getUserAccountsForCaller();
+ int userId = Binder.getCallingUserHandle().getIdentifier();
+ if (!canUserModifyAccounts(userId) || !canUserModifyAccountsForType(userId, account.type)) {
+ return false;
+ }
+ synchronized (accounts.cacheLock) {
+ final ContentValues values = new ContentValues();
+ values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis());
+ final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+ int i = db.update(
+ TABLE_ACCOUNTS,
+ values,
+ ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?",
+ new String[] {
+ account.name, account.type
+ });
+ if (i > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void completeCloningAccount(IAccountManagerResponse response,
final Bundle accountCredentials, final Account account, final UserAccounts targetUser) {
long id = clearCallingIdentity();
try {
new Session(targetUser, response, account.type, false,
- false /* stripAuthTokenFromResult */) {
+ false /* stripAuthTokenFromResult */, account.name,
+ false /* authDetailsRequired */) {
@Override
protected String toDebugString(long now) {
return super.toDebugString(now) + ", getAccountCredentialsForClone"
@@ -795,6 +829,7 @@
values.put(ACCOUNTS_NAME, account.name);
values.put(ACCOUNTS_TYPE, account.type);
values.put(ACCOUNTS_PASSWORD, password);
+ values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis());
long accountId = db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values);
if (accountId < 0) {
Log.w(TAG, "insertAccountIntoDatabase: " + account
@@ -885,7 +920,8 @@
public TestFeaturesSession(UserAccounts accounts, IAccountManagerResponse response,
Account account, String[] features) {
super(accounts, response, account.type, false /* expectActivityLaunch */,
- true /* stripAuthTokenFromResult */);
+ true /* stripAuthTokenFromResult */, account.name,
+ false /* authDetailsRequired */);
mFeatures = features;
mAccount = account;
}
@@ -1184,7 +1220,8 @@
public RemoveAccountSession(UserAccounts accounts, IAccountManagerResponse response,
Account account, boolean expectActivityLaunch) {
super(accounts, response, account.type, expectActivityLaunch,
- true /* stripAuthTokenFromResult */);
+ true /* stripAuthTokenFromResult */, account.name,
+ false /* authDetailsRequired */);
mAccount = account;
}
@@ -1419,6 +1456,13 @@
try {
final ContentValues values = new ContentValues();
values.put(ACCOUNTS_PASSWORD, password);
+ long time = 0;
+ // Only set current time, if it is a valid password. For clear password case, it
+ // should not be set.
+ if (password != null) {
+ time = System.currentTimeMillis();
+ }
+ values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, time);
final long accountId = getAccountIdLocked(db, account);
if (accountId >= 0) {
final String[] argsAccountId = {String.valueOf(accountId)};
@@ -1547,8 +1591,9 @@
UserAccounts accounts = getUserAccounts(UserHandle.getUserId(callingUid));
long identityToken = clearCallingIdentity();
try {
- new Session(accounts, response, accountType, false,
- false /* stripAuthTokenFromResult */) {
+ new Session(accounts, response, accountType, false /* expectActivityLaunch */,
+ false /* stripAuthTokenFromResult */, null /* accountName */,
+ false /* authDetailsRequired */) {
@Override
protected String toDebugString(long now) {
return super.toDebugString(now) + ", getAuthTokenLabel"
@@ -1648,7 +1693,8 @@
}
new Session(accounts, response, account.type, expectActivityLaunch,
- false /* stripAuthTokenFromResult */) {
+ false /* stripAuthTokenFromResult */, account.name,
+ false /* authDetailsRequired */) {
@Override
protected String toDebugString(long now) {
if (loginOptions != null) loginOptions.keySet();
@@ -1842,7 +1888,8 @@
long identityToken = clearCallingIdentity();
try {
new Session(accounts, response, accountType, expectActivityLaunch,
- true /* stripAuthTokenFromResult */) {
+ true /* stripAuthTokenFromResult */, null /* accountName */,
+ false /* authDetailsRequired */) {
@Override
public void run() throws RemoteException {
mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures,
@@ -1917,7 +1964,8 @@
long identityToken = clearCallingIdentity();
try {
new Session(accounts, response, accountType, expectActivityLaunch,
- true /* stripAuthTokenFromResult */) {
+ true /* stripAuthTokenFromResult */, null /* accountName */,
+ false /* authDetailsRequired */) {
@Override
public void run() throws RemoteException {
mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures,
@@ -1973,7 +2021,8 @@
long identityToken = clearCallingIdentity();
try {
new Session(accounts, response, account.type, expectActivityLaunch,
- true /* stripAuthTokenFromResult */) {
+ true /* stripAuthTokenFromResult */, account.name,
+ true /* authDetailsRequired */) {
@Override
public void run() throws RemoteException {
mAuthenticator.confirmCredentials(this, account, options);
@@ -2009,7 +2058,8 @@
long identityToken = clearCallingIdentity();
try {
new Session(accounts, response, account.type, expectActivityLaunch,
- true /* stripAuthTokenFromResult */) {
+ true /* stripAuthTokenFromResult */, account.name,
+ false /* authDetailsRequired */) {
@Override
public void run() throws RemoteException {
mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions);
@@ -2045,7 +2095,8 @@
long identityToken = clearCallingIdentity();
try {
new Session(accounts, response, accountType, expectActivityLaunch,
- true /* stripAuthTokenFromResult */) {
+ true /* stripAuthTokenFromResult */, null /* accountName */,
+ false /* authDetailsRequired */) {
@Override
public void run() throws RemoteException {
mAuthenticator.editProperties(this, mAccountType);
@@ -2071,7 +2122,8 @@
public GetAccountsByTypeAndFeatureSession(UserAccounts accounts,
IAccountManagerResponse response, String type, String[] features, int callingUid) {
super(accounts, response, type, false /* expectActivityLaunch */,
- true /* stripAuthTokenFromResult */);
+ true /* stripAuthTokenFromResult */, null /* accountName */,
+ false /* authDetailsRequired */);
mCallingUid = callingUid;
mFeatures = features;
}
@@ -2437,6 +2489,9 @@
final String mAccountType;
final boolean mExpectActivityLaunch;
final long mCreationTime;
+ final String mAccountName;
+ // Indicates if we need to add auth details(like last credential time)
+ final boolean mAuthDetailsRequired;
public int mNumResults = 0;
private int mNumRequestContinued = 0;
@@ -2448,7 +2503,8 @@
protected final UserAccounts mAccounts;
public Session(UserAccounts accounts, IAccountManagerResponse response, String accountType,
- boolean expectActivityLaunch, boolean stripAuthTokenFromResult) {
+ boolean expectActivityLaunch, boolean stripAuthTokenFromResult, String accountName,
+ boolean authDetailsRequired) {
super();
//if (response == null) throw new IllegalArgumentException("response is null");
if (accountType == null) throw new IllegalArgumentException("accountType is null");
@@ -2458,6 +2514,9 @@
mAccountType = accountType;
mExpectActivityLaunch = expectActivityLaunch;
mCreationTime = SystemClock.elapsedRealtime();
+ mAccountName = accountName;
+ mAuthDetailsRequired = authDetailsRequired;
+
synchronized (mSessions) {
mSessions.put(toString(), this);
}
@@ -2592,6 +2651,16 @@
public void onResult(Bundle result) {
mNumResults++;
Intent intent = null;
+ if (result != null && mAuthDetailsRequired) {
+ long lastAuthenticatedTime = DatabaseUtils.longForQuery(
+ mAccounts.openHelper.getReadableDatabase(),
+ "select " + ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS + " from " +
+ TABLE_ACCOUNTS + " WHERE " + ACCOUNTS_NAME + "=? AND "
+ + ACCOUNTS_TYPE + "=?",
+ new String[]{mAccountName, mAccountType});
+ result.putLong(AccountManager.KEY_LAST_AUTHENTICATE_TIME_MILLIS_EPOCH,
+ lastAuthenticatedTime);
+ }
if (result != null
&& (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
/*
@@ -2798,6 +2867,7 @@
+ ACCOUNTS_TYPE + " TEXT NOT NULL, "
+ ACCOUNTS_PASSWORD + " TEXT, "
+ ACCOUNTS_PREVIOUS_NAME + " TEXT, "
+ + ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS + " INTEGER DEFAULT 0, "
+ "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( "
@@ -2833,6 +2903,11 @@
+ "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
}
+ private void addLastSuccessfullAuthenticatedTimeColumn(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE_ACCOUNTS + " ADD COLUMN "
+ + ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS + " DEFAULT 0");
+ }
+
private void addOldAccountNameColumn(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE_ACCOUNTS + " ADD COLUMN " + ACCOUNTS_PREVIOUS_NAME);
}
@@ -2892,6 +2967,11 @@
oldVersion++;
}
+ if (oldVersion == 6) {
+ addLastSuccessfullAuthenticatedTimeColumn(db);
+ oldVersion++;
+ }
+
if (oldVersion != newVersion) {
Log.e(TAG, "failed to upgrade version " + oldVersion + " to version " + newVersion);
}