How to refresh firebase access token - reactjs

In the current project, I log in to Firebase and get the user's information.
However, the log below occurs on the server, and there is a problem of getting all user information, not a specific user.
{"level":30,"time":1675750089706,"pid":16748,"hostname":"DESKTOP-JP9RKDH","msg":"HTTP GET: /api/friends/"}
{"level":30,"time":1675750089707,"pid":16748,"hostname":"DESKTOP-JP9RKDH","msg":"UserID is invalid, retrieving all friends"}
{"level":30,"time":1675750089733,"pid":16748,"hostname":"DESKTOP-JP9RKDH","msg":"Decoded Token User ID: Yk1eA8Vbh7fFIRd3eTNXvyHCdwH3"}
I thought there was no problem because I was refreshing when the token expired as follows.
Also, checking the token stored in the cookie every hour showed that it was a new token.
Please let me know what is causing this error.
const setToken = token => {
cookie.set('FB_TOKEN', token);
};
export const getToken = () => {
fbAuth.onIdTokenChanged(async user => {
if (user) {
const newToken = await user.getIdToken();
setToken(newToken);
}
});
const token = cookie.get('FB_TOKEN') ?? '';
return token;
};
export const login = createAsyncThunk('user/login', async (data, { rejectWithValue }) => {
try {
let credential;
if (data.type === 'google') {
localStorage.clear();
const provider = new GoogleAuthProvider();
credential = await signInWithPopup(fbAuth, provider);
const token = await credential.user.getIdToken();
setToken(token);
} else {
credential = await signInWithEmailAndPassword(fbAuth, data.loginInfo.email, data.loginInfo.password);
const token = await credential.user.getIdToken();
setToken(token);
}
return {
id: credential.user.uid,
nickname: credential.user.displayName,
email: credential.user.email,
image: credential.user.photoURL,
};
} catch (error) {
return rejectWithValue(error.response.data);
}
});
axios.defaults.baseURL = backendUrl;
axios.defaults.withCredentials = true;
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.interceptors.request.use(
async config => {
const token = await getToken();
config.headers.Authorization = `Bearer ${token}`;
return config;
},
error => {
return Promise.reject(error);
},
);
export const loadMyFriends = createAsyncThunk('schedule/loadMyFriends', async () => {
const response = await axios.get('/friends');
return response.data;
});

Related

how can i wait for firebase to check the user is valid before sending a POST request with reactjs?

I am using the following code to obtain the users idToken before sending it to the backend as an authorisation header:
const user = firebase.auth().currentUser
const idToken = await user.getIdToken()
sent like this:
var res = await axios.post(backUrl + "account/load_balance", {
uid: uid,
id: id
},
{
headers: {
Authorization: 'Bearer ' + idToken
}});
It works well but on one of my pages the request is sent to the server before idtoken variable has filled and the user is still null.
I have read that i need to implement onAuthStateChanged as it waits for the token before triggering: https://firebase.google.com/docs/auth/web/manage-users#web-version-8
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
However i am unsure how to implement this in to my code.
Can anyone advise?
Full code:
const RoutingForPortfolio = (props) => {
let uid = localStorage.getItem("account-info");
let { id } = useParams();
const loadBlockchainData = async (dispatch) => {
if (id === null || id === undefined) {
id = "test";
}
const user = firebase.auth().currentUser
const idToken = await user.getIdToken()
console.log(idToken)
var res = await axios.post(backUrl + "account/load_balance", {
uid: uid,
id: id
},
{
headers: {
Authorization: 'Bearer ' + idToken
}});
if (res.data === null) {
await wait(2);
document.location.href = "/logout"
return;
}
else {
// const web3 = new Web3(new Web3.providers.HttpProvider('https://data.stocksfc.com:3200'));
// dispatch(web3Loaded(web3));
const account = res.data.address;
dispatch(web3AccountLoaded(account));
localStorage.setItem("account-address", account);
if (res.data.token_flag && res.data.exchange_flag) {
await dispatch(setLoginUserName(res.data.name));
await dispatch(setLoginUserEmail(res.data.email));
if (res.data.balance !== null) {
await dispatch(etherBalanceLoaded(res.data.balance[0]));
await dispatch(tokenBalanceLoaded(res.data.balance[1]));
await dispatch(exchangeEtherBalanceLoaded(res.data.balance[2]));
await dispatch(exchangeTokenBalanceLoaded(res.data.balance[3]));
}
}
else {
Swal.fire({
icon: "error",
title: "Error...",
text: "Error 485 - Please report to admin",
});
return;
}
}
};
useEffect(() => {
if (uid) {
loadBlockchainData(props.dispatch);
}
}, [props.dispatch, uid]);
return (
<>
{uid ? (
<div>
<Portfolio id={id} />
</div>
) : (
<Login />
)}
</>
);
};
As you correctly identified, firebase.auth().currentUser is a synchronous action that only gets the user object when it is called. You've also correctly surmised that you instead need to use firebase.auth().onAuthStateChanged() to wait to check if the user is logged in.
This can be achieved by wrapping an onAuthStateChanged listener into a Promise where it is immediately detached after being called once.
function getValidatedUser() {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth()
.onAuthStateChanged(
(user) => {
unsubscribe();
resolve(user);
},
reject // pass up any errors attaching the listener
);
});
}
This now allows you to use:
const user = await getValidatedUser();
if (!user) {
// todo: handle no user signed in, such as:
throw new Error("User not signed in!");
}
// if here, user is User object
const idToken = await user.getIdToken()

Why does axios interceptors go into an infinite loop when trying to update a token?

I have access_token and refresh_token in localStorage.
Before calling my backend, I want to check if the token has expired:
instance.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("access_token");
const auth = jwt_decode(accessToken);
const expireTime = auth.exp * 1000;
const now = + new Date();
if (expireTime > now) {
console.log("true");
config.headers["Authorization"] = 'Bearer ' + accessToken;
} else {
console.log("false");
(async() => {
const refreshToken = localStorage.getItem("refresh_token");
const response = await refreshAccessToken(refreshToken);
const data = response.data;
const token = data.accessToken;
setAuth({token,refreshToken});
localStorage.setItem("access_token", token);
config.headers["Authorization"] = 'Bearer ' + token;
})();
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
refresh token method
const refreshAccessToken = async (token) => {
return instance.post("/refresh", token);
};
And this code works until I add a line:
const response = await refreshAccessToken(refreshToken);
After that, when the token expires, the page freezes indefinitely in a repeat of the false branch although I am putting a new token in the storage.
I will be glad if someone tells me what's wrong, maybe I'm doing something wrong with interceptors?
refresh response:
{
"accessToken": "...",
"refreshToken": "..."
}

Correct implementation of refreshtoken with ApolloClient

I am trying to refresh the authentication token when it is near the expiration time, however I end up with an endless loop. The expectation is that the code that checks the expiration time in App.js should work and reset the auth token and the refresh token, however is starts looping endlessly.
I have the following Auth helper functions:
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { AuthenticationError } = require("apollo-server-express");
const config = require("config");
const jwtSecret = config.get("jwt_secret");
const jwtRefreshTokenSecret = config.get("jwt_refresh_token");
const authFunctions = {
checkSignedIn: async (req, requireAuth = true) => {
const header = req.headers.authorization;
if (header) {
const token = header.replace("Bearer ", "");
const decoded = jwt.verify(token, jwtSecret);
console.log(decoded);
let user = await User.findById(decoded.id);
if (!user) {
throw new AuthenticationError("Invalid user credentials.");
}
return user;
}
if (requireAuth) {
throw new AuthenticationError("You must be logged in.");
}
return null;
},
issueToken: async (user) => {
let token = "Bearer " + (await authFunctions.createToken(user));
let refreshToken = await authFunctions.createToken(user, 100);
return { token, refreshToken };
},
issueNewToken: async (req) => {
try {
const token = req.headers.refreshtoken;
if (token) {
const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
let user = await User.findById(decoded.id);
console.log(user);
if (!user) {
throw new AuthenticationError("No user found.");
}
let tokens = await authFunctions.issueToken(user);
return { ...tokens, user };
}
} catch (err) {
throw new AuthenticationError("Invalid Refresh Token.");
}
},
createToken: async ({ id, address }, expiresIn = 60) => {
let secret = expiresIn === 60 ? jwtSecret : jwtRefreshTokenSecret;
return await jwt.sign({ id, address }, secret, { expiresIn });
},
};
module.exports = authFunctions;
The schema:
const { gql } = require("apollo-server");
const typeDefs = gql`
scalar Date
scalar MongoId
type User {
_id: MongoId!
address: String!
createdAt: Date
}
type Auth {
user: User!
token: String!
refreshToken: String!
}
input LoginInput {
address: String!
}
type Query {
refreshTokens: Auth!
}
type Mutation {
createOrGetUser(loginInput: LoginInput): Auth!
}
`;
module.exports = typeDefs;
The resolvers:
const User = require("../../models/User");
const {
issueToken,
issueNewToken,
checkSignedIn,
} = require("../../middleware/Auth");
const resolvers = {
Query: {
refreshTokens: async (root, args, { req }, info) =>
await issueNewToken(req),
},
Mutation: {
createOrGetUser: async (root, args, { req }, info) => {
try {
const existingUser = await User.findOne({
address: args.loginInput.address,
})
.populate({
path: "orders",
model: Order,
})
.exec();
if (existingUser) {
let tokens = await issueToken(existingUser);
return {
user: existingUser,
...tokens,
};
}
const user = await new User({
address: args.loginInput.address,
});
const result = await user.save();
let tokens = await issueToken(result);
return {
user,
...tokens,
};
} catch (err) {
throw err;
}
},
},
};
module.exports = resolvers;
And the App.js
import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import {
ApolloClient,
ApolloLink,
createHttpLink,
useReactiveVar,
concat,
} from "#apollo/client";
import { ApolloProvider } from "#apollo/client/react";
import { accessTokenVar, isLoggedInVar } from "./cache";
import cache from "./cache.js";
import jwtDecode from "jwt-decode";
// Styling
import "./styles/App.css";
// Components
import Routes from "./components/routing/Routes";
import Navbar from "./components/navbar/Navbar";
import { REFRESH_TOKENS } from "./queries/User";
const httpLink = createHttpLink({
uri: "/graphql",
});
const refreshTokens = () => {
return client.query({ query: REFRESH_TOKENS }).then((response) => {
console.log(response);
// Need to set the token to cache here, but the query doesn't get executed it seems
//accessTokenVar(response.data)
return;
});
};
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
let token = accessTokenVar();
if (token) {
token = token.replace("Bearer ", "");
const { exp } = jwtDecode(token);
console.log(exp);
// Refresh the token a minute early to avoid latency issues
const expirationTime = exp - 30;
if (Date.now() / 1000 >= expirationTime) {
refreshTokens(); // this keeps going on a loop
}
}
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? token : "",
},
}));
return forward(operation);
});
const client = new ApolloClient({
cache,
link: concat(authMiddleware, httpLink),
connectToDevTools: true,
credentials: "include",
});
const App = () => {
const accessToken = useReactiveVar(accessTokenVar);
const isLoggedIn = useReactiveVar(isLoggedInVar);
//let isLoggedIn;
accessToken ? isLoggedInVar(true) : isLoggedInVar(false);
return (
<ApolloProvider client={client}>
<Router>
<Fragment>
<Navbar isLoggedIn={isLoggedIn} />
</Fragment>
</Router>
</ApolloProvider>
);
};
export default App;
Note: When createOrGetUser is executed the first time, the flow works. Then, I wait for 30 seconds and send a protected query, after which it gets in an endless loop.
Overall, I feel this flow is broken somehow, but I can't figure out what exactly. Would appreciate any help with this!

How to wait for cookie before setting authContext and login state. React, Firebase

I have a createUser function that sends an axios req to express and takes some time to finish creating a firebase authenticated user, create a token and send token back to user as a cookie.
I also have a getLoggedIn function that takes the cookie and sends it to express to get authenticated and then adjust my authContext accordingly.
My function looks like this: await createUser(registerData ).then(() => getLoggedIn())
My problem is that getLoggedIn() is being called early, before the createUser is done sending back the cookie. Any idea how to fix this? Is there a way to listen for a cookie change? I dont see other people doing that, do I have the wrong approach in general?
Thanks for any help.
New User Form
const { getLoggedIn, loggedIn } = useContext(AuthContext);
const register = async (e) => {
e.preventDefault();
const registerData = {
email,
password,
passwordVerify,
};
try {
await createUser(registerData).then(() => getLoggedIn());
} catch (err) {
console.log(err);
}
}};
CreateUser
export const createUser = async (props) => {
console.log("New User Creation Initiated", props);
const { email, password, passwordVerify } = props;
let userObject = { tokenId: null };
try {
await firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(({ user }) => {
console.log("createUserWithEmailAndPassword", user);
user
.getIdToken()
.then((tokenId) => {
userObject = { tokenId: tokenId };
axios.post(`${domain}/auth`, userObject);
})
.catch((err) => console.log("new user error", err));
})
.catch((err) => {
console.log("createUser Error", err);
});
return;
} catch (err) {
console.log("New User Creation Error: ", err);
}
};
AuthContext
const AuthContextProvider = (props) => {
console.log("AuthContextProvider Initiated");
const [loggedIn, setLoggedIn] = useState(null);
const getLoggedIn = async () => {
console.log("getLoggedIn Initiated");
let validatedUser = await axios.get(`${domain}/auth/loggedIn`);
console.log("AuthContextProvider validatedUser", validatedUser);
setLoggedIn(validatedUser.data);
};
useEffect(() => {
getLoggedIn();
}, []);
return (
<AuthContext.Provider value={{ loggedIn, getLoggedIn }}>
{props.children}
</AuthContext.Provider>
);
};
Express
router.post("/", async (req, res) => {
console.log("---User signup initiated---");
try {
const { tokenId } = req.body;
console.log("tokenId passed from frontend: ", tokenId);
admin
.auth()
.verifyIdToken(tokenId)
.then((decodedToken) => {
const { email, uid } = decodedToken;
console.log("Fetched UID: ", uid);
console.log("Fetched email: ", email);
const Users = db.collection("users");
Users.doc(`${uid}`).set({
email: email,
posts: [],
});
console.log("---jwt signing initiated---");
const token = jwt.sign(
{
user: { email, uid },
},
process.env.JWT_SECRET
);
console.log("token log: ", token);
return res
.cookie("token", token, {
httpOnly: true,
sameSite: "none",
secure: true,
})
.send();
});
} catch (err) {
console.log(err);
}
});
router.get("/loggedIn", (req, res) => {
console.log("login validation initiated");
try {
const token = req.cookies.token;
if (!token) {
console.log("no token cookie");
return res.json(null);
}
const validatedUser = jwt.verify(token, process.env.JWT_SECRET);
console.log("Token cookie: ", validatedUser);
res.json(validatedUser);
} catch (err) {
console.log("loggedIn", err);
}
});
You can use onAuthStateChanged like this in your AuthContext.
const AuthContextProvider = (props) => {
console.log("AuthContextProvider Initiated");
const [loggedIn, setLoggedIn] = useState(null);
const getLoggedIn = async () => {
console.log("getLoggedIn Initiated");
let validatedUser = await axios.get(`${domain}/auth/loggedIn`);
console.log("AuthContextProvider validatedUser", validatedUser);
setLoggedIn(validatedUser.data);
};
const handleAuthStateChanged = user => {
if(user) { //user login
getLoggedIn()
} else { //user logout
setLoggedIn(null)
}
}
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
handleAuthStateChanged(user)
})
return unsubscribe
}, []);
return (
<AuthContext.Provider value={{ loggedIn, getLoggedIn }}>
{props.children}
</AuthContext.Provider>
);
};
In my createUser function I moved my axios call and returned it outside of the firebase.auth() sequence of promises. Seems to work now.
export const createUser = async (props) => {
console.log("New User Creation Initiated", props);
const { email, password, passwordVerify } = props;
let userObject = { tokenId: null };
try {
await firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(({ user }) => {
console.log("createUserWithEmailAndPassword", user);
user
.getIdToken()
.then((tokenId) => {
userObject = { tokenId: tokenId };
})
.catch((err) => console.log("new user error", err));
})
.catch((err) => {
console.log("createUser Error", err);
});
return axios.post(`${domain}/auth`, userObject);
} catch (err) {
console.log("New User Creation Error: ", err);
}
};

Sending verification email with Firebase and React Native

I am trying to send the validation email upon the account registration, using firebase. The registration is being done successfully but whenever I try to code email verification it gives me an error. Probably because I don't know where to place it. All my firebase methods are on Fire.js, which are the following:
import firebaseKeys from './Config';
import firebase from 'firebase';
require("firebase/firestore");
class Fire {
constructor() {
if (!firebase.apps.length) {
firebase.initializeApp(firebaseKeys);
}
}
addPost = async ({ text, localUri }) => {
const remoteUri = await this.uploadPhotoAsync(localUri, 'photos/${this.uid}/${Date.now()}');
return new Promise((res, rej) => {
this.firestore.collection('posts').add({
text,
uid: this.uid,
timestamp: this.timestamp,
image: remoteUri
})
.then(ref => {
res(ref);
})
.catch(error => {
rej(error);
});
});
}
uploadPhotoAsync = async (uri, filename) => {
return new Promise(async (res, rej) => {
const response = await fetch(uri);
const file = await response.blob();
let upload = firebase
.storage()
.ref(filename)
.put(file);
upload.on(
"state_changed",
snapshot => {},
err => {
rej(err);
},
async () => {
const url = await upload.snapshot.ref.getDownloadURL();
res(url);
}
);
});
}
createUser = async user => {
let remoteUri = null
try {
await firebase.auth().createUserWithEmailAndPassword(user.email, user.password)
//I tried to code it here with user.sendEmailVerification();
let db = this.firestore.collection("users").doc(this.uid)
db.set({
name: user.name,
email: user.email,
avatar: null
})
if (user.avatar) {
remoteUri = await this.uploadPhotoAsync(user.avatar, 'avatars/${this.uid}')
db.set({avatar: remoteUri}, {merge: true});
}
} catch (error) {
alert("Error: ", error);
}
};
get firestore() {
return firebase.firestore();
}
get uid() {
return (firebase.auth().currentUser || {}).uid;
}
get timestamp() {
return Date.now();
}
}
Fire.shared = new Fire();
export default Fire;
The createUserWithEmailAndPassword() method returns a Promise which resolves with a UserCredential AND (as the the doc indicates) "on successful creation of the user account, this user will also be signed in to your application."
So you can easily get the signed in user by using the user property of the UserCredential, and call the sendEmailVerification() method, as follows:
try {
const userCredential = await firebase.auth().createUserWithEmailAndPassword(user.email, user.password);
await userCredential.user.sendEmailVerification();
//In the next line, you should most probably use userCredential.user.uid as the ID of the Firestore document (instead of this.uid)
cont db = this.firestore.collection("users").doc(this.uid);
//...
} catch (...)
Note that you may pass an ActionCodeSettings object to the sendEmailVerification() method, see the doc.

Resources