Skip to content

Admin user operations ​

Corrective actions against a user's Firebase Auth record β€” locked out, wrong email, unreachable phone, needs a reset β€” go through the agri-admin panel. It is the primary path: every action is guarded by platformRoles: super_admin, requires deliberate confirmation, and is persisted to an audit trail. Reach for the break-glass script at the bottom of this page only when the backend or the panel itself is unavailable.

Primary path: the agri-admin panel ​

Route: /data/users/:id, reached via the "Manage auth" row action on the Users list (/data/users) β€” sits between "Impersonate" and "Delete".

The panel covers:

  • View auth record β€” live Firebase state (uid, email, phone, emailVerified, disabled, providers, last sign-in, created-at) shown next to the Mongo User record, with any divergence between the two called out explicitly.
  • Send password-reset link β€” generates a Firebase reset link and emails it to the address on file.
  • Set password directly β€” the fallback for users who can't receive a reset link (no working email/phone), or who need access immediately.
  • Change email
  • Change phone number
  • Set emailVerified
  • Disable / enable account (Firebase sign-in gate β€” independent from Tellia's own isActive lifecycle flag)
  • Force sign-out (revoke refresh tokens)

Every one of these is super-admin guarded and persisted to EntityChangeLog as entityType: 'userAuth', with a non-secret projection of what changed (never the password itself, never a reset link). That record is queryable in the admin audit UI β€” from a user's "Manage auth" page, or directly at /data/userAuth/:id/audit β€” so "who changed this user's auth and when" is always answerable without grepping logs.

The ~1h token caveat ​

The panel states this verbatim, next to Force sign-out, in the confirm dialog, and in the success toast β€” it is not decoration, it is the actual mitigation for this feature's biggest gap:

Sessions already open on their device(s) can stay signed in for up to an hour after this β€” Firebase doesn't revoke issued tokens instantly.

Why: FirebaseService.verifyIdToken (apps/agri-backend/src/firebase/firebase.service.ts:101) calls admin.auth().verifyIdToken(idToken) without the checkRevoked option. Both "Disable account" and "Force sign-out" block new sign-ins (a fresh sign-in attempt fails, and revokeRefreshTokens stops a refresh token from minting a new ID token) β€” but neither invalidates an ID token that was already issued and is still cached on a device. That token keeps working against the API until it naturally expires, up to an hour later. There is no tool-level fix for this today: turning on checkRevoked: true would add a Firebase round-trip to every authenticated request across mobile and the PWA, which is an infra/latency/cost decision, not something this feature can flip on unilaterally. If you are responding to an incident (compromised account, urgent lockout), assume the old token is live for up to an hour and treat "force sign-out" / "disable" as containment, not an instant kill switch.

Two behavioural deltas vs. the old script ​

If you have used set-firebase-password.ts before, the panel does not behave identically:

  1. Session revocation on password set. The panel always revokes the user's sessions when a password is set β€” this is forced on, not a checkbox, precisely because a browser button is a lower-friction action than a TTY script and deserves the safer default. The script's --revoke-sessions flag, by contrast, defaults off (apps/agri-backend/scripts/set-firebase-password.ts:196: if (!args.revokeSessions) { ... } prints a reminder that existing sessions are still valid) β€” you must opt in explicitly.
  2. Visibility of the credential. The panel's "Send reset link" action never shows the link to the operator β€” it is generated and emailed straight to the address on file, never returned in the API response, never logged, never persisted. The script has no equivalent "send a link" mode at all: it only supports setting a password directly, and when it does, it prints the new password to the operator's terminal (console.log(' new password : ${password}')) so they can relay it themselves. That is the same shape as the panel's own "Set password directly" fallback (which also reveals the password once, for the admin to copy) β€” the point to remember is that the script has no "reset link" concept to compare against; it is always the direct-set path.

Break-glass fallback: set-firebase-password.ts ​

Use this script only when the backend API or the agri-admin panel is unavailable (outage, deploy in progress) and the fix cannot wait. It bypasses the panel's audit trail β€” no EntityChangeLog entry is written, so any use must be logged manually in the incident channel (who, which user, when, why) to avoid a second, undocumented audit trail existing alongside the real one.

One-off, human-run backend script for setting a Firebase Auth user's password directly through the Admin SDK β€” for cases the panel and the Firebase Console can't handle. Lives in apps/agri-backend/scripts/ and runs against a real environment, so treat it as a production operation: know the target, prefer the dry run, and leave a trace in the channel where you deliver any secret.

Set a user's password (no email sent) ​

Use when a user has lost access to their email and can't receive a password-reset link, and the panel itself is not reachable. The Firebase Console can only send a reset email or disable a user β€” it cannot set a password. This script sets it in place via admin.auth().updateUser().

Script: scripts/set-firebase-password.ts Β· npm script: set:firebase-password

1. Load credentials ​

The script reuses the same three env vars the backend uses. Pull them from the 1Password [pro] Firebase - Web App Config item (fields FIREBASE_CLIENT_EMAIL / FIREBASE_PRIVATE_KEY):

bash
export FIREBASE_PROJECT=tell-ia-production
export FIREBASE_CLIENT_EMAIL="$(op item get '[pro] Firebase - Web App Config' --fields label=FIREBASE_CLIENT_EMAIL)"
export FIREBASE_PRIVATE_KEY="$(op item get '[pro] Firebase - Web App Config' --fields label=FIREBASE_PRIVATE_KEY --reveal)"

FIREBASE_PRIVATE_KEY is a concealed field β€” the --reveal flag is required, or op returns a masked value and the SDK fails with Invalid PEM formatted message. The key is stored with escaped \n; the script un-escapes them.

Application Default Credentials (gcloud auth application-default login) also work only if your user holds roles/firebaseauth.admin on the project. If you hit insufficient permission to access the requested resource, either grant that role or fall back to the service-account key above (that's the reliable path β€” the backend runs with it).

2. Dry run β€” confirm the target ​

Prints the user (email, name, project) and changes nothing:

bash
pnpm --filter agri-backend set:firebase-password -- --uid <UID>

Verify the email and project are correct before applying.

3. Apply ​

bash
# generated strong password, printed once
pnpm --filter agri-backend set:firebase-password -- --uid <UID> --yes

# your own password β€” via env var (not a CLI flag: argv leaks through shell
# history and `ps`/`/proc/<pid>/cmdline`)
FIREBASE_NEW_PASSWORD='<PW>' pnpm --filter agri-backend set:firebase-password -- --uid <UID> --yes

Alternatively, run with --yes on a TTY without setting FIREBASE_NEW_PASSWORD and the script shows a hidden prompt: type a password there, or press Enter to generate a strong one.

Deliver the password over a secure channel (not plaintext email/Slack) β€” unlike the panel's reset-link flow, this script always puts the credential in front of the operator, so the operator is the one responsible for getting it to the user safely.

Flags & safety ​

Flag / envEffect
(none)Dry run β€” looks up and prints the user, makes no change
--yes / --confirmActually applies the change
FIREBASE_NEW_PASSWORDEnv var to set a specific password (else a strong one is generated); on a TTY a hidden prompt is offered instead
--revoke-sessionsAlso revoke refresh tokens, forcing re-login on all devices β€” off by default, unlike the panel
  • Existing sessions stay valid by default β€” the password change alone doesn't sign the user out. Add --revoke-sessions if you need that (the panel does this for you, unconditionally).
  • The script refuses to run without an explicit FIREBASE_PROJECT (guards against a local/misconfigured target).
  • No EntityChangeLog entry is written by this script β€” log the action manually where you deliver the password.

Mint a super-admin API key ​

For binding an API key to an existing super_admin user (e.g. the MCP server). Runs against staging/prod Mongo directly; keys expire in 30 days by default.

Script: scripts/mint-superadmin-api-key.ts Β· npm script: mint:superadmin-key

bash
pnpm --filter agri-backend mint:superadmin-key -- \
  --name "claude-mcp" \
  --as-user vincent@tell-ia.com \
  [--expires-in-days 30]

Never promotes users β€” it only binds a key to a user who already holds the role. Always pair with an audit note where the key is delivered.