how to using refresh token in react - reactjs

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

Related

Redux toolkit Bearer token undefined

Error: authorization bearer undefined is shown in the browser on the .
Here is my
Here is my code of useEffect of OrderScreen.js Here I have tried to dispatch check if user have value or not if not redirect them to /login which is login page. I am redirecting to login page because from use state i am not getting any value.
const dispatch = useDispatch()
const navigate = useNavigate()
const { user } = useSelector((state) => state.auth)
const { orders, isLoading, isError, message } = useSelector(
(state) => state.orders,
)
useEffect(() => {
if (isError) {
toast.error(message)
}
if (!user && !user.isAdmin) {
navigate('/login')
}
dispatch(getOrder())
return () => {
dispatch(reset())
}
}, [user, isError, message, dispatch, navigate])
`
Here is my orderSlice. for the getOrder Function `
const initialState = {
orders: [],
isError: false,
isSuccess: false,
isLoading: false,
message: '',
}
export const getOrder = createAsyncThunk(
'orders/getOrder',
async (_, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token
return await orderService.getOrder(token)
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString()
return thunkAPI.rejectWithValue(message)
}
},
)
`
Here is my orderService.js `
const getOrder = async (token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
}
const response = await axios.get(API_URL, config)
return response.data
}
`
I tried to solve from these stacks
Authorization Bearer Token Header in Javascript
How to send bearer token through header of axios call in react redux
You can try to pass the token as first argument of your thunk function ( currently "_") to make sure it is not undefined. Also, you can use a debugger to know the actual state of the global store (or simply log it in your component).
And lastly, try to log what returns the thunkApi.getState() function.

Remix.run, Remix-Auth destroy user session if token validation fails

I am using Remix, along with Remix-Auth and using the Twitch API/OAuth, which requires that I check in with their /validate endpoint every hour docs. I had someone recommend that I use a resource route and POST to that if the validation endpoint returned a status of 401, however, I need as I stated before the request needs to be sent every hour I figured maybe I could use something like React-Query to POST to the resource route every hour.
Just pointing out that I use createCookieSessionStorage with Remix Auth to create the session
Problem
I haven't been able to achieve the actual session being destroyed and a user being re-routed to the login page, I have left what actual code I have currently any help or suggestions to actually achieve the session being destroyed and be re-routed to the login page if the validation fails would be greatly appreciated.
// React Query client side, checks if the users token is still valid
const { error, data } = useQuery("TV-Revalidate", () =>
fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${user?.token}`,
},
}).then((res) => res.json())
);
The above React Query returns this
// My attempt at the resource route
// ~routes/auth/destroy.server.ts
import { ActionFunction, redirect } from "#remix-run/node";
import { destroySession, getSession } from "~/services/session.server";
export const action: ActionFunction = async ({request}) => {
const session = await getSession(request.headers.get("cookie"))
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session)
}
})
}
// Second attempt at resource route
// ~routes/auth/destroy.server.ts
import { ActionFunction, redirect } from "#remix-run/node";
import { destroySession, getSession } from "~/services/session.server";
export const action: ActionFunction = async ({request}) => {
const session = await getSession(request.headers.get("cookie"))
return destroySession(session)
}
I attempted using an if statement to POST to the resource route or else render the page, however, this definitely won't work as React errors out because functions aren't valid as a child and page is blank.
//index.tsx
export default function Index() {
const { user, bits, vali } = useLoaderData();
console.log("loader", vali);
const { error, data } = useQuery("TV-Revalidate", () =>
fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${user?.token}`,
},
}).then((res) => res.json())
);
if (data?.status === 401)
return async () => {
await fetch("~/services/destroy.server", { method: "POST" });
};
else
return ( ... );}
You could use Remix' useFetcher hook.
https://remix.run/docs/en/v1/api/remix#usefetcher
// Resource route
// routes/api/validate
export const loader: LoaderFunction = async ({ request }) => {
const session = await getSession(request);
try {
const { data } = await fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${session.get("token")}`
}
});
return json({
data
}, {
headers: {
"Set-Cookie": await commitSession(session),
}
});
} catch(error) {
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session)
}
});
}
}
And then in your route component something like this:
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.type === 'init') {
fetcher.load('/api/validate');
}
}, [fetcher]);
useEffect(() => {
if(fetcher.data?.someValue {
const timeout = setTimeout(() => fetcher.load('/api/validate'), 1 * 60 * 60 * 1000);
return () => clearTimeout(timeout);
}
},[fetcher.data]);

NextJS cookie token not being detected in getServerSideProps

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

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>

Prevent Multiple Token Refreshes With API React Hook

I have an API hook called useAPICall that has a callback call. This callback checks if a token stored in a reactn variable called auth is expired, refreshes it if necessary, then calls the fetch function.
I call it in my component like this:
const [api] = useAPICall();
useEffect(() => {
api.call('/api/settings/mine/').then(data => {
// do stuff here
});
}, []);
And it does work. It goes through the authentication flow and calls the API. But if I have useAPICall is multiple components that all try to call the API around the same time (such as a cold page load), then each instance of it calls the refresh token method because it's expired.
The auth info (access/refresh tokens) are stored in a reactn global variable auth such as below, inside the useAPICall.js hook
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {useDispatch, useGlobal} from 'reactn';
export function useAPICall() {
const [auth, setAuth] = useGlobal('auth');
const authRefreshSuccess = useDispatch('authRefreshSuccess');
async function refreshToken() {
console.log('Refreshing access token...');
const authResponse = await fetch('/api/auth/token/refresh/', {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({refresh: auth.refresh.token}),
headers: {
'Content-Type': 'application/json',
},
});
if (authResponse.ok) {
const authToken = await authResponse.json();
await authRefreshSuccess(authToken);
return authToken.access;
}
}
function isTokenExpired() {
if (localAuth.access)
return auth.access.exp <= Math.floor(Date.now() / 1000);
else
return false;
}
const call = useCallback(async (endpoint, options={headers: {}}) => {
console.log('performing api call');
token = undefined;
if (isTokenExpired())
token = await refreshToken();
else
token = localAuth.access.token;
const res = await fetch(endpoint, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
}
});
if (!res.ok)
throw await res.json();
return res.json();
}, []);
const anonCall = useCallback(async (endpoint, options={}}) => {
const res = await fetch(endpoint, options);
if (!res.ok)
throw await res.json();
return res.json();
}, []);
const api = useMemo(
() => ({
call,
anonCall,
}),
[call, anonCall,]
);
return [api]
}
How can I prevent them from firing off the refresh method multiple times?
If there's a better way (without redux) to have a universal API flow (where any API call would first check access token and refresh if necessary), then I'm willing to listen.
I managed to do this by storing a promise in a global variable.
let refreshPromise = null;
export function useAuthentication() {
async function getBearer() {
if (isExpired(jwt)) {
if (refreshPromise == null) {
refreshPromise = refresh().then((jwt) => {
refreshPromise = null;
return jwt;
});
}
await refreshPromise;
}
let authData = getAuthData();
if (authData && authData.accessToken) {
return `Bearer ${authData.accessToken}`;
}
return null;
}
const AuthenticationService = {
getBearer,
...
};
return AuthenticationService;
}
Hope this helps !

Resources