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