• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2015 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net;
6 
7 import android.Manifest;
8 import android.accounts.Account;
9 import android.accounts.AccountManager;
10 import android.accounts.AccountManagerCallback;
11 import android.accounts.AccountManagerFuture;
12 import android.accounts.AuthenticatorException;
13 import android.accounts.OperationCanceledException;
14 import android.app.Activity;
15 import android.content.BroadcastReceiver;
16 import android.content.Context;
17 import android.content.Intent;
18 import android.content.IntentFilter;
19 import android.content.pm.PackageManager;
20 import android.os.Build;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.os.Process;
24 
25 import androidx.annotation.VisibleForTesting;
26 
27 import org.jni_zero.CalledByNative;
28 import org.jni_zero.JNINamespace;
29 import org.jni_zero.NativeMethods;
30 
31 import org.chromium.base.ApplicationStatus;
32 import org.chromium.base.ContextUtils;
33 import org.chromium.base.Log;
34 import org.chromium.base.ThreadUtils;
35 
36 import java.io.IOException;
37 
38 /**
39  * Class to get Auth Tokens for HTTP Negotiate authentication (typically used for Kerberos) An
40  * instance of this class is created for each separate negotiation.
41  *
42  * Please keep the documentation from the chromium.org page (https://goo.gl/46hmKx) in sync.
43  * ================================================================================================
44  * In addition to the error codes that can be forwarded from the authenticator app, the following
45  * errors can be displayed when trying to authenticate a request:
46  *
47  *  - ERR_UNEXPECTED: An unexpected error happened and the request has been terminated.
48  *
49  *  - ERR_MISSING_AUTH_CREDENTIALS: The account information is not usable. It can be raised for
50  *    example if the user did not log in to the authenticator app and no eligible account is found,
51  *    if the account information can't be obtained because the current app does not have the
52  *    required permissions, or if there is more than one eligible account and we can't obtain a
53  *    selection from the user.
54  *
55  *  - ERR_MISCONFIGURED_AUTH_ENVIRONMENT: The authentication can't be completed because of some
56  *    issues in the configuration of the app. Some permissions may be missing.
57  *
58  * Please search for the "cr_net_auth" tag in logcat to have more information about the cause of
59  * these errors.
60  * ================================================================================================
61  */
62 @JNINamespace("net::android")
63 public class HttpNegotiateAuthenticator {
64     private static final String TAG = "net_auth";
65     private Bundle mSpnegoContext;
66     private final String mAccountType;
67 
68     /**
69      * Structure designed to hold the data related to a specific request across the various
70      * callbacks needed to complete it.
71      */
72     static class RequestData {
73         /** Native object to post the result to. */
74         public long nativeResultObject;
75 
76         /** Reference to the account manager to use for the various requests. */
77         public AccountManager accountManager;
78 
79         /** Authenticator-specific options for the request, used for AccountManager#getAuthToken. */
80         public Bundle options;
81 
82         /** Desired token type, used for AccountManager#getAuthToken. */
83         public String authTokenType;
84 
85         /** Account to fetch an auth token for. */
86         public Account account;
87     }
88 
89     /**
90      * Expects to receive a single account as result, and uses that account to request a token
91      * from the {@link AccountManager} provided via the {@link RequestData}
92      */
93     @VisibleForTesting
94     class GetAccountsCallback implements AccountManagerCallback<Account[]> {
95         private final RequestData mRequestData;
96 
GetAccountsCallback(RequestData requestData)97         public GetAccountsCallback(RequestData requestData) {
98             mRequestData = requestData;
99         }
100 
101         @Override
run(AccountManagerFuture<Account[]> future)102         public void run(AccountManagerFuture<Account[]> future) {
103             Account[] accounts;
104             try {
105                 accounts = future.getResult();
106             } catch (OperationCanceledException | AuthenticatorException | IOException e) {
107                 Log.w(TAG, "ERR_UNEXPECTED: Error while attempting to retrieve accounts.", e);
108                 HttpNegotiateAuthenticatorJni.get()
109                         .setResult(
110                                 mRequestData.nativeResultObject,
111                                 HttpNegotiateAuthenticator.this,
112                                 NetError.ERR_UNEXPECTED,
113                                 null);
114                 return;
115             }
116 
117             if (accounts.length == 0) {
118                 Log.w(
119                         TAG,
120                         "ERR_MISSING_AUTH_CREDENTIALS: No account provided for the kerberos "
121                                 + "authentication. Please verify the configuration policies and "
122                                 + "that the CONTACTS runtime permission is granted. ");
123                 HttpNegotiateAuthenticatorJni.get()
124                         .setResult(
125                                 mRequestData.nativeResultObject,
126                                 HttpNegotiateAuthenticator.this,
127                                 NetError.ERR_MISSING_AUTH_CREDENTIALS,
128                                 null);
129                 return;
130             }
131 
132             if (accounts.length > 1) {
133                 Log.w(
134                         TAG,
135                         "ERR_MISSING_AUTH_CREDENTIALS: Found %d accounts eligible for the "
136                                 + "kerberos authentication. Please fix the configuration by "
137                                 + "providing a single account.",
138                         accounts.length);
139                 HttpNegotiateAuthenticatorJni.get()
140                         .setResult(
141                                 mRequestData.nativeResultObject,
142                                 HttpNegotiateAuthenticator.this,
143                                 NetError.ERR_MISSING_AUTH_CREDENTIALS,
144                                 null);
145                 return;
146             }
147 
148             if (lacksPermission(
149                     ContextUtils.getApplicationContext(),
150                     "android.permission.USE_CREDENTIALS",
151                     true)) {
152                 // Protecting the AccountManager#getAuthToken call.
153                 // API  < 23 Requires the USE_CREDENTIALS permission or throws an exception.
154                 // API >= 23 USE_CREDENTIALS permission is removed
155                 Log.e(
156                         TAG,
157                         "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: USE_CREDENTIALS permission not "
158                                 + "granted. Aborting authentication.");
159                 HttpNegotiateAuthenticatorJni.get()
160                         .setResult(
161                                 mRequestData.nativeResultObject,
162                                 HttpNegotiateAuthenticator.this,
163                                 NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT,
164                                 null);
165                 return;
166             }
167             mRequestData.account = accounts[0];
168             mRequestData.accountManager.getAuthToken(
169                     mRequestData.account,
170                     mRequestData.authTokenType,
171                     mRequestData.options,
172                     /* notifyAuthFailure= */ true,
173                     new GetTokenCallback(mRequestData),
174                     new Handler(ThreadUtils.getUiThreadLooper()));
175         }
176     }
177 
178     @VisibleForTesting
179     class GetTokenCallback implements AccountManagerCallback<Bundle> {
180         private final RequestData mRequestData;
181 
GetTokenCallback(RequestData requestData)182         public GetTokenCallback(RequestData requestData) {
183             mRequestData = requestData;
184         }
185 
186         @Override
run(AccountManagerFuture<Bundle> future)187         public void run(AccountManagerFuture<Bundle> future) {
188             Bundle result;
189             try {
190                 result = future.getResult();
191             } catch (OperationCanceledException | AuthenticatorException | IOException e) {
192                 Log.w(TAG, "ERR_UNEXPECTED: Error while attempting to obtain a token.", e);
193                 HttpNegotiateAuthenticatorJni.get()
194                         .setResult(
195                                 mRequestData.nativeResultObject,
196                                 HttpNegotiateAuthenticator.this,
197                                 NetError.ERR_UNEXPECTED,
198                                 null);
199                 return;
200             }
201 
202             if (result.containsKey(AccountManager.KEY_INTENT)) {
203                 final Context appContext = ContextUtils.getApplicationContext();
204 
205                 // We wait for a broadcast that should be sent once the user is done interacting
206                 // with the notification
207                 // TODO(dgn) We currently hang around if the notification is swiped away, until
208                 // a LOGIN_ACCOUNTS_CHANGED_ACTION filter is received. It might be for something
209                 // unrelated then we would wait again here. Maybe we should limit the number of
210                 // retries in some way?
211                 BroadcastReceiver broadcastReceiver =
212                         new BroadcastReceiver() {
213 
214                             @Override
215                             public void onReceive(Context context, Intent intent) {
216                                 appContext.unregisterReceiver(this);
217                                 mRequestData.accountManager.getAuthToken(
218                                         mRequestData.account,
219                                         mRequestData.authTokenType,
220                                         mRequestData.options,
221                                         /* notifyAuthFailure= */ true,
222                                         new GetTokenCallback(mRequestData),
223                                         null);
224                             }
225                         };
226                 ContextUtils.registerProtectedBroadcastReceiver(
227                         appContext,
228                         broadcastReceiver,
229                         new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION));
230             } else {
231                 processResult(result, mRequestData);
232             }
233         }
234     }
235 
HttpNegotiateAuthenticator(String accountType)236     protected HttpNegotiateAuthenticator(String accountType) {
237         assert !android.text.TextUtils.isEmpty(accountType);
238         mAccountType = accountType;
239     }
240 
241     /**
242      * @param accountType The Android account type to use.
243      */
244     @VisibleForTesting
245     @CalledByNative
create(String accountType)246     static HttpNegotiateAuthenticator create(String accountType) {
247         return new HttpNegotiateAuthenticator(accountType);
248     }
249 
250     /**
251      * @param nativeResultObject The C++ object used to return the result. For correct C++ memory
252      *            management we must call HttpNegotiateAuthenticatorJni.get().setResult precisely
253      * once with this object.
254      * @param principal The principal (must be host based).
255      * @param authToken The incoming auth token.
256      * @param canDelegate True if we can delegate.
257      */
258     @VisibleForTesting
259     @CalledByNative
getNextAuthToken( final long nativeResultObject, final String principal, String authToken, boolean canDelegate)260     void getNextAuthToken(
261             final long nativeResultObject,
262             final String principal,
263             String authToken,
264             boolean canDelegate) {
265         assert principal != null;
266 
267         Context applicationContext = ContextUtils.getApplicationContext();
268         RequestData requestData = new RequestData();
269         requestData.authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BASE + principal;
270         requestData.accountManager = AccountManager.get(applicationContext);
271         requestData.nativeResultObject = nativeResultObject;
272         String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE};
273 
274         requestData.options = new Bundle();
275         if (authToken != null) {
276             requestData.options.putString(
277                     HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, authToken);
278         }
279         if (mSpnegoContext != null) {
280             requestData.options.putBundle(
281                     HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext);
282         }
283         requestData.options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate);
284 
285         Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
286         if (activity == null) {
287             requestTokenWithoutActivity(applicationContext, requestData, features);
288         } else {
289             requestTokenWithActivity(applicationContext, activity, requestData, features);
290         }
291     }
292 
293     /**
294      * Process a result bundle from a completed token request, communicating its content back to
295      * the native code.
296      */
processResult(Bundle result, RequestData requestData)297     private void processResult(Bundle result, RequestData requestData) {
298         mSpnegoContext = result.getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT);
299         @NetError int status;
300         switch (result.getInt(
301                 HttpNegotiateConstants.KEY_SPNEGO_RESULT, HttpNegotiateConstants.ERR_UNEXPECTED)) {
302             case HttpNegotiateConstants.OK:
303                 status = NetError.OK;
304                 break;
305             case HttpNegotiateConstants.ERR_UNEXPECTED:
306                 status = NetError.ERR_UNEXPECTED;
307                 break;
308             case HttpNegotiateConstants.ERR_ABORTED:
309                 status = NetError.ERR_ABORTED;
310                 break;
311             case HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS:
312                 status = NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS;
313                 break;
314             case HttpNegotiateConstants.ERR_INVALID_RESPONSE:
315                 status = NetError.ERR_INVALID_RESPONSE;
316                 break;
317             case HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS:
318                 status = NetError.ERR_INVALID_AUTH_CREDENTIALS;
319                 break;
320             case HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME:
321                 status = NetError.ERR_UNSUPPORTED_AUTH_SCHEME;
322                 break;
323             case HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS:
324                 status = NetError.ERR_MISSING_AUTH_CREDENTIALS;
325                 break;
326             case HttpNegotiateConstants.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS:
327                 status = NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS;
328                 break;
329             case HttpNegotiateConstants.ERR_MALFORMED_IDENTITY:
330                 status = NetError.ERR_MALFORMED_IDENTITY;
331                 break;
332             default:
333                 status = NetError.ERR_UNEXPECTED;
334         }
335         HttpNegotiateAuthenticatorJni.get()
336                 .setResult(
337                         requestData.nativeResultObject,
338                         HttpNegotiateAuthenticator.this,
339                         status,
340                         result.getString(AccountManager.KEY_AUTHTOKEN));
341     }
342 
343     /**
344      * Requests an authentication token. If the account is not properly setup, it will result in
345      * a failure.
346      *
347      * @param ctx The application context
348      * @param requestData The object holding the data related to the current request
349      * @param features An array of the account features to require, may be null or empty
350      */
requestTokenWithoutActivity( Context ctx, RequestData requestData, String[] features)351     private void requestTokenWithoutActivity(
352             Context ctx, RequestData requestData, String[] features) {
353         if (lacksPermission(ctx, Manifest.permission.GET_ACCOUNTS, /* onlyPreM= */ true)) {
354             // Protecting the AccountManager#getAccountsByTypeAndFeatures call.
355             // API  < 23 Requires the GET_ACCOUNTS permission or throws an exception.
356             // API >= 23 Requires the GET_ACCOUNTS permission (CONTACTS permission group) or
357             //           returns only the accounts whose authenticator has a signature that
358             //           matches our app. Working with this restriction and not requesting
359             //           the permission is a valid use case in the context of WebView, so we
360             //           don't require it on M+
361             Log.e(
362                     TAG,
363                     "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: GET_ACCOUNTS permission not "
364                             + "granted. Aborting authentication.");
365             HttpNegotiateAuthenticatorJni.get()
366                     .setResult(
367                             requestData.nativeResultObject,
368                             HttpNegotiateAuthenticator.this,
369                             NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT,
370                             null);
371             return;
372         }
373         requestData.accountManager.getAccountsByTypeAndFeatures(
374                 mAccountType,
375                 features,
376                 new GetAccountsCallback(requestData),
377                 new Handler(ThreadUtils.getUiThreadLooper()));
378     }
379 
380     /**
381      * Requests an authentication token. Handles the account selection/creation and the credentials
382      * confirmation if that is needed.
383      * If there is more than one account, it will show an account picker dialog for
384      * each query (e.g. html file, then favicon...)
385      * Same if the credentials need to be confirmed.
386      *
387      * @param ctx The application context
388      * @param activity The Activity context to use for launching new sub-Activities to prompt to
389      *                 add an account, select an account, and/or enter a password, as necessary;
390      *                 used only to call startActivity(); should not be null
391      * @param requestData The object holding the data related to the current request
392      * @param features An array of the account features to require, may be null or empty
393      */
requestTokenWithActivity( Context ctx, Activity activity, RequestData requestData, String[] features)394     private void requestTokenWithActivity(
395             Context ctx, Activity activity, RequestData requestData, String[] features) {
396         boolean isPreM = Build.VERSION.SDK_INT < Build.VERSION_CODES.M;
397         String permission =
398                 isPreM ? "android.permission.MANAGE_ACCOUNTS" : Manifest.permission.GET_ACCOUNTS;
399 
400         // Check if the AccountManager#getAuthTokenByFeatures call can be made.
401         // API  < 23 Requires the MANAGE_ACCOUNTS permission.
402         // API >= 23 Requires the GET_ACCOUNTS permission to behave properly. When it's not granted,
403         //           accounts not managed by the current application can't be retrieved. Depending
404         //           on the authenticator implementation, it might prompt to create an account, but
405         //           that won't be saved. This would be a bad user experience, so we also consider
406         //           it a failure case.
407         if (lacksPermission(ctx, permission, isPreM)) {
408             Log.e(
409                     TAG,
410                     "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: %s permission not granted. "
411                             + "Aborting authentication",
412                     permission);
413             HttpNegotiateAuthenticatorJni.get()
414                     .setResult(
415                             requestData.nativeResultObject,
416                             HttpNegotiateAuthenticator.this,
417                             NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT,
418                             null);
419             return;
420         }
421 
422         requestData.accountManager.getAuthTokenByFeatures(
423                 mAccountType,
424                 requestData.authTokenType,
425                 features,
426                 activity,
427                 null,
428                 requestData.options,
429                 new GetTokenCallback(requestData),
430                 new Handler(ThreadUtils.getUiThreadLooper()));
431     }
432 
433     /**
434      * Returns whether the current context lacks a given permission. Skips the check on M+ systems
435      * if {@code onlyPreM} is {@code true}, and just returns {@code false}.
436      */
437     @VisibleForTesting
438     boolean lacksPermission(Context context, String permission, boolean onlyPreM) {
439         if (onlyPreM && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) return false;
440 
441         int permissionResult =
442                 context.checkPermission(permission, Process.myPid(), Process.myUid());
443         return permissionResult != PackageManager.PERMISSION_GRANTED;
444     }
445 
446     @NativeMethods
447     interface Natives {
setResult( long nativeJavaNegotiateResultWrapper, HttpNegotiateAuthenticator caller, int status, String authToken)448         void setResult(
449                 long nativeJavaNegotiateResultWrapper,
450                 HttpNegotiateAuthenticator caller,
451                 int status,
452                 String authToken);
453     }
454 }
455