-
Notifications
You must be signed in to change notification settings - Fork 974
Description
Operating System
Any - Accessed via Web Browser
Environment (if applicable)
Chrome 140
Firebase SDK Version
^12.0.0
Firebase SDK Product(s)
Auth
Project Tooling
VueJS (Quasar), TypeScript
Detailed Problem Description
When a user signs in using a custom Firebase Auth token that contains custom claims (e.g., roles , permissions ), and subsequently their email , displayName , or photoURL are updated using updateEmail() or updateProfile() on the client-side, the custom claims embedded in the currentUser 's ID token disappear.
Calling currentUser.getIdTokenResult(true) after these profile updates does not retrieve an ID token with the custom claims. This leads to issues where Firestore security rules, which rely on these claims, fail with Missing or insufficient permissions.
The only way to restore the custom claims to the client-side session is to perform a full re-authentication using signInWithCustomToken() again. A full page refresh also resolves the issue.
This behavior is unexpected as custom claims, once associated with a user, are generally expected to persist across token refreshes and be included in all subsequent valid ID tokens for that user.
Steps and code to reproduce issue
- Setup Firebase Auth: Configure Firebase Authentication to use custom tokens. Our setup involves:
- An external IdP (Auth0) where user roles/permissions are defined.
- A Cloud Function that generates Firebase Custom Tokens using admin.auth().createCustomToken() which include custom claims like roles and permissions copied from Auth0.
- Client-Side Login Flow (Web, TypeScript/JavaScript):
a. A user successfully logs in using signInWithCustomToken(firebaseAuth, customToken) .
b. Immediately after signInWithCustomToken() resolves, call currentUser.getIdTokenResult() (or getIdTokenResult(true) ). Observe: The custom claims ( roles , permissions ) are present in the idTokenResult.claims object.
c. Within the same login flow, call updateEmail(currentUser, newEmail) or updateProfile(currentUser, { displayName: newName }) for the currentUser object.
d. After the update(s) complete, attempt to fetch the ID token claims again using currentUser.getIdTokenResult(true) .
e. Observe: The idTokenResult.claims object now unexpectedly lacks the previously present custom claims (e.g., roles and permissions ). Other standard claims (like name , email , firebase.identities ) remain.
f. Attempt to make a Firestore query that relies on these custom claims in security rules (e.g., allow read: if request.auth.token.roles.includes('admin') ). Observe: The query fails with "Missing or insufficient permissions."
Expected Behavior:
After updateEmail() or updateProfile() calls, and a subsequent currentUser.getIdTokenResult(true) call, the retrieved ID token should correctly contain all custom claims previously set for the user, in addition to any updated basic profile information. Firestore security rules should evaluate successfully based on these claims.
Actual Behavior:
Custom claims (e.g., roles , permissions ) are stripped from the ID token after updateEmail() or updateProfile() calls, even when currentUser.getIdTokenResult(true) is explicitly called. To restore the custom claims in the client's session, a full re-authentication ( signInWithCustomToken() again) is required.
Code Snippets (Illustrative):
// --- Part 1: Login Method Snippet ---
async login() {
console.debug('Firebase Service: login');
const $authStore = useAuthStore(); // Assuming VueX/Pinia store for auth state
if (!$authStore.firebaseAccessToken) {
throw new Error('No Firebase Access Token');
}
try {
const response = await signInWithCustomToken(firebaseAuth, $authStore.firebaseAccessToken);
// This call correctly shows claims present
await this.getUserInfo(); // getUserInfo calls getIdTokenResult() and logs claims
let profileUpdated = false;
const currentUser = firebaseAuth.currentUser;
if (currentUser) {
// Update email if different
if ($authStore.user?.email && $authStore.user.email !== currentUser.email) {
await updateEmail(currentUser, $authStore.user.email);
console.log("Email updated.");
profileUpdated = true;
}
// Update profile (display name, photoURL) if different
if (
($authStore.user?.name && $authStore.user.name !== currentUser.displayName) ||
($authStore.user?.picture && $authStore.user.picture !== currentUser.photoURL)
) {
await updateProfile(currentUser, {
displayName: $authStore.user?.name ?? null,
photoURL: $authStore.user?.picture ?? null,
});
console.log("Profile updated.");
profileUpdated = true;
}
// --- THE PROBLEMATIC POINT ---
// Simply calling getIdTokenResult(true) here DOES NOT restore claims
// if (profileUpdated) {
// console.log("Profile updated, attempting getIdTokenResult(true)...");
// const refreshedResult = await currentUser.getIdTokenResult(true);
// console.log("Claims after getIdTokenResult(true):", refreshedResult.claims); // Claims are MISSING here!
// }
// --- THE WORKAROUND THAT FIXES IT ---
if (profileUpdated) {
console.log("Profile updated. Re-authenticating to restore claims...");
// Re-calling signInWithCustomToken and then getUserInfo (which fetches claims)
// This is the only way to get claims back without a page refresh.
await signInWithCustomToken(firebaseAuth, $authStore.firebaseAccessToken);
await this.getUserInfo(); // This call now correctly shows claims present
console.log("Claims successfully restored via re-authentication.");
}
// After this, proceed with app logic (e.g., navigating to staff page)
// which will make Firestore queries.
// If the workaround is NOT used, Firestore queries will fail here.
} else {
console.warn("No current user after login for profile updates.");
}
} catch (error) {
console.error('Firebase Service login error:', error);
const errorMessage = getErrorMessage(error); // Helper for error messages
$authStore.setError(errorMessage);
}
}
// --- Part 2: Simplified getUserInfo (for context) ---
async getUserInfo() {
const user = firebaseAuth.currentUser;
if (user) {
try {
// This call (even without 'true') correctly shows claims immediately after signInWithCustomToken
const idTokenResult = await user.getIdTokenResult();
console.log("getUserInfo: Claims:", idTokenResult.claims);
// Example output: { ..., roles: ['one_punch_admin'], permissions: [], ... }
} catch (error) {
console.error("getUserInfo: Error getting ID token claims:", error);
}
}
}