OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.net; | 5 package org.chromium.net; |
6 | 6 |
| 7 import android.accounts.Account; |
7 import android.accounts.AccountManager; | 8 import android.accounts.AccountManager; |
8 import android.accounts.AccountManagerCallback; | 9 import android.accounts.AccountManagerCallback; |
9 import android.accounts.AccountManagerFuture; | 10 import android.accounts.AccountManagerFuture; |
10 import android.accounts.AuthenticatorException; | 11 import android.accounts.AuthenticatorException; |
11 import android.accounts.OperationCanceledException; | 12 import android.accounts.OperationCanceledException; |
12 import android.app.Activity; | 13 import android.app.Activity; |
| 14 import android.content.BroadcastReceiver; |
| 15 import android.content.Context; |
| 16 import android.content.Intent; |
| 17 import android.content.IntentFilter; |
| 18 import android.content.pm.PackageManager; |
13 import android.os.Bundle; | 19 import android.os.Bundle; |
14 import android.os.Handler; | 20 import android.os.Handler; |
| 21 import android.os.Process; |
15 | 22 |
16 import org.chromium.base.ApplicationStatus; | 23 import org.chromium.base.ApplicationStatus; |
| 24 import org.chromium.base.ContextUtils; |
| 25 import org.chromium.base.Log; |
17 import org.chromium.base.ThreadUtils; | 26 import org.chromium.base.ThreadUtils; |
18 import org.chromium.base.VisibleForTesting; | 27 import org.chromium.base.VisibleForTesting; |
19 import org.chromium.base.annotations.CalledByNative; | 28 import org.chromium.base.annotations.CalledByNative; |
20 import org.chromium.base.annotations.JNINamespace; | 29 import org.chromium.base.annotations.JNINamespace; |
21 | 30 |
22 import java.io.IOException; | 31 import java.io.IOException; |
23 | 32 |
24 /** | 33 /** |
25 * Class to get Auth Tokens for HTTP Negotiate authentication (typically used fo
r Kerberos) An | 34 * Class to get Auth Tokens for HTTP Negotiate authentication (typically used fo
r Kerberos) An |
26 * instance of this class is created for each separate negotiation. | 35 * instance of this class is created for each separate negotiation. |
27 */ | 36 */ |
28 @JNINamespace("net::android") | 37 @JNINamespace("net::android") |
29 public class HttpNegotiateAuthenticator { | 38 public class HttpNegotiateAuthenticator { |
| 39 private static final String TAG = "net_auth"; |
30 private Bundle mSpnegoContext = null; | 40 private Bundle mSpnegoContext = null; |
31 private final String mAccountType; | 41 private final String mAccountType; |
32 private AccountManagerFuture<Bundle> mFuture; | |
33 | 42 |
34 private HttpNegotiateAuthenticator(String accountType) { | 43 /** |
| 44 * Structure designed to hold the data related to a specific request across
the various |
| 45 * callbacks needed to complete it. |
| 46 */ |
| 47 static class RequestData { |
| 48 /** Native object to post the result to. */ |
| 49 public long nativeResultObject; |
| 50 |
| 51 /** Reference to the account manager to use for the various requests. */ |
| 52 public AccountManager accountManager; |
| 53 |
| 54 /** Authenticator-specific options for the request, used for AccountMana
ger#getAuthToken. */ |
| 55 public Bundle options; |
| 56 |
| 57 /** Desired token type, used for AccountManager#getAuthToken. */ |
| 58 public String authTokenType; |
| 59 |
| 60 /** Account to fetch an auth token for. */ |
| 61 public Account account; |
| 62 } |
| 63 |
| 64 /** |
| 65 * Expects to receive a single account as result, and uses that account to r
equest a token |
| 66 * from the {@link AccountManager} provided via the {@link RequestData} |
| 67 */ |
| 68 @VisibleForTesting |
| 69 class GetAccountsCallback implements AccountManagerCallback<Account[]> { |
| 70 private final RequestData mRequestData; |
| 71 public GetAccountsCallback(RequestData requestData) { |
| 72 mRequestData = requestData; |
| 73 } |
| 74 |
| 75 @Override |
| 76 public void run(AccountManagerFuture<Account[]> future) { |
| 77 Account[] accounts; |
| 78 try { |
| 79 accounts = future.getResult(); |
| 80 } catch (OperationCanceledException | AuthenticatorException | IOExc
eption e) { |
| 81 Log.w(TAG, "Unable to retrieve the results for the getAccounts c
all.", e); |
| 82 nativeSetResult(mRequestData.nativeResultObject, NetError.ERR_UN
EXPECTED, null); |
| 83 return; |
| 84 } |
| 85 |
| 86 if (accounts.length != 1) { |
| 87 Log.w(TAG, "Unable to obtain a unique eligible account for negot
iate auth."); |
| 88 nativeSetResult(mRequestData.nativeResultObject, |
| 89 NetError.ERR_INVALID_AUTH_CREDENTIALS, null); |
| 90 return; |
| 91 } |
| 92 if (!hasPermission(ContextUtils.getApplicationContext(), |
| 93 "android.permission.USE_CREDENTIALS")) { |
| 94 // API < 23 Requires the USE_CREDENTIALS permission or throws a
n exception. |
| 95 // API >= 23 USE_CREDENTIALS is auto-granted without having to b
e declared. |
| 96 Log.e(TAG, "USE_CREDENTIALS permission not granted. Aborting aut
hentication."); |
| 97 nativeSetResult(mRequestData.nativeResultObject, |
| 98 NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
| 99 return; |
| 100 } |
| 101 mRequestData.account = accounts[0]; |
| 102 mRequestData.accountManager.getAuthToken(mRequestData.account, |
| 103 mRequestData.authTokenType, mRequestData.options, true /* no
tifyAuthFailure */, |
| 104 new GetTokenCallback(mRequestData), |
| 105 new Handler(ThreadUtils.getUiThreadLooper())); |
| 106 } |
| 107 } |
| 108 |
| 109 @VisibleForTesting |
| 110 class GetTokenCallback implements AccountManagerCallback<Bundle> { |
| 111 private final RequestData mRequestData; |
| 112 public GetTokenCallback(RequestData requestData) { |
| 113 mRequestData = requestData; |
| 114 } |
| 115 |
| 116 @Override |
| 117 public void run(AccountManagerFuture<Bundle> future) { |
| 118 Bundle result; |
| 119 try { |
| 120 result = future.getResult(); |
| 121 } catch (OperationCanceledException | AuthenticatorException | IOExc
eption e) { |
| 122 Log.w(TAG, "Operation did not complete", e); |
| 123 nativeSetResult(mRequestData.nativeResultObject, NetError.ERR_UN
EXPECTED, null); |
| 124 return; |
| 125 } |
| 126 |
| 127 if (result.containsKey(AccountManager.KEY_INTENT)) { |
| 128 final Context appContext = ContextUtils.getApplicationContext(); |
| 129 |
| 130 // We wait for a broadcast that should be sent once the user is
done interacting |
| 131 // with the notification |
| 132 // TODO(dgn) We currently hang around if the notification is swi
ped away, until |
| 133 // a LOGIN_ACCOUNTS_CHANGED_ACTION filter is received. It might
be for something |
| 134 // unrelated then we would wait again here. Maybe we should limi
t the number of |
| 135 // retries in some way? |
| 136 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { |
| 137 |
| 138 @Override |
| 139 public void onReceive(Context context, Intent intent) { |
| 140 appContext.unregisterReceiver(this); |
| 141 mRequestData.accountManager.getAuthToken(mRequestData.ac
count, |
| 142 mRequestData.authTokenType, mRequestData.options
, |
| 143 true /* notifyAuthFailure */, new GetTokenCallba
ck(mRequestData), |
| 144 null); |
| 145 } |
| 146 |
| 147 }; |
| 148 appContext.registerReceiver(broadcastReceiver, |
| 149 new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_A
CTION)); |
| 150 } else { |
| 151 processResult(result, mRequestData); |
| 152 } |
| 153 } |
| 154 } |
| 155 |
| 156 protected HttpNegotiateAuthenticator(String accountType) { |
35 assert !android.text.TextUtils.isEmpty(accountType); | 157 assert !android.text.TextUtils.isEmpty(accountType); |
36 mAccountType = accountType; | 158 mAccountType = accountType; |
37 } | 159 } |
38 | 160 |
39 /** | 161 /** |
40 * @param accountType The Android account type to use. | 162 * @param accountType The Android account type to use. |
41 */ | 163 */ |
42 @VisibleForTesting | 164 @VisibleForTesting |
43 @CalledByNative | 165 @CalledByNative |
44 static HttpNegotiateAuthenticator create(String accountType) { | 166 static HttpNegotiateAuthenticator create(String accountType) { |
45 return new HttpNegotiateAuthenticator(accountType); | 167 return new HttpNegotiateAuthenticator(accountType); |
46 } | 168 } |
47 | 169 |
48 /** | 170 /** |
49 * @param nativeResultObject The C++ object used to return the result. For c
orrect C++ memory | 171 * @param nativeResultObject The C++ object used to return the result. For c
orrect C++ memory |
50 * management we must call nativeSetResult precisely once with th
is object. | 172 * management we must call nativeSetResult precisely once with th
is object. |
51 * @param principal The principal (must be host based). | 173 * @param principal The principal (must be host based). |
52 * @param authToken The incoming auth token. | 174 * @param authToken The incoming auth token. |
53 * @param canDelegate True if we can delegate. | 175 * @param canDelegate True if we can delegate. |
54 */ | 176 */ |
55 @VisibleForTesting | 177 @VisibleForTesting |
56 @CalledByNative | 178 @CalledByNative |
57 void getNextAuthToken(final long nativeResultObject, final String principal,
String authToken, | 179 void getNextAuthToken(final long nativeResultObject, final String principal,
String authToken, |
58 boolean canDelegate) { | 180 boolean canDelegate) { |
59 assert principal != null; | 181 assert principal != null; |
60 String authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BASE + p
rincipal; | 182 |
| 183 Context applicationContext = ContextUtils.getApplicationContext(); |
| 184 RequestData requestData = new RequestData(); |
| 185 requestData.authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BAS
E + principal; |
| 186 requestData.accountManager = AccountManager.get(applicationContext); |
| 187 requestData.nativeResultObject = nativeResultObject; |
| 188 String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; |
| 189 |
| 190 requestData.options = new Bundle(); |
| 191 if (authToken != null) { |
| 192 requestData.options.putString( |
| 193 HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, authToken); |
| 194 } |
| 195 if (mSpnegoContext != null) { |
| 196 requestData.options.putBundle( |
| 197 HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext); |
| 198 } |
| 199 requestData.options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE,
canDelegate); |
| 200 Handler uiThreadHandler = new Handler(ThreadUtils.getUiThreadLooper()); |
| 201 |
61 Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); | 202 Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
62 if (activity == null) { | 203 if (activity == null) { |
63 nativeSetResult(nativeResultObject, NetError.ERR_UNEXPECTED, null); | 204 // This code path is not as able to get user input as the one that h
as an activity. It |
64 return; | 205 // doesn't handle 0 or multiple accounts and will just result in a f
ailure in those |
| 206 // cases. |
| 207 if (!hasPermission(applicationContext, android.Manifest.permission.G
ET_ACCOUNTS)) { |
| 208 // API < 23 Requires the GET_ACCOUNTS permission or throws an e
xception. |
| 209 // API >= 23 Requires the GET_ACCOUNTS permission or returns onl
y the accounts |
| 210 // whose authenticator has a signature matches our app
. |
| 211 Log.e(TAG, "GET_ACCOUNTS permission not granted. Aborting authen
tication."); |
| 212 nativeSetResult(requestData.nativeResultObject, |
| 213 NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
| 214 return; |
| 215 } |
| 216 requestData.accountManager.getAccountsByTypeAndFeatures( |
| 217 mAccountType, features, new GetAccountsCallback(requestData)
, uiThreadHandler); |
| 218 } else { |
| 219 // If there is more than one account, it will show the an account pi
cker dialog for |
| 220 // each query (e.g. html file, then favicon...) |
| 221 // Same if the credentials need to be confirmed. |
| 222 // If there is a failure, it will retry automatically. |
| 223 if (!hasPermission(applicationContext, "android.permission.MANAGE_AC
COUNTS")) { |
| 224 // API < 23 Requires the MANAGE_ACCOUNTS permission. |
| 225 // API >= 23 Requires something in the CONTACTS permission group
to behave properly. |
| 226 // MANAGE_ACCOUNTS is auto-granted on install without
having be declared. |
| 227 Log.e(TAG, "MANAGE_ACCOUNTS permission not granted. Aborting aut
hentication."); |
| 228 nativeSetResult(requestData.nativeResultObject, |
| 229 NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
| 230 return; |
| 231 } |
| 232 requestData.accountManager.getAuthTokenByFeatures(mAccountType, |
| 233 requestData.authTokenType, features, activity, null, request
Data.options, |
| 234 new GetTokenCallback(requestData), uiThreadHandler); |
65 } | 235 } |
66 AccountManager am = AccountManager.get(activity); | 236 } |
67 String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; | |
68 | 237 |
69 Bundle options = new Bundle(); | 238 /** |
70 | 239 * Process a result bundle from a completed token request, communicating its
content back to |
71 if (authToken != null) { | 240 * the native code. |
72 options.putString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, au
thToken); | 241 */ |
| 242 private void processResult(Bundle result, RequestData requestData) { |
| 243 mSpnegoContext = result.getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONT
EXT); |
| 244 int status; |
| 245 switch (result.getInt( |
| 246 HttpNegotiateConstants.KEY_SPNEGO_RESULT, HttpNegotiateConstants
.ERR_UNEXPECTED)) { |
| 247 case HttpNegotiateConstants.OK: |
| 248 status = 0; |
| 249 break; |
| 250 case HttpNegotiateConstants.ERR_UNEXPECTED: |
| 251 status = NetError.ERR_UNEXPECTED; |
| 252 break; |
| 253 case HttpNegotiateConstants.ERR_ABORTED: |
| 254 status = NetError.ERR_ABORTED; |
| 255 break; |
| 256 case HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS: |
| 257 status = NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS; |
| 258 break; |
| 259 case HttpNegotiateConstants.ERR_INVALID_RESPONSE: |
| 260 status = NetError.ERR_INVALID_RESPONSE; |
| 261 break; |
| 262 case HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS: |
| 263 status = NetError.ERR_INVALID_AUTH_CREDENTIALS; |
| 264 break; |
| 265 case HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME: |
| 266 status = NetError.ERR_UNSUPPORTED_AUTH_SCHEME; |
| 267 break; |
| 268 case HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS: |
| 269 status = NetError.ERR_MISSING_AUTH_CREDENTIALS; |
| 270 break; |
| 271 case HttpNegotiateConstants.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS
: |
| 272 status = NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS; |
| 273 break; |
| 274 case HttpNegotiateConstants.ERR_MALFORMED_IDENTITY: |
| 275 status = NetError.ERR_MALFORMED_IDENTITY; |
| 276 break; |
| 277 default: |
| 278 status = NetError.ERR_UNEXPECTED; |
73 } | 279 } |
74 if (mSpnegoContext != null) { | 280 nativeSetResult(requestData.nativeResultObject, status, |
75 options.putBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnego
Context); | 281 result.getString(AccountManager.KEY_AUTHTOKEN)); |
76 } | |
77 options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate)
; | |
78 | |
79 mFuture = am.getAuthTokenByFeatures(mAccountType, authTokenType, feature
s, activity, null, | |
80 options, new AccountManagerCallback<Bundle>() { | |
81 | |
82 @Override | |
83 public void run(AccountManagerFuture<Bundle> future) { | |
84 try { | |
85 Bundle result = future.getResult(); | |
86 mSpnegoContext = | |
87 result.getBundle(HttpNegotiateConstants.KEY_
SPNEGO_CONTEXT); | |
88 int status; | |
89 switch (result.getInt(HttpNegotiateConstants.KEY_SPN
EGO_RESULT, | |
90 HttpNegotiateConstants.ERR_UNEXPECTED)) { | |
91 case HttpNegotiateConstants.OK: | |
92 status = 0; | |
93 break; | |
94 case HttpNegotiateConstants.ERR_UNEXPECTED: | |
95 status = NetError.ERR_UNEXPECTED; | |
96 break; | |
97 case HttpNegotiateConstants.ERR_ABORTED: | |
98 status = NetError.ERR_ABORTED; | |
99 break; | |
100 case HttpNegotiateConstants.ERR_UNEXPECTED_SECUR
ITY_LIBRARY_STATUS: | |
101 status = NetError.ERR_UNEXPECTED_SECURITY_LI
BRARY_STATUS; | |
102 break; | |
103 case HttpNegotiateConstants.ERR_INVALID_RESPONSE
: | |
104 status = NetError.ERR_INVALID_RESPONSE; | |
105 break; | |
106 case HttpNegotiateConstants.ERR_INVALID_AUTH_CRE
DENTIALS: | |
107 status = NetError.ERR_INVALID_AUTH_CREDENTIA
LS; | |
108 break; | |
109 case HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH
_SCHEME: | |
110 status = NetError.ERR_UNSUPPORTED_AUTH_SCHEM
E; | |
111 break; | |
112 case HttpNegotiateConstants.ERR_MISSING_AUTH_CRE
DENTIALS: | |
113 status = NetError.ERR_MISSING_AUTH_CREDENTIA
LS; | |
114 break; | |
115 case HttpNegotiateConstants | |
116 .ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATU
S: | |
117 status = NetError.ERR_UNDOCUMENTED_SECURITY_
LIBRARY_STATUS; | |
118 break; | |
119 case HttpNegotiateConstants.ERR_MALFORMED_IDENTI
TY: | |
120 status = NetError.ERR_MALFORMED_IDENTITY; | |
121 break; | |
122 default: | |
123 status = NetError.ERR_UNEXPECTED; | |
124 } | |
125 nativeSetResult(nativeResultObject, status, | |
126 result.getString(AccountManager.KEY_AUTHTOKE
N)); | |
127 } catch (OperationCanceledException | AuthenticatorExcep
tion | |
128 | IOException e) { | |
129 nativeSetResult(nativeResultObject, NetError.ERR_ABO
RTED, null); | |
130 } | |
131 } | |
132 | |
133 }, new Handler(ThreadUtils.getUiThreadLooper())); | |
134 } | 282 } |
135 | 283 |
136 @VisibleForTesting | 284 @VisibleForTesting |
| 285 boolean hasPermission(Context context, String permission) { |
| 286 int permissionResult = |
| 287 context.checkPermission(permission, Process.myPid(), Process.myU
id()); |
| 288 return permissionResult == PackageManager.PERMISSION_GRANTED; |
| 289 } |
| 290 |
| 291 @VisibleForTesting |
137 native void nativeSetResult( | 292 native void nativeSetResult( |
138 long nativeJavaNegotiateResultWrapper, int status, String authToken)
; | 293 long nativeJavaNegotiateResultWrapper, int status, String authToken)
; |
139 } | 294 } |
OLD | NEW |