I have a React app using Firebase Auth and an Express backend. I have React contexts set up for the user's authentication process and for the loading state of the app. Currently, when a user signs in, the following happens:
The app goes into a loading state
The app sends an API request to the backend to verify the user's token
The backend queries the database and then sets the user's custom claims with their permissions and sends a response with the verified token & claims
The loading state is cleared, and the app becomes useable
The user's routes / nav menu options etc are then determined by the user's permissions according to the backend - i.e, if a user doesn't have permission for a certain area of the site, its routes and nav menu items are not loaded.
My authentication context is as follows:
import { createContext, useContext, useState, useEffect } from "react";
/**
* auth = getAuth()
* provider = new GoogleAuthProvider()
*/
import { auth, provider } from "providers/firebase";
import {
getAuth,
onAuthStateChanged,
signInWithPopup,
signOut as firebaseSignOut
} from "firebase/auth";
import { api } from "providers/axios";
import { useLoading } from "providers/loading";
const UserContext = createContext(null);
export const useAuth = () => useContext(UserContext);
const verifyToken = (token) =>
api({
method: "post",
url: "/user/auth",
headers: {
token
}
});
const UserProvider = (props) => {
const [user, setUser] = useState(null);
const { loading, setLoading } = useLoading();
const signIn = async () => {
setLoading(true);
try {
const result = await signInWithPopup(auth, provider);
console.log("auth signInWithPopup", result.user.email);
} catch (e) {
setUser(null);
console.error(e);
setLoading(false);
}
};
const signOut = async () => {
let userSigningOut = user;
try {
await firebaseSignOut(auth);
setUser(null);
console.log("signed out");
} catch (e) {
console.error(e);
} finally {
return (userSigningOut = null);
}
};
const verifyUser = async (user) => {
try {
if (!user) {
throw "no user";
}
const token = await getAuth().currentUser.getIdToken(true);
if (!token) {
throw "no token";
}
const jwt = await getAuth().currentUser.getIdTokenResult();
if (!jwt) {
throw "no jwt";
}
const verifyTokenResponse = await verifyToken(token);
if (verifyTokenResponse.data.role !== jwt.claims.role) {
throw "role level claims mismatch";
} else {
user.verifiedToken = verifyTokenResponse.data;
console.log(`User ${user.uid} verified`);
setUser(user);
}
} catch (e) {
signOut();
console.error(e);
}
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
setLoading(true);
try {
if (user) {
console.log("onAuthStateChanged", user?.email);
await verifyUser(user);
} else {
throw "no user";
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
});
return unsubscribe;
}, []);
return (
<UserContext.Provider
value={{
signIn,
signOut,
user
}}
>
{props.children}
</UserContext.Provider>
);
};
export default UserProvider;
The problem is that if the user or their permissions are modified, the changes are not reflected in the app until the user performs a hard refresh.
What I'd like to achieve is for the user's token to be re-verified via the backend upon every page change (or similar) and then if their permissions etc have changed, the app then rerenders reflecting the changes. I think this could be achieved by triggering a rerender of a certain part of UserContext after taking it out of the main function, but I'm not sure how to proceed with that.
After #samthecodingman's comment, I added another state for the user's database entry and have achieved the desired outcome with the following changes to UserProvider:
useEffect(() => {
if (user) {
const userDataRef = ref(db, `/users/${user.uid}`);
return onValue(userDataRef, async snapshot => {
await verifyUser(user);
setUserData(snapshot.val());
})
}
}, [user]);
return (
<UserContext.Provider
value={{
signIn,
signOut,
user,
userData
}}
>
{props.children}
</UserContext.Provider>
);
Related
I'm trying to set user presence in my React app using Firebase.
Everytime a user logs in the app I save it the a users collection and I display it as a list in my app.
I need to show the online status for each user, so I'm using Firebase Realtime db to update Firestore db, this is the whole AuthContext.js file :
import React, { useState, useEffect, useContext, createContext } from "react";
import { db, rtdb } from "../firebase/firebase-config";
import {
getAuth,
signInWithPopup,
GoogleAuthProvider,
onAuthStateChanged,
signOut,
} from "firebase/auth";
import {
addDoc,
collection,
serverTimestamp,
getDocs,
query,
where,
limit,
updateDoc,
doc,
} from "firebase/firestore";
import { Redirect } from "react-router-dom";
import { ref as rtdbRef, set, onValue } from "firebase/database";
const authContext = createContext();
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
export const useAuth = () => {
return useContext(authContext);
};
function useProvideAuth() {
const [user, setUser] = useState(null);
const provider = new GoogleAuthProvider();
const auth = getAuth();
const [userLoading, setUserLoading] = useState(true);
const getUserByUid = async (uid) => {
let exists = false;
const querySnapshot = await getDocs(
query(collection(db, "users"), where("uid", "==", uid), limit(1))
);
querySnapshot.forEach(() => (exists = true));
return exists;
};
const persistUser = async (user) => {
const exists = await getUserByUid(user.uid);
if (!exists) {
await addDoc(collection(db, "users"), {
uid: user.uid,
displayName: user.displayName,
email: user.email,
photoURL: user.photoURL,
createdAt: serverTimestamp(),
updatedAt: null,
});
}
};
const logIn = () =>
signInWithPopup(auth, provider)
.then((result) => {
setUser(result.user);
persistUser(result.user);
})
.catch((err) => {
console.log(err);
});
const updateFirestoreUser = (user, status) => {
if (user) {
getDocs(
query(collection(db, "users"), where("uid", "==", user.uid), limit(1))
).then((querySnapshot) => {
querySnapshot.forEach((u) => {
updateDoc(doc(db, "users", u.id), {
online: status,
updatedAt: serverTimestamp(),
});
});
});
}
};
const logOut = () =>
signOut(auth)
.then(() => {
console.log("SIGNOUT", user);
updateFirestoreUser(user, false);
setUser(false);
return <Redirect to="/login" />;
})
.catch((err) => {
console.log(err);
});
function checkAuthStatus() {
return new Promise((resolve, reject) => {
try {
onAuthStateChanged(auth, async (user) => {
resolve(user);
setUser(user);
const connectedRef = rtdbRef(rtdb, ".info/connected");
const myConnectionsRef = rtdbRef(rtdb, `status`);
onValue(connectedRef, (snap) => {
if (snap.val() === true) {
console.log("TRUE");
if (user) {
set(myConnectionsRef, "connected");
updateFirestoreUser(user, true);
}
} else {
console.log("FALSE");
updateFirestoreUser(user, false);
}
//onDisconnect(myConnectionsRef)
//.set("disconnected")
//.then(() => {
// updateFirestoreUser(user, false);
//});
});
});
} catch {
reject("api failed");
setUser(false);
}
});
}
const getUser = async () => {
setUserLoading(true);
await checkAuthStatus();
setUserLoading(false);
};
useEffect(() => {
getUser();
}, []);
return {
user,
logIn,
logOut,
userLoading,
};
}
The online field in users collection gets updated when I login & logout, but not when I close the browser tab, it kinda works when I uncomment this part :
onDisconnect(myConnectionsRef)
.set("disconnected")
.then(() => {
updateFirestoreUser(user, false);
});
But the changes reflect only in the Realtime db with no changes in Firestore db users collection, the string "disconnected" is added to the Realtime database but nothing changed in the user's entry I'm tying to update in the then() method.
Been stuck here for 2 days, I don't know what I'm doing wrong, I just need the online field to be true when the app is opened and the user is logged in, and false when the user is signed out or the app is closed in the browser.
By following the presence system in the documentation you linked, you will gets presence information in Realtime Database.
To get that information into Firestore, you'll need to implement a Cloud Function that triggers in the Realtime Database writes as shown in the section on updating presence information in Firestore in the documentation on implementing presence on Firestore. That's the only way to get the presence information in Firestore, as that database doesn't have any onDisconnect like functionality that is required to build a presence system.
I am implementing aws cognito service in my react app.
I created a login page and applied the authentication method, it was working fine.
Now for state management, I wanted context.provider that will pass the authentication to all other pages.
When I am trying to useContext in my login page then it is giving error: failed to login Error: Username and Pool information are required.
My login.js page looks like this:
import React, { useState, useContext } from 'react';
import { AccountContext } from './Account';
const LoginPage = props => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const { authenticate } = useContext(AccountContext);
const onSubmit = event => {
event.preventDefault();
authenticate(username, password)
.then(data => {
console.log('logged in', data);
})
.catch(err => {
console.log('error is here');
console.error('failed to login', err);
});
};
It seems like it is not going inside authenticate function.
Can you please suggest me where am I going wrong?
My account.js(context provider) file:
import React, { createContext } from 'react';
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import Pool from '../../UserPool';
const AccountContext = createContext();
const Account = props => {
const authenticate = async (username, password) => {
return await new Promise((resolve, reject) => {
const user = new CognitoUser({ username, Pool });
const authDetails = new AuthenticationDetails({ username, password });
user.authenticateUser(authDetails, {
onSuccess: data => {
console.log('onSuccess: ', data);
resolve(data);
// setIsLoggedIn(true);
},
onFailure: err => {
console.error('onFailure: ', err);
reject(err);
}
});
});
};
return (
<AccountContext.Provider value={{ authenticate }}>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };
For context, I am working with integrating a Django Rest Framework backend with Next.js + Next-Auth. I have most of the integration down, except one part. The requirement is to have a refresh token system that will try to refresh the access token when it is almost expired. Here is the logic that I have:
/api/auth/[...nextauth].ts
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import { NextAuthOptions } from "next-auth";
import Providers from "next-auth/providers";
import axios from "axios";
import { AuthenticatedUser } from "../../../types";
import { JwtUtils, UrlUtils } from "../../../constants/Utils";
namespace NextAuthUtils {
export const refreshToken = async function (refreshToken) {
try {
const response = await axios.post(
// "http://localhost:8000/api/auth/token/refresh/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"auth",
"token",
"refresh",
),
{
refresh: refreshToken,
},
);
const { access, refresh } = response.data;
// still within this block, return true
return [access, refresh];
} catch {
return [null, null];
}
};
}
const settings: NextAuthOptions = {
secret: process.env.SESSION_SECRET,
session: {
jwt: true,
maxAge: 24 * 60 * 60, // 24 hours
},
jwt: {
secret: process.env.JWT_SECRET,
},
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async signIn(user: AuthenticatedUser, account, profile) {
// may have to switch it up a bit for other providers
if (account.provider === "google") {
// extract these two tokens
const { accessToken, idToken } = account;
// make a POST request to the DRF backend
try {
const response = await axios.post(
// tip: use a seperate .ts file or json file to store such URL endpoints
// "http://127.0.0.1:8000/api/social/login/google/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"social",
"login",
account.provider,
),
{
access_token: accessToken, // note the differences in key and value variable names
id_token: idToken,
},
);
// extract the returned token from the DRF backend and add it to the `user` object
const { access_token, refresh_token } = response.data;
user.accessToken = access_token;
user.refreshToken = refresh_token;
return true; // return true if everything went well
} catch (error) {
return false;
}
}
return false;
},
async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
if (user) {
const { accessToken, refreshToken } = user;
// reform the `token` object from the access token we appended to the `user` object
token = {
...token,
accessToken,
refreshToken,
};
// remove the tokens from the user objects just so that we don't leak it somehow
delete user.accessToken;
delete user.refreshToken;
return token;
}
// token has been invalidated, try refreshing it
if (JwtUtils.isJwtExpired(token.accessToken as string)) {
const [
newAccessToken,
newRefreshToken,
] = await NextAuthUtils.refreshToken(token.refreshToken);
if (newAccessToken && newRefreshToken) {
token = {
...token,
accessToken: newAccessToken,
refreshToken: newRefreshToken,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60),
};
return token;
}
// unable to refresh tokens from DRF backend, invalidate the token
return {
...token,
exp: 0,
};
}
// token valid
return token;
},
async session(session, userOrToken) {
session.accessToken = userOrToken.accessToken;
return session;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, settings);
Next, the example in the Next-Auth documentation shows the use of useSession() hook. But I am not a fan of it because:
It does not update the state of the session once the access token is refreshed unless the window itself is refreshed (it is an open issue)
It feels like a lot of code repetition on every component that wants to use the session, with the guards that check the existence of session object, whether the session is loading etc. So I wanted to use a HOC.
As such, I came up with the following solutions:
constants/Hooks.tsx
import { Session } from "next-auth";
import { useEffect, useMemo, useState } from "react";
export function useAuth(refreshInterval?: number): [Session, boolean] {
/*
custom hook that keeps the session up-to-date by refreshing it
#param {number} refreshInterval: The refresh/polling interval in seconds. default is 10.
#return {tuple} A tuple of the Session and boolean
*/
const [session, setSession] = useState<Session>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
async function fetchSession() {
let sessionData: Session = null;
setLoading(true);
const response = await fetch("/api/auth/session");
if (response.ok) {
const data: Session = await response.json();
if (Object.keys(data).length > 0) {
sessionData = data;
}
}
setSession(sessionData);
setLoading(false);
}
refreshInterval = refreshInterval || 10;
fetchSession();
const interval = setInterval(() => fetchSession(), refreshInterval * 1000);
return () => clearInterval(interval);
}, []);
return [session, loading];
}
constants/HOCs.tsx
import { Session } from "next-auth";
import { signIn } from "next-auth/client";
import React from "react";
import { useAuth } from "./Hooks";
type TSessionProps = {
session: Session;
};
export function withAuth<P extends object>(Component: React.ComponentType<P>) {
return React.memo(function (props: Exclude<P, TSessionProps>) {
const [session, loading] = useAuth(); // custom hook call
if (loading) {
return <h2>Loading...</h2>;
}
if (!loading && !session) {
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
<pre>{!session && "User is not logged in"}</pre>
</>
);
}
return <Component {...props} session={session} />;
});
}
Then, in a component where I have periodic data fetching requirements (I know this could be achieved in a much better way, this is just a contrived example where I am trying to simulate user inactivity but the app can still work in the background if needed), I am using the HOC:
pages/posts.tsx
import React, { useEffect, useState } from "react";
import Post from "../components/Post";
import { withAuth } from "../constants/HOCs";
import { TPost } from "../constants/Types";
import Link from "next/link";
function Posts(props) {
const { session } = props;
// const [session, loading] = useAuth();
const [posts, setPosts] = useState<TPost[]>([]);
const [fetchingPosts, setFetchingPosts] = useState<boolean>(false);
useEffect(() => {
if (!session) {
return;
}
async function getPosts() {
setFetchingPosts(true);
const response = await fetch("http://127.0.0.1:8000/api/posts", {
method: "get",
headers: new Headers({
Authorization: `Bearer ${session?.accessToken}`,
}),
});
if (response.ok) {
const posts: TPost[] = await response.json();
setPosts(posts);
}
setFetchingPosts(false);
}
// initiate the post fetching mechanism once
getPosts();
const intervalId = setInterval(() => getPosts(), 10 * 1000);
// useEffect cleanup
return () => clearInterval(intervalId);
}, [JSON.stringify(session)]);
// {
// loading && <h2>Loading...</h2>;
// }
// {
// !loading && !session && (
// <>
// Not signed in <br />
// <button onClick={() => signIn()}>Sign in</button>
// <pre>{!session && "User is not logged in"}</pre>
// </>
// );
// }
return (
<div>
<h2>Fetched at {JSON.stringify(new Date())}</h2>
<Link href="/">Back to homepage</Link>
{posts.map((post) => (
<Post key={post.title} post={post} />
))}
</div>
);
}
export default withAuth(Posts);
The problem is that the entire page gets re-rendered due to the withAuth HOC and possibly due to the useAuth hook every 10 seconds. However, I have had no luck trying to debug it. Maybe I am missing something key in my React concepts. I appreciate any and all suggestions/help possible. Thanks in advance.
PS. I am aware of a solution that uses SWR library, but I would like to avoid using that library if at all possible.
I ended up using the useSwr() hook after spending an unworldly amount of time trying to fix this issue. Also ended up writing this article for those who are interested.
Can anyone help me with this please with this useEffect in React? I am updating custom claims in firebase auth via a firestore document called user_claims. In here it has a user role. Instead of waiting an hour for the token to be refreshed....I want to refresh it as soon as I make a change to the user_claims collection. Based on the below I am getting the error:
Unhandled Rejection (TypeError): Cannot read property 'getIdTokenResult' of undefined
Have I put my listeners in the wrong order or should I have two useEffects? Not sure what I need to do and would appreciate guidance. I want to get the user role - in real-time into the AuthContext for use throughout the app.
thanks all in advance for your help and guidance, as always.
import React, { useEffect, useState, createContext } from 'react'
import {firebase, firestore} from './firebase'
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const auth=firebase.auth();
const [currentUser, setCurrentUser] = useState();
const [error, setError] = useState();
useEffect(() => {
async function reportAdminStatus() {
const result = await currentUser.getIdTokenResult(false)
const isAdmin = result.claims.isAdmin
if (isAdmin) {
console.log("Custom claims say I am an admin!")
}
else {
console.log("Custom claims say I am not an admin.")
}
}
auth.onIdTokenChanged(user => {
if (user) {
console.log(`new ID token for ${user.uid}`)
setCurrentUser(user)
reportAdminStatus()
}
})
auth.onAuthStateChanged(async (user) => {
function listenToClaims() {
firestore
.collection('user_claims')
.doc(currentUser.uid)
.onSnapshot(onNewClaims)
}
let synced
function onNewClaims(snapshot) {
const data = snapshot.data()
console.log('New claims doc\n', data)
if (data._synced) {
if (synced &&
!data._synced.isEqual(synced)) {
// Force a refresh of the user's ID token
console.log('Refreshing token')
currentUser.getIdToken(true)
}
synced = data._synced
}
}
if (user) {
try {
const idTokenResult = await user.getIdTokenResult();
setCurrentUser({...user, role: idTokenResult.claims.role, isAdmin: idTokenResult.claims.isAdmin, group: idTokenResult.claims.group });
setError(undefined);
listenToClaims();
reportAdminStatus();
} catch (e) {
setError(e);
}
} else {
setCurrentUser(undefined);
}
});
return [currentUser, error]
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
}
I'm trying to find a way to access the creationTime and lastSignInTime described in this documentation.
Are there any examples of using it within react hooks?
I can't make sense of the firebase documentation generally - it's just words on a page. I think it is designed for people who intuitively know how to fill in the blanks. I remain mystified as to how to do that in general.
I can access auth.user.email using a react hook as follows:
import React, { useState, useEffect, useContext, createContext } from "react";
import firebase from "../firebase";
import {auth} from "../firebase";
const authContext = createContext();
// Provider wraps app and makes auth object available by useAuth().
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Hook to get the auth
export const useAuth = () => {
return useContext(authContext);
};
// Provider hook that creates auth state
function useProvideAuth() {
const [user, setUser] = useState(null);
const signin = (email, password) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(response => {
setUser(response.user);
return response.user;
});
};
const signup = (email, password) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(response => {
setUser(response.user);
return response.user;
});
};
const signout = () => {
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
});
};
const sendPasswordResetEmail = email => {
return firebase
.auth()
.sendPasswordResetEmail(email)
.then(() => {
return true;
});
};
const confirmPasswordReset = (code, password) => {
return firebase
.auth()
.confirmPasswordReset(code, password)
.then(() => {
return true;
});
};
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
return () => unsubscribe();
}, []);
return {
user,
signin,
signup,
signout,
sendPasswordResetEmail,
confirmPasswordReset
};
}
Now, I'm trying to figure out what I need to do to either access the string values described here or the timestamps described here.
I tried each of (all guesses):
{auth.user.UserMetadata().creationTime}
{auth.user.creationTime}
{auth.user.UserMetadata.creationTime}
This works.
{auth.user.metadata.creationTime}
I don't understand why. The references in the firebase documentation refer to metadata as UserMetadata. I don't know how to find the piece of information that tells people to make the leap between UserMetadata and metadata.
If anyone knows what the key to this is, I'd be forever grateful for the insight.