I'm working on a project with a React frontend using Firebase authentication and a C# backend API. The frontend retrieves and sends the Firebase ID token for authentication, but I'm encountering a 401 Unauthorized error with the message "Firebase ID token expired" on an authenticated request.
Frontend Code (AuthContext.js):
// AuthContext.js
const getFreshToken = async (force = false) => {
if (!user) return null;
const now = Date.now();
if (force || now - lastTokenRefresh > 5 * 60 * 1000) {
try {
const token = await user.getIdToken(true);
setLastTokenRefresh(now);
return token;
} catch (error) {
console.error('Error refreshing token:', error);
return null;
}
}
return user.getIdToken();
};
const makeAuthenticatedRequest = async (url, options = {}, retryCount = 0) => {
if (!user) {
throw new Error('User not authenticated');
}
try {
const token = await getFreshToken(retryCount > 0);
if (!token) throw new Error('Failed to get authentication token');
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.message?.includes('expired') && retryCount < 2) {
await new Promise(resolve => setTimeout(resolve, 1000));
return makeAuthenticatedRequest(url, options, retryCount + 1);
}
throw new Error(JSON.stringify(errorData));
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
};
useEffect(() => {
const checkUserRole = async () => {
if (user) {
try {
const response = await makeAuthenticatedRequest('http://localhost:5067/api/auth/protected');
setIsAdmin(response.user.isAdmin);
} catch (error) {
console.error('Error checking user role:', error);
setIsAdmin(false);
}
} else {
setIsAdmin(false);
}
};
checkUserRole();
}, [user]);
useEffect(() => {
if (!user) return;
const refreshInterval = setInterval(async () => {
await getFreshToken(true);
}, 10 * 60 * 1000);
return () => clearInterval(refreshInterval);
}, [user]);
Backend Code (AuthController.cs): The TestProtected endpoint verifies the token and checks user permissions.
// AuthController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using FirebaseAdmin.Auth;
using UniLostAndFound.API.Services;
using UniLostAndFound.API.Models;
namespace UniLostAndFound.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly ILogger<AuthController> _logger;
private readonly FirestoreService _firestoreService;
public AuthController(ILogger<AuthController> logger, FirestoreService firestoreService)
{
_logger = logger;
_firestoreService = firestoreService;
}
[HttpGet("test")]
public ActionResult TestPublic()
{
return Ok(new { message = "Public endpoint working!" });
}
[HttpGet("protected")]
public async Task<ActionResult> TestProtected()
{
try
{
string authHeader = Request.Headers["Authorization"].ToString();
_logger.LogInformation($"Auth Header received: {authHeader.Length} characters");
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
{
return Unauthorized(new { message = "No Bearer token found" });
}
string idToken = authHeader.Substring("Bearer ".Length);
try
{
var firebaseToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
string email = firebaseToken.Claims.GetValueOrDefault("email", "").ToString();
string name = firebaseToken.Claims.GetValueOrDefault("name", "").ToString();
// Check if email is allowed
if (!await _firestoreService.IsAllowedEmail(email))
{
return Unauthorized(new { message = "Email domain not allowed" });
}
// Check if user is admin
bool isAdmin = await _firestoreService.IsAdminEmail(email);
return Ok(new
{
message = "Authentication successful",
user = new
{
uid = firebaseToken.Uid,
email = email,
name = name,
isAdmin = isAdmin
}
});
}
catch (FirebaseAuthException ex)
{
_logger.LogWarning($"Token verification failed: {ex.Message}");
return Unauthorized(new { message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError($"Unexpected error during token verification: {ex.Message}");
return StatusCode(500, new { message = "Internal server error during authentication" });
}
}
catch (Exception ex)
{
_logger.LogError($"Unexpected error: {ex.Message}");
return StatusCode(500, new { message = "Internal server error" });
}
}
}
Program.cs
// Update the JWT Bearer configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var projectId = "unilostandfound";
options.Authority = $"https://securetoken.google.com/{projectId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"https://securetoken.google.com/{projectId}",
ValidateAudience = true,
ValidAudience = projectId,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
Issues Encountered
What I've Tried
Honestly, I am pretty lost right now, I cannot pinpoint exactly what causing the token to expired. Why are the tokens expiring so quickly? Did I do the token handling wrong, if so, what is the best solution? Lastly, did I miss anything from my backend? Thank you.
Fixed It! At first I thought it was a mismatch in server-client time but it wasn't.
What I did was I fixed my token refresh.
I set up an interval to refresh the user's token every 30 minutes. This ensures that the token remains valid during active sessions. Here’s the code I used:
useEffect(() => {
if (!user) return;
// Force token refresh every 30 minutes
const refreshInterval = setInterval(async () => {
try {
await user.getIdToken(true);
console.log("[Auth] Token refreshed by interval");
} catch (error) {
console.error("[Auth] Token refresh failed:", error);
}
}, 30 * 60 * 1000); // 30 minutes
return () => clearInterval(refreshInterval);
}, [user]);