NextJS cookie token not being detected in getServerSideProps - reactjs

I've been working through a really decent tutorial about setting up NextJS, firebase, and react-context to handle user authentication. Everything has been going smoothly enough until, well ... the code within my getServerSideProps fails to find the cookie 'token', which causes my firebase query to fail, triggering my redirect to the login page.
So, in short I can login/logout users and set a cookie token. However, when I go to pages that SSR check for the token it doesn't find anything and instead triggers my redirect.
SSR + cookie resource i'm using: https://colinhacks.com/essays/nextjs-firebase-authentication
page SSR request
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
try {
const cookies = nookies.get(ctx);
console.log("cookies token", cookies.token); // returns empty string :(
const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);
// * the user is authenticated
const { uid, email } = token;
// ! stuff would be fetched here
} catch (error) {
// either the `token` cookie doesn't exist
// or the token verification failed
// either way: redirect to login page
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
props: {} as never,
};
}
return {
props: { data, params: ctx.params },
};
};
Context + where I set the cookie
export const AuthContext = createContext<{ user: firebase.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebase.User | null>(null);
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).nookies = nookies;
}
return firebaseAuth.onIdTokenChanged(async (user) => {
console.log(`token changed!`);
if (!user) {
console.log(`no token found...`);
setUser(null);
nookies.destroy(null, "token");
nookies.set(null, "token", "", {});
return;
}
console.log(`updating token...`);
const token = await user.getIdToken();
// console.log("got user token:", token);
// console.log("got user:", user);
setUser(user);
nookies.destroy(null, "token");
nookies.set(null, "token", token, {});
});
}, []);
// force token refresh every 10 minutes
useEffect(() => {
const handle = setInterval(async () => {
const user = firebaseAuth.currentUser;
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
// clean up
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}

Solved. I posted my answer to this problem here: https://github.com/maticzav/nookies/issues/255

Related

refresh firebase id token server-side

I am working on an app with Next.js 13 and firebase auth with id tokens.
I want to leverage Next.JS built-in capability for server-side components to fetch user data faster, therefore I need to verify id tokens on the server at initial request. When no user is logged in on protected routes, I want to redirect to login page.
The problem arises when the user was inactive for >1h and the id token has expired. The next request header will send the expired token causing auth.verifyIdToken to reject it. This will redirect the user to login page, before any client-side code had a chance to run, including user.getIdToken to refresh the token.
Is there a way to refresh the id token on server-side? I read here, that there is a work-around using firebase REST API, which seems insecure.
Context
I use the `firebaseui` [package][2] for login, which creates the initial id token & refresh token. Then I have an `AuthContextProvider` to provide & refresh the id token on the client:
const ServerAuthContextProvider = ({
children,
user,
cookie,
}: {
children: ReactNode;
user: UserRecord;
cookie: Cookie;
}) => {
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).cookie = cookie;
}
return auth.onIdTokenChanged(async (snap) => {
if (!snap) {
cookie.remove("__session");
cookie.set("__session", "", { path: "/" });
return;
}
const token = await snap.getIdToken();
cookie.remove("__session");
cookie.set("__session", token, { path: "/" });
});
}, [cookie]);
return (
<serverAuthContext.Provider
value={{
user,
auth,
}}
>
{children}
</serverAuthContext.Provider>
);
};
);
};
server-side root component
const RootLayout = async ({ children }: { children: React.ReactNode }) => {
const { user } = await verifyAuthToken();
if (!user) redirect("/login");
return (
<html lang="en">
<body>
<ServerAuthContextProvider user={user}>
{children}
</ServerAuthContextProvider>
</body>
</html>
);
};
server-side token verification
const verifyAuthToken = async () => {
const auth = getAuth(firebaseAdmin);
try {
const session = cookies().get("__session");
if (session?.value) {
console.log("found token");
const token = await auth.verifyIdToken(session.value);
const { uid } = token;
console.log("uid found: ", uid);
const user = await auth.getUser(uid);
return {
auth,
user,
};
}
} catch (error: unknown) {
if (typeof error === "string") {
console.log("error", error);
return {
auth,
error,
};
} else if (error instanceof Error) {
console.log("error", error.message);
return {
auth,
error: error.message,
};
}
}
return {
auth,
};
};

how to using refresh token in react

I want to use my refresh token to get a new access token in a react application. I send a request to an API to get data about books. if I get 401 or 500 error I want to send a request to another API with my refresh token to get a new access token to send to first API to get data about books.
here I use 2 useEffect first for getting books data and second to get new access token . also I have a setInterval to run first useEffect which gets books data. If I change access token in localstorage and crash it deliberately to get 401 error manually I want that new access token that comes from refresh token makes access token in localstorage correct again so stop page from crashing.so my problem is 2 things: first I dont know what to do with my new accesstoken . second is When I change accesstoken in localStorage to manually get 401 error , if I refresh the page I want to my localStorage set my new access token so page does not crash.
here is my useContext and my component which handles these two useEffects:
here is my useContext hook:
import React from "react";
import { useState } from "react";
const AuthContext = React.createContext({
token: "",
refreshToken: "",
isLoggedIn: false,
login: () => {},
logout: () => {},
booksData: [],
});
export const AuthContextProvider = (props) => {
let initialToken = localStorage.getItem("token");
let initialRefreshToken = localStorage.getItem("refresh-token");
const [token, setToken] = useState(initialToken);
const [refreshToken, setRefreshToken] = useState(initialRefreshToken);
const isUserLoggedIn = !!token;
const logoutHandler = () => {
setToken(null);
localStorage.removeItem("token");
localStorage.removeItem("books");
localStorage.removeItem("refresh-token")};
const loginHandler = (token, refreshToken) => {
setToken(token);
setRefreshToken(refreshToken);
localStorage.setItem("token", token);
localStorage.setItem("refresh-token", refreshToken);
};
const contextValue = {
token,
isLoggedIn: isUserLoggedIn,
refreshToken,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
and here is my component:
const Books = () => {
const ctx = useContext(AuthContext);
const [books, setBooks] = useState([]);
const [reqCounter, setReqCounter] = useState(0);
const [tokenError, setTokenError] = useState(false);
useEffect(() => {
const fetchData = async () => {
let response = await fetch("some API endpoint", {
method: "GET",
headers: {
Authorization: `Bearer ${ctx.token}`,
},
});
try {
const data = await response.json();
if (response.status === 200) {
setBooks(data.books);
} else if (response.status === 404) {
setError("No page found");
} else if (response.status === 403) {
setError("You dont have accsess to this page");
}
} catch (error) {
setTokenError(true);
}
};
fetchData();
}, [ctx.token, reqCounter, ctx]); // Is my dependencies right??
setInterval(() => {
setReqCounter(reqCounter + 1);
}, 5000);
useEffect(() => {
const refresh = async () => {
const response = await fetch("some API", {
method: "POST",
body: JSON.stringify({
refresh_token: ctx.refreshToken,
}),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok) {
// Dont Know what should I write here!
}
};
refresh();
}, [tokenError]); // Is my dependencies right??
const content = books.map((item) => (
<BookItem
title={item.name}
year={item.publish_date}
pages={item.pages}
author={item.Author}
img={item.thumbnail}
key={item.name}
/>
));
return (
<section className={classes.bookPage}>
{!error && books.length !== 0 && (
<ul className={`list ${classes.booksList}`}>{content}</ul>
)}
{error && <h2 className={classes.error}>{error}</h2>}
{isLoading && <PulseLoader color="#f53e3e" className={classes.spinner} />}
</section>
);
};
export default Books;
Suggestions
Ideally Handle fetch with token and token refresh in one place, something like HttpContext
but to check you can start with existing authcontext
you can refresh token on regular intervals
or when the call in unauthorized
issues:
when token expires, some call will fail, which needs to be made again with a new token
When token is refreshed at regular interval, if the old token is invalidated, some call in the queue with older token could fail
pseudo code
in AuthContext
const fetchData = async (link) => {
try {
let response = await fetch(link, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch(error) {
// check status and attempt refresh
// but existing calls will old token will fail,
// can will cause multiple refresh token to be called
}
}
//or refresh token on regular interval
useEffect(() => {
const timerId = setInterval(() => {
// refresh token and set token
// The problems is the moment the token is refreshed, the old token might get invalidated and some calls might fail
}, tokenRefershTimeInMilliSec)
return () => {
clearInterval(timerId)
}
}, [])
...
const contextValue = {
token,
isLoggedIn: isUserLoggedIn,
refreshToken,
login: loginHandler,
logout: logoutHandler,
get: fetchData
};
return <AuthContext.Provider value={contextValue} {...props}> // pass all props down
or use a http context to seperate concerns
const initalValue = // some value
const HttpContext = React.createContext(initalValue);
const initialToken = // from localstorage
const HttpContextProvider = (props) => {
const [token, setToken] = useState(initialToken)
const fetchData = async (link) => {
try {
let response = await fetch(link, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch(error) {
// check status and attempt refresh
// but existing calls will old token will fail,
// can will cause multiple refresh token to be called
}
}
const value = useMemo(() => {
return {
get: fetchData,
// post, put, delete
}}, [token]
//refresh token on regular interval
useEffect(() => {
const timerId = setInterval(() => {
// refresh token and set token
// The problems is the moment the token is refreshed, the old token might get invalidated and some calls might fail
}, tokenRefershTimeInMilliSec)
return () => {
clearInterval(timerId)
}
}, [])
return (<HttpContext.Provider {...props}>)
}
if you can are using axios, then you can check way to auto refresh or use libraries like axios-auth-refresh
Hope it points you in the right direction

Running a "refresh token" function from reactjs every X minutes

I'm trying to refresh the authentication token every few minutes using a refresh token. My problem is that the token is saved in a Context (using useContext to retrieve it when necessary) and I'm struggling to use a setInterval-like function to read the current token, POST it to the server and renew it in the state.
This is what I'm trying to do:
const {
tryLocalSignIn,
signin,
signout,
state: AuthState,
} = useContext(AuthContext);
...
let id = setInterval(async () => {
let token = AuthState.token;
let refreshToken = AuthState.refreshToken;
console.log("Running refresh token", token, refreshToken);
let answer = await ApiRefreshToken(token, refreshToken);
if (answer.status !== 200) {
setError("Error using refresh token");
return;
}
signin({
token: answer.data.token,
refreshToken: answer.data.refreshToken,
expires_in: answer.data.expires_in,
});
}, 15000);
...
But I'm unable to read from the AuthState
Not sure from the code you provided how it's structured, but did you try passing AuthState into the setInterval callback function? As in
let id = setInterval(async (state) => {...}, 15000);
and then you access token by state.token or something?
looks like most likely setInterval is refering to the stale state,
if the setInterval in inside useEffect then you might need useRef to get access to the latest version of the varaible
const SomeComponent = (props) => {
const {
tryLocalSignIn,
signin,
signout,
state: AuthState,
} = useContext(AuthContext);
const AuthStateRef = useRef(AuthState)
AuthStateRef.current = AuthState;
.....
useEffect(() => {
let id = setInterval(async () => {
let token = AuthStateRef.current.token;
let refreshToken = AuthStateRef.current.refreshToken;
console.log("Running refresh token", token, refreshToken);
let answer = await ApiRefreshToken(token, refreshToken);
if (answer.status !== 200) {
setError("Error using refresh token");
return;
}
signin({
token: answer.data.token,
refreshToken: answer.data.refreshToken,
expires_in: answer.data.expires_in,
});
}, 15000);
return () => {
clearInterval(id)
}
}, []) // check dependencies like, signin or AuthState
}
if you dont want the effect to run dependencices, then use useRef to get current references

Nextjs App doesn't work in production only in dev mode, return 500 error from firebase

I have an app that works really well in dev mode and even in the build mode of Next js, the repo is there: https://github.com/Sandrew94/space-tourism.
i think the problems is where i get the access_token from firebase in getServerSideProps for strange reason in devMode works and in production don't.
I have follow this guide to get that results https://colinhacks.com/essays/nextjs-firebase-authentication
export const getServerSideProps: GetServerSideProps = async (context) => {
try {
const cookies = nookies.get(context);
const { token } = cookies;
const planetInfo = await fetchPlanetsInfo("destinations", token);
return {
props: {
data: planetInfo || [],
},
};
////////////////////////////////////////////////////////////////
} catch (e) {
context.res.writeHead(302, { Location: "/" });
context.res.end();
return {
redirect: {
permanent: false,
destination: "/",
},
props: {} as never,
};
}
};
or in the context
export const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = React.useState<any>(null);
React.useEffect(() => {
return auth.onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
nookies.set(undefined, "token", "", { path: "/" });
} else {
const token = await user.getIdToken();
setUser(user);
nookies.set(undefined, "token", token, { path: "/" });
}
});
}, []);
// force refresh the token every 10 minutes
React.useEffect(() => {
const handle = setInterval(async () => {
const user = auth.currentUser;
console.log(user);
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
// clean up setInterval
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
};
it's so annoying this thing, it works good in devMode i don't know what's changes in production mode.
/////////////////////////////
UPDATE 1
I have done some tests, it seems like the cookie isn't set properly and return undefined ( i check with some console.log) also i get a warning maybe this is the problem
The "token" cookie does not have a valid value for the "SameSite" attribute. Soon cookies without the "SameSite" attribute or with an invalid value will be managed as "Lax". This means that the cookie will no longer be sent to third-party contexts. If the application depends on the availability of this cookie in this type of context, add the "SameSite = None" attribute. For more information on the "SameSite" attribute, see https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite

REACT - useContext state sharing sync issue

Objective : -
I want the user to be able to see his/her orders only if they are logged in. So I am using AuthContext for state management of users logged in data + tokens.
Issue : -
When I pass down the token from AuthContext to child components, AuthContext takes some time to validate the token with the backend and meanwhile the child component's logic breaks.
Child Component (using state/token) :
const MyOrders = () => {
const { userData } = useContext(AuthContext);
const history = useHistory();
if (!userData.token) { // This redirects the user back to the home page immediately
history.push("/"); // because the token hasn't been passed yet when
// the component is loaded
};
const getOrders = async () => {
const url = 'http://localhost:5000/api/orders';
try {
const res = await axios.get(url, {
headers: {
'x-auth-token': userData.token
}
});
console.log(res.data);
} catch (err) {
console.log(err.response);
}
};
useEffect(() => {
if (userData.token) getOrders();
}, []);
Work Around (is it safe ????)
const MyOrders = () => {
const token = localStorage.getItem('auth-token'); // Use localStorage token directly instead of
// validating the token first (from AuthContext)??
const history = useHistory();
if (!token) {
history.push("/");
};
const getOrders = async () => {
const url = 'http://localhost:5000/api/orders';
try {
const res = await axios.get(url, {
headers: {
'x-auth-token': token
}
});
console.log(res.data);
} catch (err) {
console.log(err.response);
}
};
useEffect(() => {
if (userData.token) getOrders();
}, []);
Parent Component (AuthContext) : // in case anyone requires
const AuthContextProvider = (props) => {
const [userData, setUserData] = useState({
token: undefined,
user: undefined
});
//Need to check if user is logged in
//every time the App is rendered
useEffect(() => {
const checkLoggedIn = async () => {
const url = "http://localhost:5000/api/users/token";
let token = localStorage.getItem("auth-token");
//when user is not logged in
if(token === null) {
localStorage.setItem("auth-token", "");
token = "";
}
//need to validate the token if it exists
const tokenResponse = await axios.post(url, null, {
headers: { "x-auth-token": token }
});
//if token is valid, collect user data
if(tokenResponse.data){
console.log(tokenResponse);
setUserData({
token,
user: tokenResponse.data,
});
} else {
localStorage.setItem("auth-token", "");
token = "";
};
};
checkLoggedIn();
}, []);
maybe you can try make a early return just after history.push("/") so rest of the logic will not executed
const MyOrders = () => {
const { userData } = useContext(AuthContext);
const history = useHistory();
if (!userData.token) { // This redirects the user back to the home page immediately
history.push("/");
return (null)
};
const getOrders = async () => {
const url = 'http://localhost:5000/api/orders';
try {
const res = await axios.get(url, {
headers: {
'x-auth-token': userData.token
}
});
console.log(res.data);
} catch (err) {
console.log(err.response);
}
};
useEffect(() => {
if (userData.token) getOrders();
}, []);
I offer you to try to retrieve the saved token in your first page of the app.
use "await" on the function. after finish trying to retrieve from localStorage,
you should navigate to the login if the token doesn't exist, otherwise navigate to the main page.
In this case you won't have a undefined token if you have a token,
But what if the token is undefined somehow? You need to send an unauthorize error from the server if the user trying to fetch with an undefined token, then catch the error in the client side, check the message and the type you received, if it does an authorization error, navigate to the home page.
It is a little bit complicated answer but it sure solve your problem and it will make it more Clean code and SOLID.
You can add loading to your user data, if setting the user data fails then set loading false and you can redirect in your order by returning a Redirect component from react-router.
const AuthContext = React.createContext();
const AuthContextProvider = ({ children }) => {
const [userData, setUserData] = React.useState({
token: undefined,
user: undefined,
loading: true,
});
React.useEffect(() => {
setTimeout(
() =>
setUserData((userData) => ({
...userData, //any other data that may be there
token: 123,
user: 'Hello world',
loading: false, //not loading anymore
})),
5000 //wait 5 seconds to set user data
);
}, []);
return (
<AuthContext.Provider value={userData}>
{children}
</AuthContext.Provider>
);
};
const App = () => {
const { loading, ...userData } = React.useContext(
AuthContext
);
if (!loading && !userData.token) {
//snippet does not have redirect
// but if you have react-router you
// can do this
//return <Redirect to="/" />;
}
React.useEffect(() => {}, []);
return loading ? (
'loading...'
) : (
<pre>{JSON.stringify(userData, undefined, 2)}</pre>
);
};
ReactDOM.render(
<AuthContextProvider>
<App />
</AuthContextProvider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Resources