skip to content

In a Flutter app using firebase_auth, how do you attach the user's ID token to your own backend calls, and when does getIdToken return a refreshed token?

level: seniorimportance: should knowfreq 42%

answer

  1. a bearer JWT for your own API
  2. cached until it expires
  3. forceRefresh defaults to false
  4. claims arrive only in a new token
  5. client-side claims are untrusted

basics

~20 s

Call await user.getIdToken() for each request and send it as a bearer token. It returns the cached token until expiry, then refreshes automatically; getIdToken(true) or getIdTokenResult(true) forces a refresh, which is how new custom claims reach the client.

solid answer

~40 s

`User.getIdToken([bool forceRefresh = false])` returns `Future<String?>`: the current ID token if it has not expired, otherwise a refreshed one. So you call it per request — typically in an HTTP interceptor — and send `Authorization: Bearer <token>`, instead of caching the string at sign-in. `forceRefresh: true` refreshes regardless of expiry and costs a network round trip, so you use it only when claims changed, for example right after a backend grants a moderator role; `getIdTokenResult(true)` then exposes the new `claims`. `idTokenChanges()` fires when the token changes, for components that must react rather than ask. The `IdTokenResult` source warns its claims are parsed client side and not to be trusted, so the backend still verifies the token and checks the claim on every privileged call.

code

dart · 10 lines
dart
import 'package:firebase_auth/firebase_auth.dart';

Future<bool> refreshModeratorFlag() async {
  final User? user = FirebaseAuth.instance.currentUser;
  if (user == null) return false;
  // Called right after the backend granted the role: force a new token.
  final IdTokenResult result = await user.getIdTokenResult(true);
  // UI hint only: the backend re-checks the claim on every request.
  return result.claims?['moderator'] == true;
}

go deeper

for a junior

Know that getIdToken() gives the token to send as a bearer header and that the SDK refreshes it for you.

for a middle

Explain the cached-until-expiry behaviour, what forceRefresh changes and costs, and the difference between getIdToken and getIdTokenResult.

for a senior

Design the request path around per-call tokens, force a refresh after role changes, and keep authorization on the server despite client-side claims.

for a principal

Decide where tokens are fetched and refreshed across the app's clients so no layer caches credentials or trusts client-side claims for access control.

## Why a Flutter app reads the ID token at all Firebase services such as Firestore attach the signed-in user's identity for you. Your **own** backend — say, the recipe-sharing app's image-moderation service — does not get that for free. The usual pattern is to send the user's Firebase **ID token**, a signed JWT, as a bearer token on each request and have the server verify it. Server-side verification and the token's lifecycle belong to the Firebase Auth product; the client-side question is how to obtain a valid token and when it changes. ## `getIdToken` and `getIdTokenResult` Both live on the `User` object: ```dart Future<String?> getIdToken([bool forceRefresh = false]); Future<IdTokenResult> getIdTokenResult([bool forceRefresh = false]); ``` The pinned documentation for both says the same thing: they **return the current token if it has not expired; otherwise they refresh it and return a new one**. With `forceRefresh: true` they refresh regardless of expiry. Consequences: - **Call it per request, not once.** Because the SDK caches and refreshes for you, `await user.getIdToken()` before each call is cheap when the token is fresh and correct when it is not. Caching the string yourself at sign-in is how apps end up sending expired tokens. - **Do not force-refresh by default.** `getIdToken(true)` makes a network round trip every time; reserve it for when you know the claims changed. - **The return type is nullable** (`Future<String?>`), so handle `null` rather than using `!` blindly. `getIdTokenResult` returns an `IdTokenResult` with the `token` plus parsed metadata: `claims`, `expirationTime`, `issuedAtTime` and the sign-in provider. ## Reacting to token changes `idTokenChanges()` emits whenever the current user's token changes, in addition to sign-in and sign-out. That is the stream to use when some component — an HTTP client wrapper, a WebSocket connection — must refresh something *when* the token changes rather than on each request. For per-request HTTP calls, calling `getIdToken()` in an interceptor is simpler and equally correct. ## Custom claims and roles Suppose moderators get a `moderator: true` custom claim set by a backend. The claim appears on the device only in a **newly issued** ID token. The FlutterFire guide lists when that happens: 1. the user signs in or re-authenticates after the claim was set; 2. the existing session refreshes its ID token after the old one expires; 3. the app forces a refresh with `getIdTokenResult(true)`. So after a backend call that grants a role, the client calls `getIdTokenResult(true)` and reads `claims`. And a warning printed in the `IdTokenResult` source itself: these claims **"are not to be trusted as they are parsed client side."** Reading `claims['moderator']` is fine for showing or hiding a button; the backend must still verify the token and check the claim on every privileged request. ## Common mistakes | Mistake | Consequence | |---|---| | Cache the token string at sign-in and reuse it | Requests start failing with 401 once it expires | | Always call `getIdToken(true)` | An extra network round trip, and slower requests, every time | | Gate privileged actions on client-side `claims` only | Anyone who modifies the client bypasses the check | | Expect a new claim to appear immediately | The UI shows the old role until a new token is issued | | Persist the ID token in your own storage | Duplicates what the SDK manages and leaves stale tokens behind | ## Checklist for the request path - Fetch the token inside the request pipeline, immediately before sending. - Treat a `null` token as "not signed in" and let the server answer accordingly. - Force a refresh only after an event that changes claims, such as a role grant. - On an unauthorized response, retry once after `getIdToken(true)` before surfacing an error. - Keep role checks on the server; client-side claims only shape the UI. ## A sketch of the interceptor approach ```dart import 'package:firebase_auth/firebase_auth.dart'; Future<Map<String, String>> authHeaders() async { final User? user = FirebaseAuth.instance.currentUser; if (user == null) return const {}; final String? token = await user.getIdToken(); return token == null ? const {} : {'Authorization': 'Bearer $token'}; } ``` The function asks for the token at request time, relies on the SDK's refresh, and degrades to no header when nobody is signed in — leaving the server to answer with its own unauthorized response.

  • Why not store the ID token in secure storage at sign-in and reuse it?
    The token is short-lived and the SDK already stores the session and refreshes the token for you. A copy saved at sign-in goes stale, and requests start failing once it expires. Asking `getIdToken()` at request time always yields a valid token without any storage of your own.
  • When would you listen to idTokenChanges() instead of calling getIdToken() per request?
    When a long-lived component holds the token, such as an open WebSocket that authenticated at connect time or a client configured once with a static header. `idTokenChanges()` tells it when to re-authenticate. For ordinary per-request HTTP calls, fetching the token in an interceptor is simpler.

saying these in an interview costs you the question

  • getIdToken() hits the network and mints a new token on every call.
  • Cache the ID token once at sign-in and reuse it for the whole session.
  • Checking claims on the device is enough to protect moderator-only endpoints.
  • A custom claim set on the server shows up in the next getIdToken() without a refresh.
  • authStateChanges() fires whenever a user's claims change.