summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
blob: 259b1cb8874252904e3ad92fb5611b737b45be9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.fxa.authenticator;

import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.receivers.FxAccountDeletedService;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.util.ThreadUtils;

import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
  public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
  public static final int UNKNOWN_ERROR_CODE = 999;

  protected final Context context;
  protected final AccountManager accountManager;

  public FxAccountAuthenticator(Context context) {
    super(context);
    this.context = context;
    this.accountManager = AccountManager.get(context);
  }

  @Override
  public Bundle addAccount(AccountAuthenticatorResponse response,
      String accountType, String authTokenType, String[] requiredFeatures,
      Bundle options)
          throws NetworkErrorException {
    Logger.debug(LOG_TAG, "addAccount");

    // The data associated to each Account should be invalidated when we change
    // the set of Firefox Accounts on the system.
    AndroidFxAccount.invalidateCaches();

    final Bundle res = new Bundle();

    if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
      res.putInt(AccountManager.KEY_ERROR_CODE, -1);
      res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
      return res;
    }

    final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
    res.putParcelable(AccountManager.KEY_INTENT, intent);
    return res;
  }

  @Override
  public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
      throws NetworkErrorException {
    Logger.debug(LOG_TAG, "confirmCredentials");

    return null;
  }

  @Override
  public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
    Logger.debug(LOG_TAG, "editProperties");

    return null;
  }

  protected static class Responder {
    final AccountAuthenticatorResponse response;
    final AndroidFxAccount fxAccount;

    public Responder(AccountAuthenticatorResponse response, AndroidFxAccount fxAccount) {
      this.response = response;
      this.fxAccount = fxAccount;
    }

    public void fail(Exception e) {
      Logger.warn(LOG_TAG, "Responding with error!", e);
      fxAccount.releaseSharedAccountStateLock();
      final Bundle result = new Bundle();
      result.putInt(AccountManager.KEY_ERROR_CODE, UNKNOWN_ERROR_CODE);
      result.putString(AccountManager.KEY_ERROR_MESSAGE, e.toString());
      response.onResult(result);
    }

    public void succeed(String authToken) {
      Logger.info(LOG_TAG, "Responding with success!");
      fxAccount.releaseSharedAccountStateLock();
      final Bundle result = new Bundle();
      result.putString(AccountManager.KEY_ACCOUNT_NAME, fxAccount.account.name);
      result.putString(AccountManager.KEY_ACCOUNT_TYPE, fxAccount.account.type);
      result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
      response.onResult(result);
    }
  }

  public abstract static class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
    protected final Context context;
    protected final AndroidFxAccount fxAccount;
    protected final Executor executor;
    protected final FxAccountClient client;

    public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
      this.context = context;
      this.fxAccount = fxAccount;
      this.executor = Executors.newSingleThreadExecutor();
      this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
    }

    @Override
    public FxAccountClient getClient() {
      return client;
    }

    @Override
    public long getCertificateDurationInMilliseconds() {
      return 12 * 60 * 60 * 1000;
    }

    @Override
    public long getAssertionDurationInMilliseconds() {
      return 15 * 60 * 1000;
    }

    @Override
    public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
      return StateFactory.generateKeyPair();
    }

    @Override
    public void handleTransition(Transition transition, State state) {
      Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
    }

    abstract public void handleNotMarried(State notMarried);
    abstract public void handleMarried(Married married);

    @Override
    public void handleFinal(State state) {
      Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
      fxAccount.setState(state);
      // Update any notifications displayed.
      final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
      notificationManager.update(context, fxAccount);

      if (state.getStateLabel() != StateLabel.Married) {
        handleNotMarried(state);
        return;
      } else {
        handleMarried((Married) state);
      }
    }
  }

  protected void getOAuthToken(final AccountAuthenticatorResponse response, final AndroidFxAccount fxAccount, final String scope) throws NetworkErrorException {
    Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope);

    final Responder responder = new Responder(response, fxAccount);
    final String oauthServerUri = fxAccount.getOAuthServerURI();

    final String audience;
    try {
      audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token.
    } catch (Exception e) {
      Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
      responder.fail(e);
      return;
    }

    final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();

    stateMachine.advance(fxAccount.getState(), StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
      @Override
      public void handleNotMarried(State state) {
        final String message = "Cannot fetch oauth token from state: " + state.getStateLabel();
        Logger.warn(LOG_TAG, message);
        responder.fail(new RuntimeException(message));
      }

      @Override
      public void handleMarried(final Married married) {
        final String assertion;
        try {
          assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
          if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
            JSONWebTokenUtils.dumpAssertion(assertion);
          }
        } catch (Exception e) {
          Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
          responder.fail(e);
          return;
        }

        final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor);
        Logger.debug(LOG_TAG, "OAuth fetch for scope: " + scope);
        oauthClient.authorization(FxAccountConstants.OAUTH_CLIENT_ID_FENNEC, assertion, null, scope, new RequestDelegate<FxAccountOAuthClient10.AuthorizationResponse>() {
          @Override
          public void handleSuccess(AuthorizationResponse result) {
            Logger.debug(LOG_TAG, "OAuth success.");
            FxAccountUtils.pii(LOG_TAG, "Fetched oauth token: " + result.access_token);
            responder.succeed(result.access_token);
          }

          @Override
          public void handleFailure(FxAccountAbstractClientRemoteException e) {
            Logger.error(LOG_TAG, "OAuth failure.", e);
            if (e.isInvalidAuthentication()) {
              // We were married, generated an assertion, and our assertion was rejected by the
              // oauth client. If it's a 401, we probably have a stale certificate.  If instead of
              // a stale certificate we have bad credentials, the state machine will fail to sign
              // our public key and drive us back to Separated.
              fxAccount.setState(married.makeCohabitingState());
            }
            responder.fail(e);
          }

          @Override
          public void handleError(Exception e) {
            Logger.error(LOG_TAG, "OAuth error.", e);
            responder.fail(e);
          }
        });
      }
    });
  }

  @Override
  public Bundle getAuthToken(final AccountAuthenticatorResponse response,
      final Account account, final String authTokenType, final Bundle options)
          throws NetworkErrorException {
    Logger.debug(LOG_TAG, "getAuthToken: " + authTokenType);

    // If we have a cached authToken, hand it over.
    final String cachedAuthToken = AccountManager.get(context).peekAuthToken(account, authTokenType);
    if (cachedAuthToken != null && !cachedAuthToken.isEmpty()) {
      Logger.info(LOG_TAG, "Return cached token.");
      final Bundle result = new Bundle();
      result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
      result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
      result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken);
      return result;
    }

    // If we're asked for an oauth::scope token, try to generate one.
    final String oauthPrefix = "oauth::";
    if (authTokenType != null && authTokenType.startsWith(oauthPrefix)) {
      final String scope = authTokenType.substring(oauthPrefix.length());
      final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
      try {
        fxAccount.acquireSharedAccountStateLock(LOG_TAG);
      } catch (InterruptedException e) {
        Logger.warn(LOG_TAG, "Could not acquire account state lock; return error bundle.");
        final Bundle bundle = new Bundle();
        bundle.putInt(AccountManager.KEY_ERROR_CODE, 1);
        bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Could not acquire account state lock.");
        return bundle;
      }
      getOAuthToken(response, fxAccount, scope);
      return null;
    }

    // Otherwise, fail.
    Logger.warn(LOG_TAG, "Returning error bundle for getAuthToken with unknown token type.");
    final Bundle bundle = new Bundle();
    bundle.putInt(AccountManager.KEY_ERROR_CODE, 2);
    bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Unknown token type: " + authTokenType);
    return bundle;
  }

  @Override
  public String getAuthTokenLabel(String authTokenType) {
    Logger.debug(LOG_TAG, "getAuthTokenLabel");

    return null;
  }

  @Override
  public Bundle hasFeatures(AccountAuthenticatorResponse response,
      Account account, String[] features) throws NetworkErrorException {
    Logger.debug(LOG_TAG, "hasFeatures");

    return null;
  }

  @Override
  public Bundle updateCredentials(AccountAuthenticatorResponse response,
      Account account, String authTokenType, Bundle options)
          throws NetworkErrorException {
    Logger.debug(LOG_TAG, "updateCredentials");

    return null;
  }

  /**
   * If the account is going to be removed, broadcast an "account deleted"
   * intent. This allows us to clean up the account.
   * <p>
   * It is preferable to receive Android's LOGIN_ACCOUNTS_CHANGED_ACTION broadcast
   * than to create our own hacky broadcast here, but that doesn't include enough
   * information about which Accounts changed to correctly identify whether a Sync
   * account has been removed (when some Firefox channels are installed on the SD
   * card). We can work around this by storing additional state but it's both messy
   * and expensive because the broadcast is noisy.
   * <p>
   * Note that this is <b>not</b> called when an Android Account is blown away
   * due to the SD card being unmounted.
   */
  @Override
  public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account)
      throws NetworkErrorException {
    Bundle result = super.getAccountRemovalAllowed(response, account);

    if (result == null ||
        !result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) ||
        result.containsKey(AccountManager.KEY_INTENT)) {
      return result;
    }

    final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
    if (!removalAllowed) {
      return result;
    }

    // Broadcast a message to all Firefox channels sharing this Android
    // Account type telling that this Firefox account has been deleted.
    //
    // Broadcast intents protected with permissions are secure, so it's okay
    // to include private information such as a password.
    final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account);

    // Deleting the pickle file in a blocking manner will avoid race conditions that might happen when
    // an account is unpickled while an FxAccount is being deleted.
    // Also we have an assumption that this method is always called from a background thread, so we delete
    // the pickle file directly without being afraid from a StrictMode violation.
    ThreadUtils.assertNotOnUiThread();

    final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent(
            new Intent(context, FxAccountDeletedService.class)
    );
    Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
        "starting FxAccountDeletedService with action: " + serviceIntent.getAction() + ".");
    context.startService(serviceIntent);

    Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " +
            "deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
    deletePickle();

    return result;
  }

  private void deletePickle() {
    try {
      AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
    } catch (Exception e) {
      // This should never happen, but we really don't want to die in a background thread.
      Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
    }
  }
}