Chromium Code Reviews| Index: net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
| diff --git a/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java b/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
| index 917f174c50e8208af9144518460d0ec5a6015adc..6a27537d4f0360b91b084356965301154c101ddc 100644 |
| --- a/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
| +++ b/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
| @@ -4,16 +4,22 @@ |
| package org.chromium.net; |
| +import android.accounts.Account; |
| import android.accounts.AccountManager; |
| import android.accounts.AccountManagerCallback; |
| import android.accounts.AccountManagerFuture; |
| import android.accounts.AuthenticatorException; |
| import android.accounts.OperationCanceledException; |
| import android.app.Activity; |
| +import android.content.BroadcastReceiver; |
| +import android.content.Context; |
| +import android.content.Intent; |
| +import android.content.IntentFilter; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import org.chromium.base.ApplicationStatus; |
| +import org.chromium.base.Log; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.CalledByNative; |
| @@ -27,11 +33,116 @@ |
| */ |
| @JNINamespace("net::android") |
| public class HttpNegotiateAuthenticator { |
| + private static final String TAG = "net_auth"; |
| private Bundle mSpnegoContext = null; |
| private final String mAccountType; |
| - private AccountManagerFuture<Bundle> mFuture; |
| - private HttpNegotiateAuthenticator(String accountType) { |
| + /** |
| + * Structure designed to hold the data related to a specific request across the various |
| + * callbacks needed to complete it. |
| + */ |
| + static class RequestData { |
| + /** Native object to post the result to. */ |
| + public long nativeResultObject; |
| + |
| + /** Reference to the account manager to use for the various requests. */ |
| + public AccountManager accountManager; |
| + |
| + /** Authenticator-specific options for the request, used for AccountManager#getAuthToken. */ |
| + public Bundle options; |
| + |
| + /** Desired token type, used for AccountManager#getAuthToken. */ |
| + public String authTokenType; |
| + |
| + /** Account to fetch an auth token for. */ |
| + public Account account; |
| + } |
| + |
| + /** |
| + * Expects to receive a single account as result, and uses that account to request a token |
| + * from the {@link AccountManager} provided via the {@link RequestData} |
| + */ |
| + @VisibleForTesting |
| + class GetAccountsCallback implements AccountManagerCallback<Account[]> { |
| + private final RequestData mRequestData; |
| + public GetAccountsCallback(RequestData requestData) { |
| + mRequestData = requestData; |
| + } |
| + |
| + @Override |
| + public void run(AccountManagerFuture<Account[]> future) { |
| + Account[] accounts; |
| + try { |
| + accounts = future.getResult(); |
| + } catch (OperationCanceledException | AuthenticatorException | IOException e) { |
| + notifyFailure("Unable to retrieve the results for the getAccounts call.", e, |
| + mRequestData); |
| + return; |
| + } |
| + |
| + if (accounts.length != 1) { |
|
asanka
2015/11/03 17:45:51
Is there no way to select an account in this case?
dgn
2015/11/05 14:39:14
There isn't any. Showing any kind of UI requires h
|
| + // TODO(dgn) Maybe should fail with an error like ERR_INVALID_AUTH_CREDENTIALS |
| + // instead of UNEXPECTED? |
|
asanka
2015/11/03 17:45:51
Yeah. ERR_INVALID_AUTH_CREDENTIALS would be better
dgn
2015/11/05 14:39:14
Done.
|
| + notifyFailure("Unable to obtain a unique eligible account for negotiate auth.", |
| + null, mRequestData); |
| + return; |
| + } |
| + |
| + mRequestData.account = accounts[0]; |
| + mRequestData.accountManager.getAuthToken(mRequestData.account, |
| + mRequestData.authTokenType, mRequestData.options, true /* notifyAuthFailure */, |
| + new GetTokenCallback(mRequestData), |
| + new Handler(ThreadUtils.getUiThreadLooper())); |
| + } |
| + } |
| + |
| + @VisibleForTesting |
| + class GetTokenCallback implements AccountManagerCallback<Bundle> { |
| + private final RequestData mRequestData; |
| + public GetTokenCallback(RequestData requestData) { |
| + mRequestData = requestData; |
| + } |
| + |
| + @Override |
| + public void run(AccountManagerFuture<Bundle> future) { |
| + Bundle result; |
| + try { |
| + result = future.getResult(); |
| + } catch (OperationCanceledException | AuthenticatorException | IOException e) { |
| + notifyFailure("Operation did not complete", e, mRequestData); |
| + return; |
| + } |
| + |
| + if (result.containsKey(AccountManager.KEY_INTENT)) { |
| + final Context appContext = ApplicationStatus.getApplicationContext(); |
|
dgn
2015/10/30 12:38:56
Note: torne@ is adding a replacement for this, I'l
aberent
2015/10/30 16:38:10
nit: Maybe add a TODO if this is landing first.
dgn
2015/10/30 17:04:40
I'll wait. It's broken otherwise, even though the
|
| + |
| + // We wait for a broadcast that should be sent once the user is done interacting |
| + // with the notification |
| + // TODO(dgn) We currently hang around if the notification is swiped away, until |
| + // a LOGIN_ACCOUNTS_CHANGED_ACTION filter is received. It might be for something |
| + // unrelated then we would wait again here. Maybe we should limit the number of |
| + // retries in some way? |
| + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { |
| + |
| + @Override |
| + public void onReceive(Context context, Intent intent) { |
| + appContext.unregisterReceiver(this); |
| + mRequestData.accountManager.getAuthToken(mRequestData.account, |
| + mRequestData.authTokenType, mRequestData.options, |
| + true /* notifyAuthFailure */, new GetTokenCallback(mRequestData), |
| + null); |
| + } |
| + |
| + }; |
| + appContext.registerReceiver(broadcastReceiver, |
| + new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION)); |
| + } else { |
| + setResult(result, mRequestData); |
| + } |
| + } |
| + } |
| + |
| + protected HttpNegotiateAuthenticator(String accountType) { |
| assert !android.text.TextUtils.isEmpty(accountType); |
| mAccountType = accountType; |
| } |
| @@ -57,80 +168,89 @@ static HttpNegotiateAuthenticator create(String accountType) { |
| void getNextAuthToken(final long nativeResultObject, final String principal, String authToken, |
| boolean canDelegate) { |
| assert principal != null; |
| - String authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BASE + principal; |
| - Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| - if (activity == null) { |
| - nativeSetResult(nativeResultObject, NetError.ERR_UNEXPECTED, null); |
| - return; |
| - } |
| - AccountManager am = AccountManager.get(activity); |
| - String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; |
| - Bundle options = new Bundle(); |
| + RequestData requestData = new RequestData(); |
| + requestData.authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BASE + principal; |
| + requestData.accountManager = AccountManager.get(ApplicationStatus.getApplicationContext()); |
| + requestData.nativeResultObject = nativeResultObject; |
| + String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; |
| + requestData.options = new Bundle(); |
| if (authToken != null) { |
| - options.putString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, authToken); |
| + requestData.options.putString( |
| + HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, authToken); |
| } |
| if (mSpnegoContext != null) { |
| - options.putBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext); |
| + requestData.options.putBundle( |
| + HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext); |
| } |
| - options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate); |
| + requestData.options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate); |
| + Handler uiThreadHandler = new Handler(ThreadUtils.getUiThreadLooper()); |
| - mFuture = am.getAuthTokenByFeatures(mAccountType, authTokenType, features, activity, null, |
| - options, new AccountManagerCallback<Bundle>() { |
| + Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| + if (activity == null) { |
| + // This code path is not as able to get user input as the one that has an activity. It |
| + // doesn't handle 0 or multiple accounts and will just result in a failure in those |
| + // cases. |
| + requestData.accountManager.getAccountsByTypeAndFeatures( |
| + mAccountType, features, new GetAccountsCallback(requestData), uiThreadHandler); |
| + } else { |
| + // Behavior in Chrome |
| + // If there is more than one account, it will show the disambig for each query |
|
asanka
2015/11/03 17:45:51
expn disambig
dgn
2015/11/06 16:51:48
Done.
|
| + // (e.g. main page, then favicon ...) |
| + // Same if the credentials need to be confirmed/ |
| + // If there is a failure, it will retry automatically. |
| + requestData.accountManager.getAuthTokenByFeatures(mAccountType, |
| + requestData.authTokenType, features, activity, null, requestData.options, |
| + new GetTokenCallback(requestData), uiThreadHandler); |
| + } |
| + } |
| - @Override |
| - public void run(AccountManagerFuture<Bundle> future) { |
| - try { |
| - Bundle result = future.getResult(); |
| - mSpnegoContext = |
| - result.getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT); |
| - int status; |
| - switch (result.getInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, |
| - HttpNegotiateConstants.ERR_UNEXPECTED)) { |
| - case HttpNegotiateConstants.OK: |
| - status = 0; |
| - break; |
| - case HttpNegotiateConstants.ERR_UNEXPECTED: |
| - status = NetError.ERR_UNEXPECTED; |
| - break; |
| - case HttpNegotiateConstants.ERR_ABORTED: |
| - status = NetError.ERR_ABORTED; |
| - break; |
| - case HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS: |
| - status = NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS; |
| - break; |
| - case HttpNegotiateConstants.ERR_INVALID_RESPONSE: |
| - status = NetError.ERR_INVALID_RESPONSE; |
| - break; |
| - case HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS: |
| - status = NetError.ERR_INVALID_AUTH_CREDENTIALS; |
| - break; |
| - case HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME: |
| - status = NetError.ERR_UNSUPPORTED_AUTH_SCHEME; |
| - break; |
| - case HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS: |
| - status = NetError.ERR_MISSING_AUTH_CREDENTIALS; |
| - break; |
| - case HttpNegotiateConstants |
| - .ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS: |
| - status = NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS; |
| - break; |
| - case HttpNegotiateConstants.ERR_MALFORMED_IDENTITY: |
| - status = NetError.ERR_MALFORMED_IDENTITY; |
| - break; |
| - default: |
| - status = NetError.ERR_UNEXPECTED; |
| - } |
| - nativeSetResult(nativeResultObject, status, |
| - result.getString(AccountManager.KEY_AUTHTOKEN)); |
| - } catch (OperationCanceledException | AuthenticatorException |
| - | IOException e) { |
| - nativeSetResult(nativeResultObject, NetError.ERR_ABORTED, null); |
| - } |
| - } |
| + private void notifyFailure(String message, Exception e, RequestData requestData) { |
| + Log.w(TAG, message, e); |
| + nativeSetResult(requestData.nativeResultObject, NetError.ERR_UNEXPECTED, null); |
| + } |
| - }, new Handler(ThreadUtils.getUiThreadLooper())); |
| + private void setResult(Bundle result, RequestData requestData) { |
| + mSpnegoContext = result.getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT); |
| + int status; |
| + switch (result.getInt( |
| + HttpNegotiateConstants.KEY_SPNEGO_RESULT, HttpNegotiateConstants.ERR_UNEXPECTED)) { |
| + case HttpNegotiateConstants.OK: |
|
asanka
2015/11/03 17:45:51
How permanent are the error codes in HttpNegotiate
dgn
2015/11/05 14:39:14
They are supposed to not change as 3p devs will us
asanka
2015/11/05 17:41:32
To give some background, the net::Error values wer
dgn
2015/11/06 16:51:48
Moved this discussion to http://crbug.com/552414.
|
| + status = 0; |
| + break; |
| + case HttpNegotiateConstants.ERR_UNEXPECTED: |
| + status = NetError.ERR_UNEXPECTED; |
| + break; |
| + case HttpNegotiateConstants.ERR_ABORTED: |
| + status = NetError.ERR_ABORTED; |
| + break; |
| + case HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS: |
| + status = NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS; |
| + break; |
| + case HttpNegotiateConstants.ERR_INVALID_RESPONSE: |
| + status = NetError.ERR_INVALID_RESPONSE; |
| + break; |
| + case HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS: |
| + status = NetError.ERR_INVALID_AUTH_CREDENTIALS; |
| + break; |
| + case HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME: |
| + status = NetError.ERR_UNSUPPORTED_AUTH_SCHEME; |
| + break; |
| + case HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS: |
| + status = NetError.ERR_MISSING_AUTH_CREDENTIALS; |
| + break; |
| + case HttpNegotiateConstants.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS: |
| + status = NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS; |
| + break; |
| + case HttpNegotiateConstants.ERR_MALFORMED_IDENTITY: |
| + status = NetError.ERR_MALFORMED_IDENTITY; |
| + break; |
| + default: |
| + status = NetError.ERR_UNEXPECTED; |
| + } |
| + nativeSetResult(requestData.nativeResultObject, status, |
| + result.getString(AccountManager.KEY_AUTHTOKEN)); |
| } |
| @VisibleForTesting |