The Full Auth Flow: Bridging Firebase and Your Backend
Most Flutter authentication tutorials stop at the happy path: the user signs in with Firebase, you redirect to the home screen, done. Then the app tries to call your own backend — a /api/orders or /api/profile endpoint — and the questions start. What does your server actually trust? How does it know the request came from this user and not someone replaying a stolen request? What happens when the Firebase session expires mid-use?
This article details the complete, end-to-end authentication pipeline, designed for production environments. Firebase handles identity and sign-in on the device. Your backend server manages trust. The JSON Web Token (JWT) acts as the crucial bridge connecting these two domains. By the end, you will understand the entire pipeline, not just the initial login screen.
Why Two Tokens? The Separation of Concerns
The core principle here is a clear separation of concerns: Firebase owns identity on the device, and your backend owns trust for your specific application resources. Relying solely on Firebase authentication for backend access introduces significant security risks and architectural limitations. Firebase's primary role is user authentication and management within the Firebase ecosystem. Its ID tokens are designed for verifying a user's identity with Firebase services, not for authorizing access to your custom backend APIs.
When a user successfully signs in via Firebase (using email/password, Google, etc.), Firebase issues an ID token. This token is a cryptographically signed JSON object containing claims about the authenticated user, such as their unique Firebase User ID (UID), email, and issuance/expiry times. While this token proves the user's identity to Firebase, it is not inherently trusted by your custom backend. Your backend has no direct relationship with Firebase's private keys or authentication infrastructure to verify the authenticity and integrity of a Firebase ID token independently.
This is where the JWT comes in. Your backend's responsibility is to establish and maintain trust for its own resources. It should not delegate this critical function to a third-party service whose internal workings it cannot fully control or verify. A JWT, issued by your backend, serves as a bearer token that your backend *does* trust. It contains claims specific to your application's authorization model, such as user roles, permissions, or application-specific identifiers. By exchanging the Firebase ID token for a backend-issued JWT, you create a secure and auditable chain of trust.

The Token Exchange: From Firebase to Your Backend
The process begins after a user has successfully authenticated with Firebase. The Flutter app, running on the user's device, retrieves the Firebase ID token. This token is typically obtained using methods like FirebaseAuth.instance.currentUser.getIdToken(). This token is short-lived, usually valid for about an hour, and is intended for verifying the user's identity with Firebase services.
Instead of passing this Firebase ID token directly to your backend APIs for authorization, the app sends it to a dedicated endpoint on your backend server. This endpoint, often something like /api/auth/exchange-token, is responsible for validating the Firebase ID token and issuing a new, backend-trusted JWT.
Upon receiving the Firebase ID token, your backend performs several crucial steps:
- Verification: The backend verifies the signature of the Firebase ID token using Firebase's public keys. This ensures the token hasn't been tampered with and was indeed issued by Firebase.
- Audience Check: It checks the
aud(audience) claim within the token to ensure it was intended for your specific backend project. - Expiration Check: It verifies that the token has not expired.
- User Identification: It extracts the user's Firebase UID from the verified token.
- Backend Trust Establishment: Using the Firebase UID, the backend can identify the corresponding user in its own user database. It then generates a new JWT. This JWT contains claims relevant to your application, such as the user's application-specific ID, roles, and permissions. This JWT is typically signed with your backend's private key.
- Issuance: The backend returns this newly generated JWT to the Flutter app.
This exchange is critical. It ensures that your backend only issues its own trusted tokens to authenticated Firebase users, thereby maintaining control over its security and authorization model.
Securing API Calls with JWTs
Once the Flutter app has obtained the backend-issued JWT, it can use this token to authenticate requests to your protected backend APIs. The standard practice is to include the JWT in the Authorization header of HTTP requests, typically prefixed with Bearer.
For example, a request to fetch user orders might look like this:
GET /api/orders
Authorization: Bearer On the backend, every incoming request to a protected endpoint must be intercepted by middleware. This middleware performs the following actions:
- Extract Token: It extracts the JWT from the
Authorizationheader. - Verify Signature: It verifies the JWT's signature using your backend's public key (or shared secret, depending on the signing algorithm).
- Validate Claims: It checks essential claims within the JWT, such as expiration time (
exp), issuer (iss), and audience (aud), to ensure the token is valid and intended for this API. - Identify User: If the token is valid, the middleware extracts the user information (e.g., application-specific user ID) from the JWT claims. This information is then typically attached to the request object, making it available to the API handler.
- Authorize Access: Based on the extracted user information and any associated permissions or roles within the JWT claims, the API handler can then authorize the specific action requested by the user.
This pattern ensures that your backend is the ultimate arbiter of trust for its own resources. The JWT acts as a verifiable credential for each API call.
Handling Token Expiration and Refresh
JWTs, like Firebase ID tokens, are typically short-lived to enhance security. A common JWT expiration time might be 15 minutes to a few hours. When a JWT expires, API requests made with it will fail authentication. The Flutter app must be prepared to handle this gracefully.
The strategy involves implementing a token refresh mechanism. When an API call fails due to an expired JWT (often indicated by a 401 Unauthorized HTTP status code), the Flutter app should attempt to obtain a new JWT. This usually involves a refresh token mechanism, though in this Firebase-centric flow, it might involve a dedicated refresh endpoint on your backend that uses a refresh token or re-validates the *still-valid* Firebase ID token if the JWT expiry is much shorter than the Firebase token expiry.
A more robust approach for this specific flow: if the backend JWT expires, the app can attempt to silently re-authenticate with Firebase in the background to get a fresh Firebase ID token. This fresh Firebase ID token is then sent back to the backend's token exchange endpoint to get a new JWT. This process should be managed transparently to the user.
If the Firebase authentication itself has expired or the user has logged out, the app will need to prompt the user to log in again. Implementing an interceptor in your HTTP client (e.g., Dio or http package in Flutter) is an effective way to manage these token refresh and retry operations automatically.
The refresh flow typically looks like this:
- API request fails with 401 (expired JWT).
- Flutter app intercepts the error.
- App attempts to obtain a fresh Firebase ID token (if the Firebase session is still active).
- App sends the new Firebase ID token to the backend's exchange endpoint to get a new JWT.
- App retries the original API request with the new JWT.
- If the Firebase session is also expired or invalid, the app redirects the user to the login screen.
Sign Out Flow
The sign-out process involves both the client-side and potentially server-side actions:
- Client-Side: On the Flutter app, the user initiates sign-out. The app should clear any stored JWTs and refresh tokens. Then, it should sign the user out from Firebase using
FirebaseAuth.instance.signOut(). Finally, navigate the user back to the login screen. - Server-Side (Optional but Recommended): For enhanced security, especially if refresh tokens are involved or if you want to immediately revoke access, your backend can maintain a denylist or blacklist of issued JWTs or refresh tokens. When a user signs out, their tokens can be added to this denylist. This prevents a stolen, but not yet expired, token from being used after the user has ostensibly logged out. This requires a mechanism to store and check this denylist, often using a cache like Redis.
By implementing this comprehensive flow, you establish a secure, scalable, and robust authentication system for your Flutter applications that leverages the strengths of both Firebase for device-level identity and your custom backend for server-level trust and authorization.
