| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net; |
| |
| import android.Manifest; |
| 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.content.pm.PackageManager; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Process; |
| |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.JNINamespace; |
| |
| import java.io.IOException; |
| |
| /** |
| * Class to get Auth Tokens for HTTP Negotiate authentication (typically used for Kerberos) An |
| * instance of this class is created for each separate negotiation. |
| * |
| * Please keep the documentation from the chromium.org page (https://goo.gl/46hmKx) in sync. |
| * ================================================================================================ |
| * In addition to the error codes that can be forwarded from the authenticator app, the following |
| * errors can be displayed when trying to authenticate a request: |
| * |
| * - ERR_UNEXPECTED: An unexpected error happened and the request has been terminated. |
| * |
| * - ERR_MISSING_AUTH_CREDENTIALS: The account information is not usable. It can be raised for |
| * example if the user did not log in to the authenticator app and no eligible account is found, |
| * if the account information can't be obtained because the current app does not have the |
| * required permissions, or if there is more than one eligible account and we can't obtain a |
| * selection from the user. |
| * |
| * - ERR_MISCONFIGURED_AUTH_ENVIRONMENT: The authentication can't be completed because of some |
| * issues in the configuration of the app. Some permissions may be missing. |
| * |
| * Please search for the "cr_net_auth" tag in logcat to have more information about the cause of |
| * these errors. |
| * ================================================================================================ |
| */ |
| @JNINamespace("net::android") |
| public class HttpNegotiateAuthenticator { |
| private static final String TAG = "net_auth"; |
| private Bundle mSpnegoContext = null; |
| private final String mAccountType; |
| |
| /** |
| * 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) { |
| Log.w(TAG, "ERR_UNEXPECTED: Error while attempting to retrieve accounts.", e); |
| nativeSetResult(mRequestData.nativeResultObject, NetError.ERR_UNEXPECTED, null); |
| return; |
| } |
| |
| if (accounts.length == 0) { |
| Log.w(TAG, "ERR_MISSING_AUTH_CREDENTIALS: No account provided for the kerberos " |
| + "authentication. Please verify the configuration policies and " |
| + "that the CONTACTS runtime permission is granted. "); |
| nativeSetResult(mRequestData.nativeResultObject, |
| NetError.ERR_MISSING_AUTH_CREDENTIALS, null); |
| return; |
| } |
| |
| if (accounts.length > 1) { |
| Log.w(TAG, "ERR_MISSING_AUTH_CREDENTIALS: Found %d accounts eligible for the " |
| + "kerberos authentication. Please fix the configuration by " |
| + "providing a single account.", |
| accounts.length); |
| nativeSetResult(mRequestData.nativeResultObject, |
| NetError.ERR_MISSING_AUTH_CREDENTIALS, null); |
| return; |
| } |
| |
| if (lacksPermission(ContextUtils.getApplicationContext(), |
| "android.permission.USE_CREDENTIALS", true)) { |
| // Protecting the AccountManager#getAuthToken call. |
| // API < 23 Requires the USE_CREDENTIALS permission or throws an exception. |
| // API >= 23 USE_CREDENTIALS permission is removed |
| Log.e(TAG, "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: USE_CREDENTIALS permission not " |
| + "granted. Aborting authentication."); |
| nativeSetResult(mRequestData.nativeResultObject, |
| NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
| 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) { |
| Log.w(TAG, "ERR_UNEXPECTED: Error while attempting to obtain a token.", e); |
| nativeSetResult(mRequestData.nativeResultObject, NetError.ERR_UNEXPECTED, null); |
| return; |
| } |
| |
| if (result.containsKey(AccountManager.KEY_INTENT)) { |
| final Context appContext = ContextUtils.getApplicationContext(); |
| |
| // 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 { |
| processResult(result, mRequestData); |
| } |
| } |
| } |
| |
| protected HttpNegotiateAuthenticator(String accountType) { |
| assert !android.text.TextUtils.isEmpty(accountType); |
| mAccountType = accountType; |
| } |
| |
| /** |
| * @param accountType The Android account type to use. |
| */ |
| @VisibleForTesting |
| @CalledByNative |
| static HttpNegotiateAuthenticator create(String accountType) { |
| return new HttpNegotiateAuthenticator(accountType); |
| } |
| |
| /** |
| * @param nativeResultObject The C++ object used to return the result. For correct C++ memory |
| * management we must call nativeSetResult precisely once with this object. |
| * @param principal The principal (must be host based). |
| * @param authToken The incoming auth token. |
| * @param canDelegate True if we can delegate. |
| */ |
| @VisibleForTesting |
| @CalledByNative |
| void getNextAuthToken(final long nativeResultObject, final String principal, String authToken, |
| boolean canDelegate) { |
| assert principal != null; |
| |
| Context applicationContext = ContextUtils.getApplicationContext(); |
| RequestData requestData = new RequestData(); |
| requestData.authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BASE + principal; |
| requestData.accountManager = AccountManager.get(applicationContext); |
| requestData.nativeResultObject = nativeResultObject; |
| String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; |
| |
| requestData.options = new Bundle(); |
| if (authToken != null) { |
| requestData.options.putString( |
| HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, authToken); |
| } |
| if (mSpnegoContext != null) { |
| requestData.options.putBundle( |
| HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext); |
| } |
| requestData.options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate); |
| |
| Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| if (activity == null) { |
| requestTokenWithoutActivity(applicationContext, requestData, features); |
| } else { |
| requestTokenWithActivity(applicationContext, activity, requestData, features); |
| } |
| } |
| |
| /** |
| * Process a result bundle from a completed token request, communicating its content back to |
| * the native code. |
| */ |
| private void processResult(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: |
| 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)); |
| } |
| |
| /** |
| * Requests an authentication token. If the account is not properly setup, it will result in |
| * a failure. |
| * |
| * @param ctx The application context |
| * @param requestData The object holding the data related to the current request |
| * @param features An array of the account features to require, may be null or empty |
| */ |
| private void requestTokenWithoutActivity( |
| Context ctx, RequestData requestData, String[] features) { |
| if (lacksPermission(ctx, Manifest.permission.GET_ACCOUNTS, true /* onlyPreM */)) { |
| // Protecting the AccountManager#getAccountsByTypeAndFeatures call. |
| // API < 23 Requires the GET_ACCOUNTS permission or throws an exception. |
| // API >= 23 Requires the GET_ACCOUNTS permission (CONTACTS permission group) or |
| // returns only the accounts whose authenticator has a signature that |
| // matches our app. Working with this restriction and not requesting |
| // the permission is a valid use case in the context of WebView, so we |
| // don't require it on M+ |
| Log.e(TAG, "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: GET_ACCOUNTS permission not " |
| + "granted. Aborting authentication."); |
| nativeSetResult(requestData.nativeResultObject, |
| NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
| return; |
| } |
| requestData.accountManager.getAccountsByTypeAndFeatures(mAccountType, features, |
| new GetAccountsCallback(requestData), new Handler(ThreadUtils.getUiThreadLooper())); |
| } |
| |
| /** |
| * Requests an authentication token. Handles the account selection/creation and the credentials |
| * confirmation if that is needed. |
| * If there is more than one account, it will show an account picker dialog for |
| * each query (e.g. html file, then favicon...) |
| * Same if the credentials need to be confirmed. |
| * |
| * @param ctx The application context |
| * @param activity The Activity context to use for launching new sub-Activities to prompt to |
| * add an account, select an account, and/or enter a password, as necessary; |
| * used only to call startActivity(); should not be null |
| * @param requestData The object holding the data related to the current request |
| * @param features An array of the account features to require, may be null or empty |
| */ |
| private void requestTokenWithActivity( |
| Context ctx, Activity activity, RequestData requestData, String[] features) { |
| boolean isPreM = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; |
| String permission = isPreM |
| ? "android.permission.MANAGE_ACCOUNTS" |
| : Manifest.permission.GET_ACCOUNTS; |
| |
| // Check if the AccountManager#getAuthTokenByFeatures call can be made. |
| // API < 23 Requires the MANAGE_ACCOUNTS permission. |
| // API >= 23 Requires the GET_ACCOUNTS permission to behave properly. When it's not granted, |
| // accounts not managed by the current application can't be retrieved. Depending |
| // on the authenticator implementation, it might prompt to create an account, but |
| // that won't be saved. This would be a bad user experience, so we also consider |
| // it a failure case. |
| if (lacksPermission(ctx, permission, isPreM)) { |
| Log.e(TAG, "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: %s permission not granted. " |
| + "Aborting authentication", permission); |
| nativeSetResult(requestData.nativeResultObject, |
| NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
| return; |
| } |
| |
| requestData.accountManager.getAuthTokenByFeatures(mAccountType, requestData.authTokenType, |
| features, activity, null, requestData.options, new GetTokenCallback(requestData), |
| new Handler(ThreadUtils.getUiThreadLooper())); |
| } |
| |
| /** |
| * Returns whether the current context lacks a given permission. Skips the check on M+ systems |
| * if {@code onlyPreM} is {@code true}, and just returns {@code false}. |
| */ |
| @VisibleForTesting |
| boolean lacksPermission(Context context, String permission, boolean onlyPreM) { |
| if (onlyPreM && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) return false; |
| |
| int permissionResult = |
| context.checkPermission(permission, Process.myPid(), Process.myUid()); |
| return permissionResult != PackageManager.PERMISSION_GRANTED; |
| } |
| |
| @VisibleForTesting |
| native void nativeSetResult( |
| long nativeJavaNegotiateResultWrapper, int status, String authToken); |
| } |