I'm trying to use Firebase custom claims to protect content for my users, but the first time a user signs up and is redirected to /protectedpage, they cannot view the page because their claim is not set. If they log out and log back in, everything works properly.
Signup Flow
User signs up with email and password
A user document is created in a users collection in Firestore
The user is redirected to /protectedpage
Creation of the user document triggers a cloud function which assigns the custom claim role=A or role=B depending on the information in the user document.
In Javascript (React), it looks like this
Client side
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Add a new document in collection "users"
setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Send email verification
sendEmailVerification(userCredential.user)
.then(() => {
// Redirect to home page
router.push('/protectedpage');
})
.catch((error) => {
console.log("Error sending email verification", error.message);
});
})
.catch((error) => {
setFormError(error.message);
})
Server side
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// This function runs when a document is created in
// the users collection
exports.createUser = functions.firestore
.document('users/{userId}')
.onCreate(async (snap, context) => {
// Get an object representing the document
const doc = snap.data()
const userId = context.params.userId;
// Declare customClaims
let customClaims = {};
// Assign user role
if (doc.account_type == 'A') {
customClaims["role"] = "A"
} else if (doc.account_type == 'B') {
customClaims["role"] = "B"
} else {
functions.logger.info('A role could not be assigned to user:', doc)
response.send('Error: A role could not be assigned')
}
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(userId, customClaims);
} catch (error) {
functions.logger.info(error);
}
return "OK"
})
By the time the user gets to /protectedpage, his JWT does not have the custom claim.
Authorization
My authorization code is using a React context manager, and looks like this
import { createContext, useContext, useEffect, useState } from 'react'
import { onAuthStateChanged, signOut as authSignOut } from 'firebase/auth'
import { auth } from './firebase'
export default function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const clear = () => {
setAuthUser(null)
setIsLoading(false)
}
const authStateChanged = async (user) => {
setIsLoading(true)
if (!user) {
clear()
return
}
// Use getIdTokenResult() to fetch the custom claims
user.getIdTokenResult()
.then((idTokenResult) => {
console.log("idTokenResult", idTokenResult)
setAuthUser({
uid: user.uid,
email: user.email,
role: idTokenResult.claims.role,
})
setIsLoading(false)
})
.catch((error) => {
console.log(error)
})
}
const signOut = () => authSignOut(auth).then(clear)
// Listen for Firebase Auth state change
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, authStateChanged)
return () => unsubscribe()
}, [])
return {
authUser,
isLoading,
signOut,
}
}
const AuthUserContext = createContext({
authUser: null,
isLoading: true,
signOut: async () => {},
})
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth()
return (
<AuthUserContext.Provider value={auth}>{children}</AuthUserContext.Provider>
)
}
export const useAuth = () => useContext(AuthUserContext)
If I change user.getIdTokenResult() to user.getIdTokenResult(true), the user no longer has to sign out and sign back in to access the custom claim BUT
They need to manually refresh the page to acquire the custom claim
I think this is bad, as it's going to forcibly refresh the token on every page load ??
The Firebase docs seem to address this problem with some trickery involving "metadataRef" but I don't understand it exactly, as I think it's related to the Realtime database whereas I'm using Firestore.
Finally got this to work. Two things were tripping me up.
router.push('/protectedpage') doesn't do a hard refresh. I changed this to window.location.replace('/protectedpage')
Instead of assigning the custom claim on creation of the user record, I wrote a cloud function to do it. After my user is created, I call this function. After I get the response, then I redirect the user to /protectedpage
My cloud function looks like this
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// IMPORTANT:
// Note the distinction between onCall and onRequest
// With onCall, authentication / user information is automatically added to the request.
// https://stackoverflow.com/questions/51066434/firebase-cloud-functions-difference-between-onrequest-and-oncall
// https://firebase.google.com/docs/functions/callable
// Function to set a user's role as either "A" or "B"
exports.setRole = functions.https.onCall((data, context) => {
// Check that the user is authenticated.
if (!context.auth) {
// Throw an HttpsError so that the client gets the error details.
// List of error codes: https://firebase.google.com/docs/reference/node/firebase.functions#functionserrorcode
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
// Confirm that the function contains a role
if (!data.hasOwnProperty("role")) {
throw new functions.https.HttpsError(
'failed-precondition',
"The function data must contain a 'role'"
);
}
// Confirm that role is either A or B
if (data.role !== "A" && data.role !== "B") {
throw new functions.https.HttpsError(
'failed-precondition',
"'role' must be set to either 'A' or 'B'"
);
}
// Confirm that the user doesn't already have a role
if (context.auth.token.role) {
throw new functions.https.HttpsError(
'failed-precondition',
"The user's role has already been set"
);
}
// Assign the role
// IMPORTANT:
// We need to return the promise! The promise returns the response. This way, on the client,
// we can wait for the promise to get resolved before moving onto the next step.
return getAuth().setCustomUserClaims(context.auth.uid, { role: data.role })
.then(() => {
return "OK"
})
.catch((error) => {
throw new functions.https.HttpsError(
'internal',
'Error setting custom user claim'
);
})
})
and I call it from the client like this
// Handle form submission
const onSubmit = (formValues) => {
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Send email verification
sendEmailVerification(user);
// Add a new document in collection "users"
const promise1 = setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Set the user role (custom claim)
// Then force refresh the user token (JWT)
const setRole = httpsCallable(functions, 'setRole');
const promise2 = setRole({ role: formValues.account_type })
.then(() => user.getIdTokenResult(true));
// When the user document has been created and the role has been set,
// redirect the user
// IMPORTANT: router.push() doesn't work for this!
Promise.all([promise1, promise2]).then((values) => {
window.location.replace('/protectedpage');
})
})
.catch((error) => {
setFormError(error.message);
})
}
I'm trying after successful login, to redirect the user on another page.
I'm on Svelte JS, and i use Routify, Firebase for Auth and datas
my imports :
import { Router, redirect, routes, goto } from "#roxi/routify";
my function to verify form and redirect when user is ok
// submit form
let params = {};
function handleSubmission({params}) {
hasBeenClicked = true;
if (isValidEmail && isValidPassword) {
// Trim email and password
let trimEmail = email.trim();
let trimPassword = password.trim();
// Initialize Firebase
const auth = getAuth();
const db = getFirestore();
// Test connection
signInWithEmailAndPassword(auth, trimEmail, trimPassword)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
console.log('user',user.uid);
// Try to redirect to the page with the params
// doc : src/pages/admin/[business].svelte corresponds to /admin/:business, where :business // is a parameter.
$goto(`/adminUser/userPage:params?user=${user.uid}`, params);
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(errorCode, errorMessage);
});
}
}
when i submit i got this :
Cannot read properties of undefined (reading 'params')
I've been read the doc, but I can't figure out what to do and how to put the user in the params.
I'm having a hard time getting the Firebase signInWithRedirect with google to work. Below is the doc I've been looking at: https://firebase.google.com/docs/auth/web/google-signin.
I have a login page which contains a button. When the button is clicked it take you to google sign in page. However every time I sign in with google it redirects me back to the login page of my app. Part of the code is shown below:
login() {
const auth = getAuth();
signInWithRedirect(auth, provider);
console.log("AAA")
auth.onAuthStateChanged((user) => {
console.log("BBB")
if (user) {
this.$router.push('home')
} else {
getRedirectResult(auth)
.then((result) => {
console.log("WIN0")
// This gives you a Google Access Token. You can use it to access Google APIs.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
console.log(token)
console.log(user)
console.log("WIN")
}).catch((error) => {
console.log("ERROR0")
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
// The email of the user's account used.
const email = error.customData.email;
// The AuthCredential type that was used.
const credential = GoogleAuthProvider.credentialFromError(error);
console.log(errorCode)
console.log(errorMessage)
console.log(email)
console.log(credential)
console.log("ERROR")
// ...
});
}
})
From the console, I only ever see the log AAA, but I don't see any other logs at all. What am I missing? Thank you.
I should also mention that I'm doing this in vue js but I don't think it matters.
So the problem was that I have onAuthStateChanged within the login function. Since the SignInWithRedirect takes to a different page and then redirects back to the page where it was called, in vue I basically moved the onAuthStateChanged to the mounted lifecycle hook.
mounted() {
auth.onAuthStateChanged((user) => {
console.log("BBB")
if (user) {
this.$router.push('home')
} else {
getRedirectResult(auth)
.then((result) => {
console.log("WIN0")
// This gives you a Google Access Token. You can use it to access Google APIs.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
console.log(token)
console.log(user)
console.log("WIN")
}).catch((error) => {
console.log("ERROR0")
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
// The email of the user's account used.
const email = error.customData.email;
// The AuthCredential type that was used.
const credential = GoogleAuthProvider.credentialFromError(error);
console.log(errorCode)
console.log(errorMessage)
console.log(email)
console.log(credential)
console.log("ERROR")
// ...
});
}
})
}
Hey all so I'll keep it short. I've used firebase to log users in and recently implemented 'Sign In with Apple'. The following code is able to open the pop up and let users sign in with their Apple ID but the moment they hit sign in my web app remains unaffected and the console shows the following error:
The script that I'm using is following:
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.4.1/firebase-app.js";
import { getAuth ,OAuthProvider, signInWithRedirect, getRedirectResult, signInWithPopup, signOut } from "https://www.gstatic.com/firebasejs/9.4.1/firebase-auth.js";
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const provider = new OAuthProvider('apple.com');
provider.addScope('email');
provider.addScope('name');
signInWithPopup(auth, provider)
.then((result) => {
// The signed-in user info.
const user = result.user;
// Apple credential
const credential = OAuthProvider.credentialFromResult(result);
const accessToken = credential.accessToken;
const idToken = credential.idToken;
// ...
})
.catch((error) => {
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
// The email of the user's account used.
const email = error.customData.email;
// The credential that was used.
const credential = OAuthProvider.credentialFromError(error);
// ...
});
getRedirectResult(auth)
.then((result) => {
const credential = OAuthProvider.credentialFromResult(result);
if (credential) {
// You can also get the Apple OAuth Access and ID Tokens.
const accessToken = credential.accessToken;
const idToken = credential.idToken;
}
// The signed-in user info.
const user = result.user;
})
.catch((error) => {
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
// The email of the user's account used.
const email = error.email;
// The credential that was used.
const credential = OAuthProvider.credentialFromError(error);
// ...
});
When I click the 'index.ts:116' link I get the error at the following line:
Any input shall be appreciated. Please let me know if I can provide more information. Thanks!
my data is not adding in to my firebase real time database. I have facing the problem during sending data in firebase real time database. It gives an error config_firebase__WEBPACK_IMPORTED_MODULE_0_.default.database is not a function.
I also import 'firebase/database' in my firebase.js file.
import firebase from "../../config/firebase"
import { getAuth, signInWithPopup, FacebookAuthProvider } from "firebase/auth";
const facebook_login=()=>{
return (dispatch)=>{
const provider = new FacebookAuthProvider();
const auth = getAuth();
signInWithPopup(auth, provider)
.then((result) => {
console.log("Facebook Login")
var user = result.user;
var credential = FacebookAuthProvider.credentialFromResult(result);
var accessToken = credential.accessToken;
let create_user={
name:user.displayName,
email:user.email,
profile:user.photoURL,
uid:user.uid
}
firebase.database().ref('/').child(`users/${user.uid}`).set(create_user)
.then(()=>{
alert("Login Successfull")
})
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
const email = error.email;
const credential = FacebookAuthProvider.credentialFromError(error);
console.log(errorMessage)
});
}
}
export{
facebook_login
}
Since you are using the new Modular SDK, you should not use the firebase.database() namespaced syntax.
import { getDatabase, ref, set } from "firebase/database";
const db = getDatabase();
set(ref(db, `users/${user.uid}`), create_user);
You can learn more about the modular syntax in the documentation.