android-components/components/service/firefox-accounts/README.md

318 lines
13 KiB
Markdown
Raw Permalink Normal View History

2023-11-28 09:10:03 +01:00
# [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/