diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31ca7e4..d62bc3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- **Web, Android and iOS apps per profile.** A profile now holds a list of the
+ project's apps instead of a single API key and App ID, and an **App**
+ dropdown in the top bar picks the one the ID-token and App Check tabs use.
+ **Load apps** imports every app in the project, with its API key, package
+ name and SHA-1 or bundle ID, through the Firebase Management API; apps can
+ also be added by hand.
+- **API keys restricted to an Android or iOS app now work.** Requests made with
+ such an app's key send `X-Android-Package` and `X-Android-Cert`, or
+ `X-Ios-Bundle-Identifier`, the way the Firebase SDKs do. Previously only the
+ key itself was sent, so Google rejected any key with an app restriction.
+
- **A user guide**, published at
. It walks through
setting up a Firebase project, every tab, profiles and the Users panel, and
@@ -16,8 +27,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
screenshots come from a real demo project, and the README now opens with a
short recording of the main flow.
+### Changed
+
+- The persistence checkbox is now **remember API keys & debug tokens**, and it
+ no longer covers App IDs. App names, App IDs, package names, SHA-1s and
+ bundle IDs are always saved: they are public, and an app list that forgot
+ them on every restart would be of little use. API keys and App Check debug
+ tokens remain opt-in.
+- Existing settings migrate on first launch: each profile's API key, App ID and
+ debug token become its first app, with the type taken from the App ID.
+
### Fixed
+- **Clear all claims failed** with `INVALID_CLAIMS : Not a JSON Object: null`.
+ It sent an empty `customAttributes` string, which the Identity Toolkit API now
+ rejects; it now sends `{}`, as the Admin SDKs do.
+- The last rows of a long **Decoded JWT claims** table could fall below the
+ window with no way to scroll to them. Each tab now scrolls as a whole.
+- A refused **Save** on the User Custom Claims tab (for example, over the
+ 1,000-byte limit) no longer shows the previous save's "Saved." message next to
+ the error.
- `README.md` and `SECURITY.md` called the persistence checkbox "Remember
secrets"; its label is **remember API key & App ID**. They also left out that
the App Check debug token is gated by the same checkbox, and `SECURITY.md`
diff --git a/Cargo.lock b/Cargo.lock
index e3d0cc9..f7082ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1274,6 +1274,7 @@ dependencies = [
"rand 0.8.6",
"reqwest",
"rfd",
+ "ron",
"rsa",
"serde",
"serde_json",
diff --git a/Cargo.toml b/Cargo.toml
index d064064..9d75bae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -65,6 +65,9 @@ arboard = "3"
# private key needs to be committed to the repository.
rsa = { version = "0.9", features = ["pem"] }
rand = "0.8"
+# Parses saved-settings fixtures in the config migration tests. Already in the
+# tree through eframe's persistence feature; keep the version in step with it.
+ron = "0.11"
[build-dependencies]
winresource = "0.1"
diff --git a/README.md b/README.md
index f9ea6bd..b5438a7 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A native desktop GUI for the Firebase auth tokens you need during development an
[](./LICENSE)
[](https://smaranjit.github.io/firebase-token-toolkit/)
-
+
**[Read the user guide →](https://smaranjit.github.io/firebase-token-toolkit/)** It covers setup, every tab, profiles, and troubleshooting.
@@ -41,7 +41,8 @@ xattr -dr com.apple.quarantine "/Applications/Firebase Token Toolkit.app"
## Features
-- **Multiple profiles** — switch between Firebase projects (dev/staging/prod, multiple clients, …) from a dropdown in the top bar. Each profile remembers its own service-account path, project ID, API key, App ID, and debug token. Switching a profile reloads the service account and clears the user list, OAuth cache, selected UID, and all tab outputs.
+- **Multiple profiles** — switch between Firebase projects (dev/staging/prod, multiple clients, …) from a dropdown in the top bar. Each profile remembers its own service-account path, project ID, and apps. Switching a profile reloads the service account and clears the user list, OAuth cache, selected UID, and all tab outputs.
+- **Web, Android and iOS apps** — **Load apps** imports the project's apps with their API keys through the Firebase Management API, or add them by hand. Requests made with an Android or iOS app's key send its package name and SHA-1, or bundle ID, so keys restricted to that app work.
- **UID -> Custom Token** — sign a Firebase custom token (RS256) using a service account, with optional custom claims.
- **UID -> ID Token** — one-shot: sign a custom token, then exchange it via `signInWithCustomToken`.
- **Custom -> ID Token** — paste a custom token and exchange it for an ID token.
@@ -49,17 +50,17 @@ xattr -dr com.apple.quarantine "/Applications/Firebase Token Toolkit.app"
- **App Check** — exchange a registered debug token via `firebaseappcheck.googleapis.com`.
- **Dynamic UID picker** — a left-side panel lists every user via `accounts:batchGet` (paginated up to 5,000) and lets you search by email, phone, or UID. Direct lookups fall through to `accounts:lookup` for users not present in the loaded page.
- Decoded JWT claims table for every token, with timestamp → ISO conversion and clipboard copy.
-- Persists project ID, service-account path, and last-used tab between launches. API keys, App IDs and App Check debug tokens are persisted **only if you opt in** — see [Security](#security).
+- Persists project ID, service-account path, and last-used tab between launches. API keys and App Check debug tokens are persisted **only if you opt in** — see [Security](#security).
## Configure
In the top bar:
1. **Service account** → *Browse* and pick the JSON from Firebase Console → Project Settings → Service accounts → *Generate new private key*. The project ID auto-populates from the file.
-2. **API key** → Firebase Console → Project Settings → General → Web API Key. Only needed for the ID-token and App Check tabs.
-3. **App ID** → only needed for the App Check tab.
+2. **Load apps** → imports the project's web, Android and iOS apps with their App IDs and API keys. Or use **+ Add app** and paste an API key and App ID from Firebase Console → Project Settings → General → Your apps.
+3. Pick the app to use from the **App** dropdown. Its API key is used by the ID-token and App Check tabs.
-The status indicator next to the service account turns green and reads **ready** once the service account is loaded and an API key is set. The [quick start](https://smaranjit.github.io/firebase-token-toolkit/quick-start.html) walks through it with screenshots.
+The status indicator next to the service account turns green and reads **ready** once the service account is loaded and the selected app has an API key. The [quick start](https://smaranjit.github.io/firebase-token-toolkit/quick-start.html) walks through it with screenshots.
## Build from source
@@ -89,7 +90,7 @@ To reproduce the packaged release artifacts, see [`scripts/`](https://github.com
This tool handles Firebase **service-account private keys** and mints signed tokens with them. Please read [SECURITY.md](https://github.com/smaranjit/firebase-token-toolkit/blob/main/SECURITY.md) before using it against anything that matters — it covers what is written to disk, what stays in memory, and how to report a vulnerability.
-The short version: the service account is read from the path you pick and never copied; OAuth access tokens live in memory only; API keys, App IDs and App Check debug tokens are written to disk only when you tick **remember API key & App ID**.
+The short version: the service account is read from the path you pick and never copied; OAuth access tokens live in memory only; API keys and App Check debug tokens are written to disk only when you tick **remember API keys & debug tokens**.
## Contributing
diff --git a/README.txt b/README.txt
index 9370bb2..171c766 100644
--- a/README.txt
+++ b/README.txt
@@ -34,13 +34,15 @@ SETTING IT UP
Project Settings > Service accounts > Generate new private key. The
project ID fills itself in from the file.
- 2. API key. Firebase Console > Project Settings > General > Web API Key.
- Only needed for the ID token and App Check tabs.
+ 2. Apps. Click Load apps to import the project's web, Android and iOS
+ apps with their API keys. Or click + Add app and paste an API key and
+ App ID from Firebase Console > Project Settings > General > Your apps.
- 3. App ID. Only needed for the App Check tab.
+ 3. Pick the app to use from the App list. Its API key is used by the
+ ID token and App Check tabs.
- The indicator in the top right turns green once the service account is
- loaded and an API key is set.
+ The indicator turns green once the service account is loaded and the
+ selected app has an API key.
IF NOTHING OPENS
diff --git a/SECURITY.md b/SECURITY.md
index f48bc25..258bc50 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -28,14 +28,16 @@ Persisted for every profile:
on demand and never copied into the config.
- Project ID, and the last-used tab.
- Window size and position.
+- Each app's name, type, App ID, Android package name and SHA-1, and iOS bundle
+ ID. These are public identifiers that ship inside every build of the app.
-Persisted **only when "remember API key & App ID" is enabled**:
+Persisted **only when "remember API keys & debug tokens" is enabled**:
-- Web API key, App ID, and App Check debug token.
+- Each app's API key and App Check debug token.
These are stored in **plaintext**. There is no OS keychain integration — the file is
protected by nothing but its filesystem permissions. If you would not paste the value
-into a plaintext file, leave "remember API key & App ID" off. Turning it off clears those fields
+into a plaintext file, leave "remember API keys & debug tokens" off. Turning it off clears those fields
for *all* profiles on the next save (`clear_secrets_all` in `src/config.rs`).
**Never persisted:** the service-account private key, OAuth access tokens, and any
@@ -59,6 +61,13 @@ All requests go to Google endpoints over TLS (`rustls` — no OpenSSL):
- `identitytoolkit.googleapis.com` — `signInWithCustomToken`, `accounts:batchGet`,
`accounts:lookup`, `accounts:update`
- `firebaseappcheck.googleapis.com` — `exchangeDebugToken`
+- `firebase.googleapis.com` — `searchApps` and each app's config and SHA
+ certificates, when you click **Load apps**. The responses include the apps'
+ API keys, which are held like any other API key above.
+
+Requests made with an Android or iOS app's API key carry that app's package
+name and SHA-1, or bundle ID, in the `X-Android-Package`, `X-Android-Cert` and
+`X-Ios-Bundle-Identifier` headers, as the Firebase SDKs do.
There is **no telemetry, analytics, or update check**. The application makes no
network request you did not initiate.
diff --git a/docs/src/configuration.md b/docs/src/configuration.md
index f39fc72..c707a82 100644
--- a/docs/src/configuration.md
+++ b/docs/src/configuration.md
@@ -2,6 +2,8 @@
Everything the tabs need comes from the top bar. Its values belong to the
active [profile](profiles.md), so switching profiles swaps them all at once.
+The first rows hold the profile and service account; the last two hold the
+selected [app](#apps).

@@ -26,9 +28,9 @@ right now:
| Indicator | Meaning | Tabs available |
|---|---|---|
-| **not configured** (red) | No service account loaded | *Custom -> ID Token* and *App Check* only, given an API key |
-| **SA only** (yellow) | Service account loaded, no API key | *UID -> Custom Token*, *User Custom Claims*, the Users panel |
-| **ready** (green) | Service account and API key | All tabs (*App Check* also needs an App ID) |
+| **not configured** (red) | No service account loaded | *Custom -> ID Token* and *App Check* only, given an app with an API key |
+| **SA only** (yellow) | Service account loaded, the selected app has no API key | *UID -> Custom Token*, *User Custom Claims*, the Users panel |
+| **ready** (green) | Service account, and the selected app has an API key | All tabs (*App Check* also needs the app's App ID) |
## Project ID
@@ -36,22 +38,46 @@ The Firebase project ID, for example `fir-token-toolkit-demo`. The Users panel,
User Custom Claims and App Check use it to build their requests. It normally
comes from the key file, but you can edit it.
-## API key
+## remember API keys & debug tokens
-The project's Web API key. The ID-token tabs and App Check send it to Google
-with each request. The field is masked.
-
-## App ID
-
-The web app's App ID, used only by the App Check tab.
-
-## remember API key & App ID
-
-Off by default. When ticked, the API key, App ID and App Check debug token are
-saved to disk in plaintext for **every** profile. When unticked, they are kept
-in memory only and cleared from every profile the next time the app saves. See
+Off by default. When ticked, every app's API key and App Check debug token are
+saved to disk in plaintext, for **every** profile. When unticked, they are kept
+in memory only and cleared from every profile the next time the app saves.
+App names, App IDs and the Android and iOS identifiers are always saved: they
+are not secret, and they ship inside every build of the app. See
[Settings and storage](reference/settings-storage.md).
+## Apps
+
+A profile holds a list of the project's apps, and the tabs that call Google
+with an API key (*UID -> ID Token*, *Custom -> ID Token* and *App Check*) use
+the one selected in the **App** dropdown.
+
+
+
+| Control | What it does |
+|---|---|
+| **App** dropdown | Selects the app, listed as `name (type)`. Switching clears the output of the three API-key tabs, but not the user list or the other tabs. |
+| **name** | Renames the selected app. |
+| **+ Add app** | Adds an empty **Web**, **Android** or **iOS** app and selects it. |
+| **Remove** | Removes the selected app. Removing the last one leaves an empty web app. |
+| **Load apps** | Imports the project's apps from Firebase. Needs the service account and Project ID. |
+| **Type** | Web, Android or iOS. Decides which identifying headers go with the API key. |
+| **API key** | The key sent with requests for this app. Masked. |
+| **App ID** | The app's Firebase App ID. Pasting one sets **Type** to match, and renames an app still carrying a default name such as *Web app*. |
+| **Package**, **SHA-1** | Android only: the package name and signing-certificate SHA-1. |
+| **Bundle ID** | iOS only. |
+
+**Load apps** matches apps on their App ID, so running it again updates the
+list rather than duplicating it. It refreshes each app's type, package, bundle
+ID and API key from Firebase, but keeps a name or SHA-1 you changed yourself.
+
+The **Package** and **SHA-1**, or **Bundle ID**, matter only for an API key
+restricted to an Android or iOS app; see
+[API keys restricted to an app](firebase-setup.md#api-keys-restricted-to-an-app).
+A SHA-1 that is not 40 hex digits (colons are fine) is marked *invalid* in
+yellow and is not sent.
+
## Selected UID
Once you pick a user in the [Users panel](guide/users-panel.md), a **Selected
diff --git a/docs/src/firebase-setup.md b/docs/src/firebase-setup.md
index 58fff91..d68ec91 100644
--- a/docs/src/firebase-setup.md
+++ b/docs/src/firebase-setup.md
@@ -1,9 +1,10 @@
# Preparing your Firebase project
-The app needs up to three things from your Firebase project. Which ones depend
-on the tabs you plan to use:
+The app needs up to three things from your Firebase project: a
+service-account key, the project ID, and the details of the apps you test
+with. Which ones depend on the tabs you plan to use:
-| Tab | Service-account key | Project ID | Web API key | App ID |
+| Tab | Service-account key | Project ID | App's API key | App ID |
|---|:-:|:-:|:-:|:-:|
| UID -> Custom Token | ✔ | for the Users panel | | |
| UID -> ID Token | ✔ | for the Users panel | ✔ | |
@@ -39,29 +40,63 @@ happens locally with the key and needs no extra role.
The app fills this in from the service-account file, so you rarely need to
type it. It is also shown under **Project settings** → **General**.
-## Web API key
-
-Under **Project settings** → **General**, copy **Web API key**. If your project
-has no web app yet, add one from the same page first.
-
-If you have restricted the key in Google Cloud Console, allow at least the
+## Apps: web, Android and iOS
+
+Each [profile](profiles.md) holds a list of the project's apps, and the tabs
+that need an API key use the one you select. Any app registered in the
+project works, whatever its platform.
+
+**The quick way:** with the service-account key loaded, click **Load apps** in
+the top bar. The app reads every web, Android and iOS app in the project
+through the Firebase Management API and fills in each app's App ID, API key,
+package name and SHA-1 (Android) or bundle ID (iOS). See
+[The top bar](configuration.md#apps).
+
+The service account needs to be allowed to read Firebase apps
+(`firebase.clients.list` and `firebase.clients.get`). The key the console
+generates already is; for another account, the **Firebase Viewer** role
+(`roles/firebase.viewer`) covers it.
+
+**By hand:** under **Project settings** → **General** → **Your apps**, each app
+shows its **App ID**, for example `1:316659987175:android:b9522e663c7b203d12a842`.
+Its API key is in the config file offered there (`google-services.json`,
+`GoogleService-Info.plist`, or the web snippet's `apiKey`). The project-wide
+**Web API Key** on the same page also works for any app, as long as it has no
+application restriction.
+
+### API keys restricted to an app
+
+Google Cloud lets you restrict a key to particular Android apps (package name
+plus signing-certificate SHA-1) or iOS apps (bundle ID). Google accepts such a
+key only from a request that names the app, the way the Firebase SDKs do. The
+app sends those details for you, taken from the selected app:
+
+| App type | Sent with the request |
+|---|---|
+| Web | the API key only |
+| Android | `X-Android-Package` (package name) and `X-Android-Cert` (SHA-1) |
+| iOS | `X-Ios-Bundle-Identifier` (bundle ID) |
+
+So for a restricted key, make sure the app's **Package** and **SHA-1**, or
+**Bundle ID**, match one of the entries in the key's restriction. **Load apps**
+fills them from Firebase, using the first SHA-1 certificate registered for an
+Android app; if the key allows a different certificate, paste that one in.
+
+If you have restricted which **APIs** a key may call, allow at least the
**Identity Toolkit API**, and the **Firebase App Check API** if you use the
App Check tab.
-## App ID (App Check only)
-
-Under **Project settings** → **General** → **Your apps**, copy the **App ID** of
-the web app, which looks like `1:316659987175:web:ebcef8864368ab1a12a842`.
-
## App Check debug token (App Check only)
-The App Check tab exchanges a *debug token* you have registered for the app:
+The App Check tab exchanges a *debug token* you have registered for the app.
+This works the same for web, Android and iOS apps:
1. Register the app with App Check under **App Check** → **Apps** if you have
not already.
2. Open the app's overflow menu (⋮) and choose **Manage debug tokens**.
3. Click **Add debug token**, give it a name, and either generate a token or
- paste one your client printed. Copy the value; it is a UUID.
+ paste one your client printed (the browser console on the web, Logcat on
+ Android, the Xcode console on iOS). Copy the value; it is a UUID.
Treat the debug token like a password. Anyone holding it can get valid App
Check tokens for your app.
diff --git a/docs/src/guide/app-check.md b/docs/src/guide/app-check.md
index 5df1ee4..53595d1 100644
--- a/docs/src/guide/app-check.md
+++ b/docs/src/guide/app-check.md
@@ -5,12 +5,13 @@ for a real App Check token. Use it to call App Check–protected backends, such
as Cloud Functions or your own server, from scripts or API tools that cannot
run the App Check SDK.
-**Needs:** the Project ID, App ID and Web API key from the top bar. No service
-account is required.
+**Needs:** the Project ID, and an [app](../configuration.md#apps) with an App
+ID and API key. No service account is required. It works for web, Android and
+iOS apps; the tab names the app it will use.
## Before you start
-Register a debug token for your web app in the Firebase console under **App
+Register a debug token for the app in the Firebase console under **App
Check** → **Apps** → ⋮ → **Manage debug tokens**. The steps are in [Preparing
your Firebase project](../firebase-setup.md#app-check-debug-token-app-check-only).
@@ -38,9 +39,10 @@ configured for a different project rejects the token.
## Saving the debug token
-The debug token belongs to the profile, but like the API key it is saved
-between launches only when **remember API key & App ID** is ticked. Otherwise
-you paste it again after restarting.
+Each app keeps its own debug token, so switching apps switches tokens too. Like
+API keys, debug tokens are saved between launches only when **remember API
+keys & debug tokens** is ticked. Otherwise you paste it again after
+restarting.
## When the exchange fails
diff --git a/docs/src/guide/custom-claims.md b/docs/src/guide/custom-claims.md
index 33f1401..43ab48d 100644
--- a/docs/src/guide/custom-claims.md
+++ b/docs/src/guide/custom-claims.md
@@ -39,7 +39,7 @@ Firebase rejects claims larger than 1,000 bytes. A counter under the editor
measures the claims compactly serialized, which is how Firebase counts them, so
whitespace from **Pretty-print** does not count:
-- grey `29 / 1000 bytes serialized`: within the limit,
+- grey `39 / 1000 bytes serialized`: within the limit,
- red `1040 / 1000 bytes (Firebase will reject)`: too large; **Save** refuses
with an error,
- yellow `(invalid JSON — fix before saving)`.
@@ -52,9 +52,10 @@ suit roles and flags rather than profile data.
## Removing all claims
Saving an empty editor is refused on purpose, to avoid wiping claims by
-accident. To remove everything, click **Clear all claims**. It sends an empty
-`customAttributes` value, and the message reads *Saved — all custom claims
-cleared.*
+accident. To remove everything, click **Clear all claims**. It saves an empty
+claims object (`{}`), and the message reads *Saved — all custom claims
+cleared.* Loading a user whose claims are `{}` shows an empty editor and *Loaded
+— user has no custom claims set.*
## When changes take effect
diff --git a/docs/src/guide/custom-to-id-token.md b/docs/src/guide/custom-to-id-token.md
index f0dc052..b7b43c5 100644
--- a/docs/src/guide/custom-to-id-token.md
+++ b/docs/src/guide/custom-to-id-token.md
@@ -5,15 +5,16 @@ Exchanges a custom token you already have for an ID token, by calling
somewhere else, such as by your own backend, or one copied from the
[UID -> Custom Token](uid-to-custom-token.md) tab.
-**Needs:** the Web API key. No service account is required, so this tab works
-in a profile that only has an API key.
+**Needs:** an [app](../configuration.md#apps) with an API key. No service
+account is required, so this tab works in a profile that only has an app's
+key.
## Exchanging a token
1. Paste the token into **Custom token**.
2. Click **Exchange**.
-
+
The result shows the ID token with a **Copy** button, **Expires in**, and the
decoded claims. Claims carried in the custom token, such as `tier` above, show
diff --git a/docs/src/guide/reading-results.md b/docs/src/guide/reading-results.md
index 4ea84a1..2bbdcf1 100644
--- a/docs/src/guide/reading-results.md
+++ b/docs/src/guide/reading-results.md
@@ -16,7 +16,8 @@ text in the box and copy it with Ctrl+C
## Decoded JWT claims
Click **Decoded JWT claims** to expand a table of everything in the token's
-payload, sorted by name. Values are selectable for copying.
+payload, sorted by name. Values are selectable for copying. Scroll the tab to
+reach the rest of a long table.
Timestamps are shown twice, as the raw Unix time and as UTC, for example
`1791308692 (2026-10-06T17:44:52+00:00)`. That applies to `iat` (issued at),
diff --git a/docs/src/guide/uid-to-id-token.md b/docs/src/guide/uid-to-id-token.md
index 631e859..4d465fa 100644
--- a/docs/src/guide/uid-to-id-token.md
+++ b/docs/src/guide/uid-to-id-token.md
@@ -4,7 +4,9 @@ Gets a Firebase ID token for the selected user in one click. The app signs a
custom token locally, exchanges it with Google through `signInWithCustomToken`,
and shows the resulting ID token.
-**Needs:** a service account, the Web API key, and a selected user.
+**Needs:** a service account, an [app](../configuration.md#apps) with an API
+key, and a selected user. The exchange uses the selected app's key, so you can
+check that an Android or iOS app's restricted key works.
## Generating a token
diff --git a/docs/src/images/about-dialog.png b/docs/src/images/about-dialog.png
index d2714f2..f865c35 100644
Binary files a/docs/src/images/about-dialog.png and b/docs/src/images/about-dialog.png differ
diff --git a/docs/src/images/app-check-result.png b/docs/src/images/app-check-result.png
index 0ee5c32..7379f53 100644
Binary files a/docs/src/images/app-check-result.png and b/docs/src/images/app-check-result.png differ
diff --git a/docs/src/images/apps-dropdown.png b/docs/src/images/apps-dropdown.png
new file mode 100644
index 0000000..d0bd5e9
Binary files /dev/null and b/docs/src/images/apps-dropdown.png differ
diff --git a/docs/src/images/claims-loaded.png b/docs/src/images/claims-loaded.png
index 5bbfe91..8e0f5da 100644
Binary files a/docs/src/images/claims-loaded.png and b/docs/src/images/claims-loaded.png differ
diff --git a/docs/src/images/claims-over-limit.png b/docs/src/images/claims-over-limit.png
index ae63595..2491e04 100644
Binary files a/docs/src/images/claims-over-limit.png and b/docs/src/images/claims-over-limit.png differ
diff --git a/docs/src/images/claims-saved.png b/docs/src/images/claims-saved.png
index d5e99b4..e791098 100644
Binary files a/docs/src/images/claims-saved.png and b/docs/src/images/claims-saved.png differ
diff --git a/docs/src/images/custom-to-id-result.png b/docs/src/images/custom-to-id-result.png
index 3809435..7145709 100644
Binary files a/docs/src/images/custom-to-id-result.png and b/docs/src/images/custom-to-id-result.png differ
diff --git a/docs/src/images/first-run.png b/docs/src/images/first-run.png
index b47a2e8..7ccda22 100644
Binary files a/docs/src/images/first-run.png and b/docs/src/images/first-run.png differ
diff --git a/docs/src/images/lookup-not-found.png b/docs/src/images/lookup-not-found.png
index 0329263..f5652e6 100644
Binary files a/docs/src/images/lookup-not-found.png and b/docs/src/images/lookup-not-found.png differ
diff --git a/docs/src/images/profiles-dropdown.png b/docs/src/images/profiles-dropdown.png
index 1bc705d..e960799 100644
Binary files a/docs/src/images/profiles-dropdown.png and b/docs/src/images/profiles-dropdown.png differ
diff --git a/docs/src/images/quick-start.gif b/docs/src/images/quick-start.gif
index b9fdfc8..81f5984 100644
Binary files a/docs/src/images/quick-start.gif and b/docs/src/images/quick-start.gif differ
diff --git a/docs/src/images/service-account-loaded.png b/docs/src/images/service-account-loaded.png
index 7e62b78..518f39b 100644
Binary files a/docs/src/images/service-account-loaded.png and b/docs/src/images/service-account-loaded.png differ
diff --git a/docs/src/images/top-bar-android.png b/docs/src/images/top-bar-android.png
new file mode 100644
index 0000000..5b85604
Binary files /dev/null and b/docs/src/images/top-bar-android.png differ
diff --git a/docs/src/images/top-bar-ready.png b/docs/src/images/top-bar-ready.png
index dfe35dc..65a68fe 100644
Binary files a/docs/src/images/top-bar-ready.png and b/docs/src/images/top-bar-ready.png differ
diff --git a/docs/src/images/uid-to-custom-form.png b/docs/src/images/uid-to-custom-form.png
index 42231b7..72fcee8 100644
Binary files a/docs/src/images/uid-to-custom-form.png and b/docs/src/images/uid-to-custom-form.png differ
diff --git a/docs/src/images/uid-to-custom-result.png b/docs/src/images/uid-to-custom-result.png
index 5fbf4f3..d62b8a4 100644
Binary files a/docs/src/images/uid-to-custom-result.png and b/docs/src/images/uid-to-custom-result.png differ
diff --git a/docs/src/images/uid-to-id-result.png b/docs/src/images/uid-to-id-result.png
index 5f4aa3f..e7e3766 100644
Binary files a/docs/src/images/uid-to-id-result.png and b/docs/src/images/uid-to-id-result.png differ
diff --git a/docs/src/images/users-filtered.png b/docs/src/images/users-filtered.png
index 0af6882..8549293 100644
Binary files a/docs/src/images/users-filtered.png and b/docs/src/images/users-filtered.png differ
diff --git a/docs/src/images/users-loaded.png b/docs/src/images/users-loaded.png
index f304ba2..85cd057 100644
Binary files a/docs/src/images/users-loaded.png and b/docs/src/images/users-loaded.png differ
diff --git a/docs/src/introduction.md b/docs/src/introduction.md
index 74cd6d8..335cfe1 100644
--- a/docs/src/introduction.md
+++ b/docs/src/introduction.md
@@ -5,7 +5,7 @@ you need while developing and testing a backend. Pick a user, click
**Generate**, and you have a real ID token for that user, with its claims
decoded underneath and a **Copy** button next to it.
-
+
It covers five jobs, each on its own tab:
@@ -20,7 +20,9 @@ It covers five jobs, each on its own tab:
A [Users panel](guide/users-panel.md) on the left lists the project's users and
searches them by email, phone or UID, so you rarely need to look a UID up by
hand. [Profiles](profiles.md) keep the settings for several Firebase projects
-side by side.
+side by side, and each profile can hold the project's
+[web, Android and iOS apps](configuration.md#apps), including API keys that are
+restricted to one app.
The app is a single native binary for Linux, Windows and macOS. It talks only to
Google's endpoints, only when you click something, and never writes your
@@ -29,8 +31,9 @@ private key or the tokens it mints to disk.
## Where to start
1. [Install the app](installation.md).
-2. [Collect what you need from your Firebase project](firebase-setup.md): a
- service-account key and the Web API key.
+2. [Collect what you need from your Firebase project](firebase-setup.md):
+ usually just a service-account key. The app can read the project's web,
+ Android and iOS apps itself.
3. Follow the [Quick start](quick-start.md) to your first ID token.
> **Use a development project.** A service-account key can sign in as any user
diff --git a/docs/src/profiles.md b/docs/src/profiles.md
index 65bd989..5833f71 100644
--- a/docs/src/profiles.md
+++ b/docs/src/profiles.md
@@ -1,7 +1,8 @@
# Profiles
-A profile holds one project's settings: service-account path, Project ID, API
-key, App ID and App Check debug token. Keep one per Firebase project, for
+A profile holds one project's settings: service-account path, Project ID, and
+the project's [apps](configuration.md#apps) with their API keys and App Check
+debug tokens. Keep one per Firebase project, for
example *Dev*, *Staging* and *Production*, and switch between them from the
**Profile** dropdown.
@@ -34,7 +35,7 @@ belongs to the profile shown in the dropdown.
## Saving
-Profile names, service-account paths and Project IDs are always saved. API
-keys, App IDs and debug tokens are saved only while **remember API key & App
-ID** is ticked, and that checkbox applies to every profile, not just the active
-one. See [Settings and storage](reference/settings-storage.md).
+Profile names, service-account paths, Project IDs and app lists are always
+saved. API keys and debug tokens are saved only while **remember API keys &
+debug tokens** is ticked, and that checkbox applies to every profile, not just
+the active one. See [Settings and storage](reference/settings-storage.md).
diff --git a/docs/src/quick-start.md b/docs/src/quick-start.md
index 036c09c..c15a3b5 100644
--- a/docs/src/quick-start.md
+++ b/docs/src/quick-start.md
@@ -1,9 +1,9 @@
# Quick start
This walk-through goes from a fresh install to an ID token for one of your
-users. You need a service-account key file and the project's Web API key; see
-[Preparing your Firebase project](firebase-setup.md) if you do not have them
-yet.
+users. You need a service-account key file; see
+[Preparing your Firebase project](firebase-setup.md) if you do not have one
+yet. The app reads everything else from the project.
## 1. Open the app
@@ -24,14 +24,17 @@ message confirms it; click **dismiss** to hide it.

-## 3. Enter the Web API key
+## 3. Load the project's apps
-Paste the Web API key into **API key**. The field is masked. The indicator
-turns green and reads **ready**.
+Click **Load apps**. The app lists the project's web, Android and iOS apps and
+fills in each one's App ID and API key. Pick the app you want to test as from
+the **App** dropdown; the indicator turns green and reads **ready**.
-If you plan to use the App Check tab, paste the web app's **App ID** too.
+
-
+If the service account is not allowed to read apps, enter an app by hand
+instead: **+ Add app**, choose its type, and paste its **API key** and
+**App ID**. See [The top bar](configuration.md#apps).
## 4. Load your users
@@ -62,9 +65,9 @@ curl -H "Authorization: Bearer " https://localhost:8080/api/me
## 6. Keep the setup for next time
-The service-account path and Project ID are saved automatically. The API key
-and App ID are saved only if you tick **remember API key & App ID**; without
-it, you paste them again on each launch. See
+The service-account path, Project ID and app list are saved automatically.
+API keys are saved only if you tick **remember API keys & debug tokens**;
+without it, click **Load apps** again after each launch, or paste the key. See
[Settings and storage](reference/settings-storage.md) for what that writes to
disk.
diff --git a/docs/src/reference/endpoints.md b/docs/src/reference/endpoints.md
index a2c6d41..a5e6941 100644
--- a/docs/src/reference/endpoints.md
+++ b/docs/src/reference/endpoints.md
@@ -8,11 +8,27 @@ browser; nothing else leaves the app.
| Action in the app | Request | Authenticated with |
|---|---|---|
| **Generate** on *UID -> Custom Token* | None. The token is signed locally. | — |
-| **Generate** on *UID -> ID Token*, **Exchange** on *Custom -> ID Token* | `POST identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken` | Web API key |
+| **Generate** on *UID -> ID Token*, **Exchange** on *Custom -> ID Token* | `POST identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken` | Selected app's API key |
| **Load users** / **Refresh** | `GET identitytoolkit.googleapis.com/v1/projects/{project}/accounts:batchGet` (1,000 per page, up to 5,000) | OAuth access token |
| **Lookup**, **Load current** | `POST identitytoolkit.googleapis.com/v1/projects/{project}/accounts:lookup` | OAuth access token |
| **Save**, **Clear all claims** | `POST identitytoolkit.googleapis.com/v1/projects/{project}/accounts:update`, then a lookup to read the result back | OAuth access token |
-| **Exchange** on *App Check* | `POST firebaseappcheck.googleapis.com/v1beta/projects/{project}/apps/{app}:exchangeDebugToken` | Web API key |
+| **Exchange** on *App Check* | `POST firebaseappcheck.googleapis.com/v1beta/projects/{project}/apps/{app}:exchangeDebugToken` | Selected app's API key |
+| **Load apps** | `GET firebase.googleapis.com/v1beta1/projects/{project}:searchApps`, then for each app `GET …/webApps/{app}/config`, `…/androidApps/{app}/config` and `…/androidApps/{app}/sha`, or `…/iosApps/{app}/config` | OAuth access token |
+
+## API keys and app identity
+
+Requests authenticated with an API key send it as the `key` query parameter.
+For an Android or iOS app, the request also names the app, so that keys
+restricted to that app are accepted:
+
+| App type | Headers |
+|---|---|
+| Web | none |
+| Android | `X-Android-Package: `, `X-Android-Cert: ` |
+| iOS | `X-Ios-Bundle-Identifier: ` |
+
+A header is left out when its value is empty, and a SHA-1 that is not 40 hex
+digits is not sent.
## OAuth access tokens
diff --git a/docs/src/reference/settings-storage.md b/docs/src/reference/settings-storage.md
index c34166b..d2bc712 100644
--- a/docs/src/reference/settings-storage.md
+++ b/docs/src/reference/settings-storage.md
@@ -19,17 +19,20 @@ The file is written when the app closes and periodically while it runs.
| Project ID | Always | |
| Active profile, last-used tab | Always | |
| Window size and position | Always | |
-| Web API key | Only with **remember API key & App ID** | Plaintext |
-| App ID | Only with **remember API key & App ID** | Plaintext |
-| App Check debug token | Only with **remember API key & App ID** | Plaintext |
+| Each app's name, type and App ID | Always | |
+| Android package and SHA-1, iOS bundle ID | Always | Public identifiers, included in every app build |
+| Selected app | Always | |
+| Each app's API key | Only with **remember API keys & debug tokens** | Plaintext |
+| Each app's App Check debug token | Only with **remember API keys & debug tokens** | Plaintext |
| Service-account private key | Never | Re-read from the saved path at every launch |
| OAuth access tokens | Never | Held in memory, refreshed automatically |
| Minted custom, ID and App Check tokens | Never | |
-The **remember API key & App ID** checkbox applies to every profile at once.
-When it is off, the app clears the API key, App ID and debug token from all
-profiles when it starts and again every time it saves, so unticking it also
-removes values that were saved earlier.
+The **remember API keys & debug tokens** checkbox applies to every profile at
+once. When it is off, the app clears every app's API key and debug token, in
+all profiles, when it starts and again every time it saves, so unticking it
+also removes values that were saved earlier. Clicking **Load apps** fetches the
+keys again.
If the service-account file has moved or been deleted, the app starts with no
service account loaded and the status indicator shows `● not configured`. Use
@@ -40,6 +43,14 @@ service account loaded and the status indicator shows `● not configured`. Use
Close the app and delete `app.ron`. The next launch starts with a single empty
profile named `Default`.
+## Upgrading from 0.1.x
+
+Version 0.1.x stored one API key, App ID and debug token per profile. The first
+launch of a newer version turns them into the profile's first app, named
+after its type (for example *Web app*) and with its type taken from the App ID.
+If **remember** was off, 0.1.x saved none of the three, so the profile starts
+with one empty web app; **Load apps** fills in the project's apps.
+
## Upgrading from early versions
Versions before profiles existed stored a single set of fields at the top level
diff --git a/docs/src/security.md b/docs/src/security.md
index dd6dccb..f8100ef 100644
--- a/docs/src/security.md
+++ b/docs/src/security.md
@@ -12,8 +12,10 @@ version for day-to-day use.
is never copied into the settings file.
- **OAuth access tokens** and every **token shown on screen** live in memory
only.
-- The **API key, App ID and App Check debug token** are written to disk, in
- plaintext, only when **remember API key & App ID** is ticked. See
+- **API keys and App Check debug tokens** are written to disk, in plaintext,
+ only when **remember API keys & debug tokens** is ticked. App IDs, package
+ names, SHA-1s and bundle IDs are always saved; they are public, since every
+ build of your app contains them. See
[Settings and storage](reference/settings-storage.md).
## Habits worth keeping
diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md
index 82fb86d..4f9e44b 100644
--- a/docs/src/troubleshooting.md
+++ b/docs/src/troubleshooting.md
@@ -49,12 +49,15 @@ carries on with wgpu.
| Message | Cause and fix |
|---|---|
-| `API key required` | Enter the Web API key in the top bar. |
+| `API key required` | The selected app has no API key. Click **Load apps**, or paste the key. |
| `API error (400): INVALID_CUSTOM_TOKEN : Invalid assertion format. 3 dot separated segments required.` | The pasted text is not a whole token. It usually lost characters while being copied. |
| `API error (400): INVALID_CUSTOM_TOKEN …` with other text | The token has expired (custom tokens last one hour) or is malformed. Generate a new one. |
| `API error (400): CREDENTIAL_MISMATCH` | The custom token was signed with a key from a different project than the API key belongs to. Check that the profile's key and API key are from the same project. |
| `API error (400): API key not valid. Please pass a valid API key.` | The API key is wrong or deleted. Copy it again from **Project settings** → **General**. |
-| `API error (403): … are blocked.` | The API key has restrictions that exclude the Identity Toolkit API or the App Check API. Loosen them in Google Cloud Console. |
+| `API error (403): Requests from this Android client application are blocked.` (or the iOS equivalent) | The key is restricted to an Android or iOS app, but the selected app is a different type, or its **Package** or **Bundle ID** is empty. Select the matching app, or fill in its identifiers. |
+| `API error (403): Requests from this Android client application dev.example.app are blocked.` | The package or SHA-1 does not match the key's restriction. Check both against the key in Google Cloud Console → **Credentials**. A SHA-1 marked *invalid* in the top bar is not sent. |
+| `API error (403): Requests from this iOS client application are blocked.` | The bundle ID does not match the key's restriction. |
+| `API error (403): … are blocked.` (other) | The API key has restrictions that exclude the Identity Toolkit API or the App Check API. Loosen them in Google Cloud Console. |
| `Invalid claims JSON: …` / `custom claims must be a JSON object` | The optional claims box must hold a JSON object, such as `{"role":"admin"}`, or be empty. |
| `Paste a custom token first` | The *Custom -> ID Token* input is empty. |
@@ -76,6 +79,14 @@ carries on with wgpu.
| `API error (403): …` with other text | The App Check API is not enabled for the project, or the API key's restrictions exclude it. |
| `API error (404): …` | The App ID does not exist in the project named in **Project ID**. |
+## Load apps
+
+| Message | Cause and fix |
+|---|---|
+| `Loading apps failed: API error (403): …` | The service account may not read Firebase apps, or the Firebase Management API is disabled. Grant **Firebase Viewer** (`roles/firebase.viewer`), or add the app by hand with **+ Add app**. |
+| `No apps found in this Firebase project.` | The project has no registered apps. Add one under **Project settings** → **General** → **Your apps**. |
+| `Loaded apps: … Some details are missing — …` | The list loaded, but one app's config or certificates could not be read. That app is listed without its key; fill it in by hand. |
+
## Linux desktop issues
**Browse… does nothing.** The file chooser goes through an XDG desktop
@@ -88,6 +99,8 @@ pure Wayland session without XWayland, select the token text and copy it with
## About dialog links return 404
+
+
The **Changelog** and **Security notes** links in the About dialog point at
the git tag matching the app's version. A build from an untagged commit has no
such tag, so the links 404. Release builds are not affected.
diff --git a/src/app.rs b/src/app.rs
index f2b00fb..e9961a7 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5,7 +5,7 @@ use tokio::runtime::Handle;
use tokio::sync::Mutex;
use crate::async_task::AsyncTask;
-use crate::config::PersistedConfig;
+use crate::config::{PersistedConfig, RemoteApp};
use crate::firebase::oauth::AccessToken;
use crate::firebase::service_account::ServiceAccount;
use crate::firebase::HttpClient;
@@ -68,6 +68,10 @@ pub struct SharedState {
pub selected_user_label: Option,
pub status_message: Option<(StatusKind, String)>,
pub requested_profile_switch: Option,
+ /// Index into the active profile's apps, applied at the top of next frame.
+ pub requested_app_switch: Option,
+ /// In-flight "Load apps" import from the Firebase Management API.
+ pub apps_import: AsyncTask>>,
/// In-flight service-account file picker. Async so the dialog's D-Bus round
/// trip does not block the render thread.
pub file_dialog: AsyncTask