summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/service/firefox-accounts/README.md
blob: dc78194d782a59f8bc623bbde24b72243bfced5c (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
# [Android Components](../../../README.md) > Service > Firefox Accounts (FxA)

A library for integrating with Firefox Accounts.

## Motivation

The **Firefox Accounts Android Component** provides both low and high level accounts functionality.

At a low level, there is direct interaction with the accounts system:
* Obtain scoped OAuth tokens that can be used to access the user's data in Mozilla-hosted services like Firefox Sync
* Fetch client-side scoped keys needed for end-to-end encryption of that data
* Fetch a user's profile to personalize the application

At a high level, there is an Account Manager:
* Handles account state management and persistence
* Abstracts away OAuth details, handling scopes, token caching, recovery, etc. Application can still specify custom scopes if needed
* Integrates with FxA device management, automatically creating and destroying device records as appropriate
* (optionally) Provides Send Tab integration - allows sending and receiving tabs within the Firefox Account ecosystem
* (optionally) Provides Firefox Sync integration

Sample applications:
* [accounts sample app](https://github.com/mozilla-mobile/android-components/tree/main/samples/firefox-accounts), demonstrates how to use low level APIs
* [sync app](https://github.com/mozilla-mobile/android-components/tree/main/samples/sync), demonstrates a high level accounts integration, complete with syncing multiple data stores

Useful companion components:
* [feature-accounts](https://github.com/mozilla-mobile/android-components/tree/main/components/feature/accounts), provides a `tabs` integration on top of `FxaAccountManager`, to handle display of web sign-in UI.
* [browser-storage-sync](https://github.com/mozilla-mobile/android-components/tree/main/components/browser/storage-sync), provides data storage layers compatible with Firefox Sync.

## Before using this component
Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
This component provides data collection using the [Glean SDK](https://mozilla.github.io/glean/book/index.html).
The list of metrics being collected is available in the [metrics documentation](../../support/sync-telemetry/docs/metrics.md).

## Usage
### Setting up the dependency

Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):

```Groovy
implementation "org.mozilla.components:service-firefox-accounts:{latest-version}"
```

### High level APIs, recommended for most applications

Below is an example of how to integrate most of the common functionality exposed by `FxaAccountManager`.
Additionally, see `feature-accounts`

```kotlin
// Make the two "syncable" stores accessible to account manager's sync machinery.
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage)

val accountManager = FxaAccountManager(
    context = this,
    serverConfig = ServerConfig.release(CLIENT_ID, REDIRECT_URL),
    deviceConfig = DeviceConfig(
        name = "Sample app",
        type = DeviceType.MOBILE,
        capabilities = setOf(DeviceCapability.SEND_TAB)
    ),
    syncConfig = SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), syncPeriodInMinutes = 15L)
)

// Observe changes to the account and profile.
accountManager.register(accountObserver, owner = this, autoPause = true)

// Observe sync state changes.
accountManager.registerForSyncEvents(syncObserver, owner = this, autoPause = true)

// Observe incoming account events (e.g. when another device connects or
// disconnects to/from the account, SEND_TAB commands from other devices, etc).
// Note that since the device is configured with a SEND_TAB capability, device constellation will be
// automatically updated during any account initialization flow (restore, login, sign-up, recovery).
// It is up to the application to keep it up-to-date beyond that.
// See `account.deviceConstellation().refreshDeviceStateAsync()`.
accountManager.registerForAccountEvents(accountEventsObserver, owner = this, autoPause = true)

// Now that all of the observers we care about are registered, kick off the account manager.
// If we're already authenticated
launch { accountManager.initAsync().await() }

// 'Sync Now' button binding.
findViewById<View>(R.id.buttonSync).setOnClickListener {
    accountManager.syncNowAsync(SyncReason.User)
}

// 'Sign-in' button binding.
findViewById<View>(R.id.buttonSignIn).setOnClickListener {
    launch {
        val authUrl = accountManager.beginAuthenticationAsync().await()
        authUrl?.let { openWebView(it) }
    }
}

// 'Sign-out' button binding
findViewById<View>(R.id.buttonLogout).setOnClickListener {
    launch {
        accountManager.logoutAsync().await()
    }
}

// 'Disable periodic sync' button binding
findViewById<View>(R.id.disablePeriodicSync).setOnClickListener {
    launch {
        accountManager.setSyncConfigAsync(
            SyncConfig(setOf(SyncReason.History, SyncReason.Bookmarks)
        ).await()
    }
}

// 'Enable periodic sync' button binding
findViewById<View>(R.id.enablePeriodicSync).setOnClickListener {
    launch {
        accountManager.setSyncConfigAsync(
            SyncConfig(setOf(SyncReason.History, SyncReason.Bookmarks), syncPeriodInMinutes = 60L)
        ).await()
    }
}

// Globally disabled syncing an engine - this affects all Firefox Sync clients.
findViewById<View>(R.id.globallyDisableHistoryEngine).setOnClickListener {
    SyncEnginesStorage.setStatus(SyncEngine.History, false)
    accountManager.syncNowAsync(SyncReason.EngineChange)
}

// Get current status of SyncEngines. Note that this may change after every sync, as other Firefox Sync clients can change it.
val engineStatusMap = SyncEnginesStorage.getStatus() // type is: Map<SyncEngine, Boolean>

// This is expected to be called from the webview/geckoview integration, which intercepts page loads and gets
// 'code' and 'state' out of the 'successful sign-in redirect' url.
fun onLoginComplete(code: String, state: String) {
    launch {
        accountManager.finishAuthenticationAsync(code, state).await()
    }
}

// Observe changes to account state.
val accountObserver = object : AccountObserver {
    override fun onLoggedOut() = launch {
        // handle logging-out in the UI
    }

    override fun onAuthenticationProblems() = launch {
        // prompt user to re-authenticate
    }

    override fun onAuthenticated(account: OAuthAccount) = launch {
        // logged-in successfully; display account details
    }

    override fun onProfileUpdated(profile: Profile) {
        // display ${profile.displayName} and ${profile.email} if desired
    }
}

// Observe changes to sync state.
val syncObserver = object : SyncStatusObserver {
    override fun onStarted() = launch {
        // sync started running; update some UI to indicate this
    }

    override fun onIdle() = launch {
        // sync stopped running; update some UI to indicate this
    }

    override fun onError(error: Exception?) = launch {
        // sync encountered an error; optionally indicate this in the UI
    }
}

// Observe incoming account events.
val accountEventsObserver = object : AccountEventsObserver {
    override fun onEvents(event: List<AccountEvent>) {
        // device received some commands; for example, here's how you can process incoming Send Tab commands:
        commands
            .filter { it is AccountEvent.CommandReceived }
            .map { it.command }
            .filter { it is DeviceCommandIncoming.TabReceived }
            .forEach {
                val tabReceivedCommand = it as DeviceCommandIncoming.TabReceived
                val fromDeviceName = tabReceivedCommand.from?.displayName
                showNotification("Tab ${tab.title}, received from: ${fromDisplayName}", tab.url)
            }
            // (although note the SendTabFeature makes dealing with these commands
            // easier still.)
    }
}
```

### Low level APIs

First you need some OAuth information. Generate a `client_id`, `redirectUrl` and find out the scopes for your application.
See the [Firefox Account documentation](https://mozilla.github.io/application-services/docs/accounts/welcome.html)
for that.

Once you have the OAuth info, you can start adding `FxAClient` to your Android project.
As part of the OAuth flow your application will be opening up a WebView or a Custom Tab.
Currently the SDK does not provide the WebView, you have to write it yourself.

Create a global `account` object:

```kotlin
var account: FirefoxAccount? = null
```

You will need to save state for FxA in your app, this example just uses `SharedPreferences`. We suggest using the [Android Keystore]( https://developer.android.com/training/articles/keystore) for this data.
Define variables to help save state for FxA:

```kotlin
val STATE_PREFS_KEY = "fxaAppState"
val STATE_KEY = "fxaState"
```

Then you can write the following:

```kotlin

account = getAuthenticatedAccount()
if (account == null) {
  // Start authentication flow
  val config = Config(CONFIG_URL, CLIENT_ID, REDIRECT_URL)
  // Some helpers such as Config.release(CLIENT_ID, REDIRECT_URL)
  // are also provided for well-known Firefox Accounts servers.
  account = FirefoxAccount(config)
}

fun getAuthenticatedAccount(): FirefoxAccount? {
    val savedJSON = getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).getString(FXA_STATE_KEY, "")
    return savedJSON?.let {
        try {
            FirefoxAccount.fromJSONString(it)
        } catch (e: FxaException) {
            null
        }
    } ?: null
}
```

The code above checks if you have some existing state for FxA, otherwise it configures it. All asynchronous methods on `FirefoxAccount` are executed on `Dispatchers.IO`'s dedicated thread pool. They return `Deferred` which is Kotlin's non-blocking cancellable Future type.

Once the configuration is available and an account instance was created, the authentication flow can be started:

```kotlin
launch {
    val url = account.beginOAuthFlow(scopes).await()
    openWebView(url)
}
```

When spawning the WebView, be sure to override the `OnPageStarted` function to intercept the redirect url and fetch the code + state parameters:

```kotlin
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
    if (url != null && url.startsWith(redirectUrl)) {
        val uri = Uri.parse(url)
        val mCode = uri.getQueryParameter("code")
        val mState = uri.getQueryParameter("state")
        if (mCode != null && mState != null) {
            // Pass the code and state parameters back to your main activity
            listener?.onLoginComplete(mCode, mState, this@LoginFragment)
        }
    }

    super.onPageStarted(view, url, favicon)
}
```

Finally, complete the OAuth flow, retrieve the profile information, then save your login state once you've gotten valid profile information:

```kotlin
launch {
    // Complete authentication flow
    account.completeOAuthFlow(code, state).await()

    // Display profile information
    val profile = account.getProfile().await()
    txtView.txt = profile.displayName

    // Persist login state
    val json = account.toJSONString()
    getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).edit()
        .putString(FXA_STATE_KEY, json).apply()
}
```

## Automatic sign-in via trusted on-device FxA Auth providers

If there are trusted FxA auth providers available on the device, and they're signed-in, it's possible
to automatically sign-in into the same account, gaining access to the same data they have access to (e.g. Firefox Sync).

Currently supported FxA auth providers are:
- Firefox for Android (release, beta and nightly channels)

`AccountSharing` provides facilities to securely query auth providers for available accounts. It may be used
directly in concert with a low-level `FirefoxAccount.migrateFromSessionTokenAsync`, or via the high-level `FxaAccountManager`:

```kotlin
val availableAccounts = accountManager.shareableAccounts(context)
// Display a list of accounts to the user, identified by account.email and account.sourcePackage
// Or, pick the first available account. They're sorted in an order of internal preference (release, beta, nightly).
val selectedAccount = availableAccounts[0]
launch {
    val result = accountManager.signInWithShareableAccountAsync(selectedAccount).await()
    if (result) {
        // Successfully signed-into an account.
        // accountManager.authenticatedAccount() is the new account.
    } else {
        // Failed to sign-into an account, either due to bad credentials or networking issues.
    }
}
```

## License

    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/