JWT Handling: Decoding, Validation & Refresh
JWT Handling: Decoding, Validation & Refresh
JSON Web Tokens (JWTs) are the backbone of modern stateless authentication. When a user signs in with Firebase Authentication, the Firebase SDK issues a signed ID token — a compact, URL-safe JWT that encodes the user's identity and claims. In this lesson you will learn to parse and verify these tokens on the client using the dart_jsonwebtoken package, implement silent token refresh so sessions never expire unexpectedly, and forward the Bearer token to a REST API with every authenticated request.
Anatomy of a Firebase ID Token
A JWT consists of three Base64URL-encoded parts separated by dots: header.payload.signature. The payload contains standard claims you will read frequently:
- sub — the Firebase UID (stable, unique per user)
- email / email_verified — user's email address and verification status
- iat (issued-at) and exp (expiry) — Unix timestamps; Firebase tokens expire after 1 hour
- aud — your Firebase project ID (validates the token is for your app)
- firebase.sign_in_provider — e.g.,
password,google.com
Decoding a Token with dart_jsonwebtoken
Add the package to your pubspec.yaml:
pubspec.yaml dependency
dependencies:
dart_jsonwebtoken: ^2.8.0
firebase_auth: ^5.0.0
http: ^1.2.0
The JWT.decode() method parses the token without signature verification — useful for extracting claims on the client. To verify the signature you supply a SecretKey or RSAPublicKey; for Firebase tokens the issuer uses RS256, but client-side you typically decode without verification (the Firebase SDK has already authenticated the session).
Decoding and reading Firebase JWT claims
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:firebase_auth/firebase_auth.dart';
/// Returns the decoded payload map from the current user's ID token.
/// Throws if there is no signed-in user.
Future<Map<String, dynamic>> getTokenClaims() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) throw StateError('No signed-in user');
// Force-refresh=false: use cached token when still valid
final idToken = await user.getIdToken(false);
// Decode without signature verification (client-side only)
final jwt = JWT.decode(idToken!);
final payload = jwt.payload as Map<String, dynamic>;
final uid = payload['sub'] as String;
final email = payload['email'] as String?;
final expiry = DateTime.fromMillisecondsSinceEpoch(
(payload['exp'] as int) * 1000);
final provider = (payload['firebase'] as Map)['sign_in_provider'];
print('UID: $uid | expires: $expiry | provider: $provider');
return payload;
}
/// Returns true when the cached token will expire within [threshold].
bool isTokenExpiringSoon(Map<String, dynamic> payload,
{Duration threshold = const Duration(minutes: 5)}) {
final exp = DateTime.fromMillisecondsSinceEpoch(
(payload['exp'] as int) * 1000);
return DateTime.now().add(threshold).isAfter(exp);
}
Silent Token Refresh
Firebase tokens expire every 60 minutes. Rather than forcing the user to re-authenticate, you implement silent refresh: call getIdToken(true) to force a new token before the current one expires. The best practice is to refresh proactively — check expiry before each API call and refresh if fewer than 5 minutes remain.
AuthService with proactive silent refresh
import 'dart:async';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
/// Returns a fresh (or still-valid) ID token string.
/// Automatically force-refreshes if expiring within 5 minutes.
Future<String> getFreshToken() async {
final user = _auth.currentUser;
if (user == null) throw StateError('Not authenticated');
// Obtain the token (may be cached)
final rawToken = await user.getIdToken(false);
final payload = JWT.decode(rawToken!).payload as Map<String, dynamic>;
final exp = DateTime.fromMillisecondsSinceEpoch(
(payload['exp'] as int) * 1000);
final needsRefresh =
DateTime.now().add(const Duration(minutes: 5)).isAfter(exp);
if (needsRefresh) {
// Force a new token from Firebase servers
return await user.getIdToken(true) ?? '';
}
return rawToken;
}
/// Listens to Firebase ID token changes and emits fresh tokens.
Stream<String?> get tokenStream =>
_auth.idTokenChanges().asyncMap((user) async {
if (user == null) return null;
return await user.getIdToken(false);
});
}
FirebaseAuth.instance.idTokenChanges() in your auth state notifier. Firebase automatically refreshes the token in the background and emits a new value, so your app always holds a current token without any polling logic.Forwarding the Bearer Token to a REST API
Once you have a fresh token, attach it to every HTTP request as an Authorization: Bearer <token> header. The cleanest pattern is a thin HTTP wrapper or a custom http.BaseClient that injects the header automatically.
AuthenticatedClient — auto-injects Bearer token
import 'dart:async';
import 'package:http/http.dart' as http;
import 'auth_service.dart'; // contains AuthService
/// An http.BaseClient that injects the Firebase Bearer token
/// into every request and silently refreshes when needed.
class AuthenticatedClient extends http.BaseClient {
final AuthService _authService;
final http.Client _inner;
AuthenticatedClient(this._authService,
{http.Client? inner})
: _inner = inner ?? http.Client();
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
// Obtain a fresh (auto-refreshed if needed) token
final token = await _authService.getFreshToken();
// Clone request and add the Authorization header
request.headers['Authorization'] = 'Bearer $token';
request.headers['Content-Type'] = 'application/json';
return _inner.send(request);
}
@override
void close() => _inner.close();
}
// --- Usage ---
// final client = AuthenticatedClient(AuthService());
// final response = await client.get(
// Uri.parse('https://api.example.com/profile'),
// );
// print(response.body);
SharedPreferences). They are short-lived credentials. Use flutter_secure_storage if you must persist any token, and always transmit over HTTPS.Handling Token Errors Gracefully
Network failures, revoked tokens, or clock-skew can cause FirebaseAuthException. Catch specific error codes and redirect the user appropriately:
- user-token-expired — the refresh token has been revoked; redirect to login
- network-request-failed — no connectivity; show offline UI, retry later
- user-disabled — account suspended; sign out immediately
Summary
In this lesson you learned to decode Firebase JWTs using dart_jsonwebtoken to read claims such as UID, email, and expiry. You implemented a proactive silent-refresh strategy via getIdToken(true) and built a reusable AuthenticatedClient that injects the Authorization: Bearer header into every REST call. In the next lesson you will secure local storage and handle token revocation for a complete production-grade auth flow.