This Firebase Authentication error occurs when your application tries to use an expired ID token for authentication. Firebase ID tokens have a limited lifespan (typically 1 hour) and must be refreshed before they expire. The fix involves implementing proper token refresh logic and handling token expiration gracefully in your application.
The "auth/id-token-expired" error in Firebase Authentication indicates that your application is attempting to use an ID token that has passed its expiration time. Firebase ID tokens are JSON Web Tokens (JWTs) that have a built-in expiration time (typically 1 hour from issuance) for security reasons. When a user authenticates with Firebase (via email/password, Google Sign-In, etc.), they receive an ID token that proves their identity. This token includes an "exp" (expiration) claim that specifies when the token becomes invalid. Once this time passes, the token can no longer be used for authentication or authorization. This error commonly appears when: 1. Your application caches tokens for too long without refreshing them 2. Background processes or scheduled jobs use old tokens 3. Users leave your application open for extended periods without interaction 4. Server-side code doesn't handle token refresh properly 5. Mobile apps are suspended and resumed after token expiration The error is a security feature that prevents replay attacks and ensures tokens have a limited window of validity. Proper handling involves detecting expired tokens and refreshing them before use.
Firebase uses two types of tokens with different lifetimes:
ID Tokens (short-lived):
- Valid for 1 hour (3600 seconds)
- Used for authenticating requests to Firebase services
- Contains user identity claims (uid, email, etc.)
- Must be refreshed before expiration
Refresh Tokens (long-lived):
- Valid until explicitly revoked or user account changes
- Used to obtain new ID tokens
- Stored securely by Firebase client SDKs
How token refresh works:
1. When an ID token is about to expire (or has expired), the Firebase client SDK automatically uses the refresh token to get a new ID token
2. This happens transparently in most cases when using Firebase client SDKs
3. However, if you're storing tokens manually or using them outside the SDK, you need to handle refresh manually
Important: Never expose refresh tokens to client-side code - they should only be handled by Firebase SDKs or secure server-side code.
For web applications using Firebase JavaScript SDK, implement proper token refresh handling:
Option A: Use Firebase SDK's built-in token refresh (recommended):
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
// Listen for auth state changes - Firebase handles token refresh automatically
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, token is automatically refreshed by Firebase
console.log("User signed in:", user.uid);
// Get current ID token (automatically refreshes if needed)
user.getIdToken().then((idToken) => {
// Use this token for API calls
console.log("Current ID token:", idToken);
});
} else {
// User is signed out
console.log("User signed out");
}
});
// For API calls that need fresh tokens:
async function makeAuthenticatedApiCall() {
const user = auth.currentUser;
if (!user) throw new Error("User not authenticated");
// getIdToken() automatically refreshes the token if it's expired or about to expire
const idToken = await user.getIdToken(true); // true = force refresh
const response = await fetch("/api/protected", {
headers: {
Authorization: "Bearer " + idToken,
},
});
return response.json();
}
Option B: Listen for token refresh events:
import { getAuth, onIdTokenChanged } from "firebase/auth";
const auth = getAuth();
// Listen for ID token changes (including refreshes)
onIdTokenChanged(auth, (user) => {
if (user) {
// Get the fresh token whenever it changes
user.getIdToken().then((idToken) => {
// Update your API client or store the new token
localStorage.setItem("firebaseIdToken", idToken);
console.log("Token refreshed:", new Date().toISOString());
});
}
});
// Set up periodic token refresh (optional, Firebase usually handles this)
setInterval(() => {
const user = auth.currentUser;
if (user) {
// Force token refresh every 45 minutes to stay ahead of expiration
user.getIdToken(true);
}
}, 45 * 60 * 1000); // 45 minutes
Mobile apps need special consideration due to app lifecycle events:
React Native with Firebase:
import auth from '@react-native-firebase/auth';
// Listen for auth state changes
const unsubscribe = auth().onAuthStateChanged(async (user) => {
if (user) {
// Get fresh token
const idToken = await user.getIdToken(true);
// Store for API calls
await AsyncStorage.setItem('firebaseIdToken', idToken);
// Schedule background refresh
scheduleTokenRefresh();
}
});
// Handle app state changes
import { AppState } from 'react-native';
AppState.addEventListener('change', async (nextAppState) => {
if (nextAppState === 'active') {
// App came to foreground - refresh token if needed
const user = auth().currentUser;
if (user) {
try {
const idToken = await user.getIdToken(true);
await AsyncStorage.setItem('firebaseIdToken', idToken);
} catch (error) {
console.error('Token refresh failed:', error);
// Handle re-authentication if needed
}
}
}
});
// Schedule periodic refresh
function scheduleTokenRefresh() {
// Refresh every 30 minutes while app is active
setInterval(async () => {
const user = auth().currentUser;
if (user) {
try {
await user.getIdToken(true);
} catch (error) {
console.error('Background token refresh failed:', error);
}
}
}, 30 * 60 * 1000);
}
Flutter with Firebase:
import 'package:firebase_auth/firebase_auth.dart';
final FirebaseAuth _auth = FirebaseAuth.instance;
// Listen for auth state changes
_auth.authStateChanges().listen((User? user) async {
if (user != null) {
// Get fresh token
String idToken = await user.getIdToken(true);
// Store for API calls
await storage.write(key: 'firebaseIdToken', value: idToken);
// Schedule refresh
_scheduleTokenRefresh();
}
});
// Handle app lifecycle
import 'package:flutter/services.dart';
SystemChannels.lifecycle.setMessageHandler((msg) async {
if (msg == AppLifecycleState.resumed.toString()) {
// App resumed - refresh token
User? user = _auth.currentUser;
if (user != null) {
try {
String idToken = await user.getIdToken(true);
await storage.write(key: 'firebaseIdToken', value: idToken);
} catch (e) {
print('Token refresh failed: $e');
}
}
}
return null;
});
When using Firebase Admin SDK on the server, you need to validate tokens and handle expiration:
Node.js with Firebase Admin SDK:
const admin = require('firebase-admin');
// Initialize Firebase Admin SDK
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
// Middleware to verify and handle token expiration
async function verifyFirebaseToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const idToken = authHeader.split('Bearer ')[1];
try {
// Verify the token - this will throw if token is expired
const decodedToken = await admin.auth().verifyIdToken(idToken);
// Check if token is about to expire (within 5 minutes)
const now = Math.floor(Date.now() / 1000);
const expiresIn = decodedToken.exp - now;
if (expiresIn < 300) { // 5 minutes
// Token will expire soon - return header to client to refresh
res.set('X-Token-Expiring-Soon', 'true');
}
// Attach user info to request
req.user = decodedToken;
next();
} catch (error) {
console.error('Token verification failed:', error.code, error.message);
if (error.code === 'auth/id-token-expired') {
// Token is expired - client needs to refresh
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED',
message: 'Please refresh your authentication token'
});
}
// Other authentication errors
return res.status(401).json({ error: 'Invalid token' });
}
}
// Use the middleware
app.get('/api/protected', verifyFirebaseToken, (req, res) => {
res.json({ message: 'Authenticated!', user: req.user });
});
// Client-side handling of expired tokens
async function callProtectedApi() {
try {
const response = await fetch('/api/protected', {
headers: {
Authorization: "Bearer " + await getCurrentIdToken(),
},
});
if (response.status === 401) {
const error = await response.json();
if (error.code === 'TOKEN_EXPIRED') {
// Refresh token and retry
await refreshIdToken();
return callProtectedApi(); // Retry with fresh token
}
}
return response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
When handling token expiration, implement retry logic with exponential backoff:
class ApiClient {
constructor() {
this.maxRetries = 3;
this.baseDelay = 1000; // 1 second
}
async request(url, options = {}, retryCount = 0) {
try {
// Get fresh token for each request
const idToken = await this.getFreshIdToken();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: "Bearer " + idToken,
},
});
if (response.status === 401) {
const error = await response.json();
if (error.code === 'TOKEN_EXPIRED' && retryCount < this.maxRetries) {
// Token expired - refresh and retry
await this.forceTokenRefresh();
// Exponential backoff delay
const delay = this.baseDelay * Math.pow(2, retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
return this.request(url, options, retryCount + 1);
}
}
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
return response.json();
} catch (error) {
if (retryCount < this.maxRetries && this.isRetryableError(error)) {
// Exponential backoff for network errors
const delay = this.baseDelay * Math.pow(2, retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
return this.request(url, options, retryCount + 1);
}
throw error;
}
}
async getFreshIdToken() {
const user = auth.currentUser;
if (!user) throw new Error('User not authenticated');
// Force refresh if token is old (last refreshed > 30 minutes ago)
const lastRefresh = localStorage.getItem('lastTokenRefresh');
const now = Date.now();
const forceRefresh = !lastRefresh || (now - parseInt(lastRefresh)) > 30 * 60 * 1000;
const idToken = await user.getIdToken(forceRefresh);
if (forceRefresh) {
localStorage.setItem('lastTokenRefresh', now.toString());
}
return idToken;
}
async forceTokenRefresh() {
const user = auth.currentUser;
if (!user) throw new Error('User not authenticated');
await user.getIdToken(true);
localStorage.setItem('lastTokenRefresh', Date.now().toString());
}
isRetryableError(error) {
// Network errors and 5xx server errors are retryable
return error.message.includes('Network') ||
(error.status && error.status >= 500);
}
}
// Usage
const api = new ApiClient();
api.request('/api/data').then(data => {
console.log('Data:', data);
}).catch(error => {
console.error('Request failed after retries:', error);
});
Test your token expiration handling to ensure it works correctly:
1. Simulate token expiration in development:
// Mock Firebase auth for testing
const mockAuth = {
currentUser: {
getIdToken: async (forceRefresh) => {
if (forceRefresh) {
// Simulate fresh token
return 'mock-fresh-token-' + Date.now();
}
// Simulate expired token on first call
if (!mockAuth._hasRefreshed) {
mockAuth._hasRefreshed = true;
throw { code: 'auth/id-token-expired' };
}
return 'mock-token';
}
}
};
// Test your error handling
async function testTokenRefresh() {
try {
const token = await mockAuth.currentUser.getIdToken();
console.log('Got token:', token);
} catch (error) {
if (error.code === 'auth/id-token-expired') {
console.log('Token expired - refreshing...');
const freshToken = await mockAuth.currentUser.getIdToken(true);
console.log('Got fresh token:', freshToken);
}
}
}
2. Test with real Firebase (using short-lived tokens):
- Set up a test Firebase project
- Manually shorten token expiration for testing (not recommended for production)
- Use Firebase Emulator Suite to control token behavior
3. Integration tests:
describe('Token expiration handling', () => {
it('should refresh token when expired', async () => {
// Mock expired token
localStorage.setItem('firebaseIdToken', 'expired-token');
// Make API call
const response = await fetch('/api/protected');
// Should get 401 with TOKEN_EXPIRED code
expect(response.status).toBe(401);
const error = await response.json();
expect(error.code).toBe('TOKEN_EXPIRED');
// Client should refresh token and retry
await refreshToken();
const retryResponse = await fetch('/api/protected');
expect(retryResponse.status).toBe(200);
});
it('should handle concurrent requests with expired token', async () => {
// Simulate multiple requests with same expired token
const promises = Array(5).fill().map(() =>
fetch('/api/protected')
);
const responses = await Promise.all(promises);
// Only one should trigger refresh, others should wait
const successCount = responses.filter(r => r.status === 200).length;
expect(successCount).toBe(5);
});
});
4. Monitor token refresh in production:
- Log token refresh events
- Track token expiration errors
- Set up alerts for abnormal refresh patterns
- Monitor user sessions for unexpected logouts
### Understanding Firebase Token Architecture
Firebase uses a dual-token system for security and performance:
ID Token (JWT):
- Short-lived (1 hour)
- Contains user claims (uid, email, provider, etc.)
- Signed by Firebase
- Used for authenticating requests
- Includes standard JWT claims: iss, aud, exp, iat, sub
Refresh Token:
- Long-lived (until revoked)
- Used to obtain new ID tokens
- Stored securely by Firebase SDK
- Can be revoked by admin or security events
Custom Tokens:
- Created server-side with Admin SDK
- Can have custom expiration (default 1 hour)
- Used for server-to-server auth or custom auth flows
### Security Considerations
Token Storage:
- Never store refresh tokens in client-side storage (localStorage, cookies)
- Use secure, HTTP-only cookies for web apps
- Use platform secure storage for mobile apps (Keychain, Keystore)
- Server-side: store in memory with appropriate cleanup
Token Revocation:
Firebase can revoke tokens in these scenarios:
1. User changes password
2. User account is disabled/deleted
3. Admin revokes refresh tokens
4. Suspicious activity detected
5. Major account changes (email verification, etc.)
Replay Attack Prevention:
The short token lifetime (1 hour) limits the window for replay attacks. Always use HTTPS to prevent token interception.
### Performance Optimization
Token Refresh Strategies:
1. Proactive refresh: Refresh token 5-10 minutes before expiration
2. Lazy refresh: Refresh only when needed (simpler, but may cause delays)
3. Background refresh: Periodic refresh in background (mobile apps)
Caching Considerations:
- Cache ID tokens for short periods (5-15 minutes)
- Invalidate cache on token refresh
- Consider user-specific caching to avoid mixing tokens
Concurrent Request Handling:
When multiple requests use the same expired token:
- Implement token refresh locking to prevent multiple refresh attempts
- Queue requests waiting for token refresh
- Use Promise-based coordination
### Edge Cases and Troubleshooting
Offline Scenarios:
- Mobile apps may need to work offline
- Cache last valid token for limited offline use
- Queue operations for when connectivity returns
- Implement exponential backoff for retries
Clock Skew Issues:
- Server clocks must be synchronized (use NTP)
- Allow small clock skew in token validation (60 seconds)
- Monitor for clock drift in server infrastructure
Multiple Tabs/Windows:
- Share auth state across tabs using storage events
- Coordinate token refresh across tabs
- Handle race conditions in token updates
Firebase Emulator:
- Use for development and testing
- Control token expiration for testing
- Simulate network conditions and errors
### Monitoring and Alerting
Key Metrics to Monitor:
1. Token refresh success/failure rate
2. Token expiration error frequency
3. Average token age at refresh
4. User session duration
5. Concurrent refresh attempts
Alerting Thresholds:
- Token refresh failure rate > 5%
- Token expiration errors spiking
- Abnormal refresh patterns (possible attack)
- User complaints about frequent logouts
Logging:
- Log all token refresh events
- Include token age and refresh reason
- Track user ID and client platform
- Monitor for patterns indicating issues
### Migration and Version Upgrades
SDK Version Changes:
- Test token behavior with new SDK versions
- Review changelogs for auth/token changes
- Plan gradual rollout with monitoring
Project Migration:
- Tokens are project-specific
- Users need to re-authenticate when changing projects
- Plan migration with overlapping token validity
Custom Auth Systems:
- If integrating with custom auth, ensure token compatibility
- Test token exchange flows thoroughly
- Monitor for edge cases in hybrid auth systems
messaging/UNSPECIFIED_ERROR: No additional information available
How to fix "messaging/UNSPECIFIED_ERROR: No additional information available" in Firebase Cloud Messaging
App Check: reCAPTCHA Score Too Low
App Check reCAPTCHA Score Too Low
storage/invalid-url: Invalid URL format for Cloud Storage reference
How to fix invalid URL format in Firebase Cloud Storage
auth/missing-uid: User ID identifier required
How to fix "auth/missing-uid: User ID identifier required" in Firebase
auth/invalid-argument: Invalid parameter passed to method
How to fix "auth/invalid-argument: Invalid parameter passed to method" in Firebase