Prevent Multiple Token Refreshes With API React Hook - reactjs

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 !

Related

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

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]);

Access React context in an API service

In my React application I use the context API to store the user information through the useContext hook:
const AuthContext = createContext<AuthContextType>(null!);
const useAuth = () => useContext(AuthContext);
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User>();
// Implementations of values
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export { AuthProvider, useAuth };
Accessing the auth information works all fine and dandy in the components:
export default function CoolComponent() {
const auth = useAuth();
if (auth.user) {
// Do something
}
return <div>Hello {auth.user}</div>;
}
The thing is that my jwt-token is stored in the user object and I need it for my API calls in my service, but hooks are not allowed outside functional components. Can I circumvent this in a clever way? Some things that I can think of is to pass the token on every call to the service (not very DRY) or save the token in localStorage and then retrieve it from there in the service, but it seems unnecessary to store the same information in two different places?
Update:
Now with the service code:
const baseUrl = environment.apiUrl;
function getToken() {
// This is what I would like to get some help with
}
const headers = {
...(getToken() && { Authorization: `Bearer ${getToken()}` }),
"Content-Type": "application/json",
};
function getAllProjects(): Promise<IProject[]> {
return fetch(`${baseUrl}projects`, {
headers,
}).then((response) => response.json());
}
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
// + many more
export { getAllProjects, createProject };
Calling the service in a component:
useEffect(() => {
const fetchProjects = async () => {
setIsLoading(true);
try {
const allProjects = await getAllProjects();
setProjects(allProjects);
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
fetchProjects();
}, []);
The React documentation says that you cannot call hooks inside JavaScript functions.
What can you do?
Use custom hooks. rename functions as useCreateProject and return your function. Then you will be able to call useAuth inside your custom hook:
const useCreateProject =() =>{
const {user} = useAuth();
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
return createProject
}
Then call it like this:
const createProject = useCreateProject()
useEffect(() => {
const create = async () => {
setIsLoading(true);
try {
await createProject()
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
create();
}, []);
But my advice is to store the token on localStorage or in cookies. Context data will be lost when user refreshes page. However, if that is not case for you, you can continue using context.

How to fetch API data from Axios inside the getServerSideProps function in NextJS?

I'm building an App with Next.js, and I need to connect to specific API routes (set up with API Platform) and populate pages with the route's responses.
The API is working fine, but no matter how I try to implement my Axios call inside the getServerSideProps, I always get the same error, ECONNREFUSED, from my Node stack.
I tried to get the data from useEffect() and it's working fine, but I would like to know if there's a way to call it directly in getServerSideProps.
I'm using a Node container for Docker, and the routes are authenticated through a JWT Token (stored in the session and the client cookies for the server-side connection)
Here are is my code:
pages/accounts.js:
export async function getServerSideProps(context) {
const cookies = new Cookies(context.req.headers.cookie)
const adminToken = cookies.get('jwtToken')
const res = await getAllAccounts(adminToken)
return {
props: {
testdata: ''
},
}
}
lib/accounts.js:
import service from '../service'
export const getAllAccounts = async (adminToken) => {
const res = service({ jwtToken : adminToken }).get(`/accounts`).then((response) => {
}).catch((error) => {
console.dir(error)
})
}
HTTP wrapper:
import axios from 'axios';
import jwt_decode from "jwt-decode";
import mockAdapter from 'axios-mock-adapter';
const service = ({ jwtToken = null, store = null, mockURL = null, mockResponse = null, multipart = false } = {}) => {
const options = {};
options.baseURL = process.env.NEXT_PUBLIC_API_URL + '/api';
if(multipart === true) {
options.headers = {
'Content-Type': 'multipart/form-data'
}
} else {
options.headers = {
'Content-Type': 'application/ld+json',
accept: 'application/ld+json'
}
}
const instance = axios.create(options);
instance.interceptors.response.use(response => {
return response;
}, error => {
return Promise.reject(error);
})
if (mockURL !== null && mockResponse !== null) {
let mock = new mockAdapter(instance);
mock.onAny(mockURL).reply(200, mockResponse)
}
return instance;
};
export default service;
Through the error dump in the node stack, I managed to see that the request headers are correct, and the JWT correctly passed through.
Do not use Axios. Just use fetch().
Next.js polyfills fetch() by default on both the client and server, so you can just use it:
In addition to fetch() on the client-side, Next.js polyfills fetch() in the Node.js environment. You can use fetch() in your server code (such as getStaticProps/getServerSideProps) without using polyfills such as isomorphic-unfetch or node-fetch.
Source.
getServerSideProps works well with axios if we return response.data
export const getServerSideProps: GetStaticProps = async ({ params }) => {
const { brandName } = params as IParams;
const brandData = await $host.get(`api/brand/${brandName}`).then(response => response.data);
return {
props: {
brand: brandData,
},
};
};
Your problem is that your async method does not return a promise.
import service from '../service'
export const getAllAccounts = async (adminToken) => {
const res = service({ jwtToken : adminToken }).get(`/accounts`);
return res;
}
In my NextJS begining I followed this tutorial , and I changed fetch to axios in this way:
export const getStaticPaths = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await res.json();
const paths = data.map((ninja) => {
return {
params: { id: ninja.id.toString() },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context) => {
const id = context.params.id;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
const data = await res.json();
return {
props: { ninja: data },
};
};
I applied the change using useEffect()
useEffect(() => {
// const data = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
// const res = await data.json();
// setninja(res);
const fetchData = async () => {
const result = await axios(`https://jsonplaceholder.typicode.com/users/${id}`);
setninja(result.data);
};
fetchData();
console.log(data);
}, []);
I hope this info will be useful for you.
I Used Axios in getServerSideProps without any problems.
export const getServerSideProps: GetServerSideProps = async({
params,
res
}) => {
try {
const response = await axios.get(`/api/test`);
return {
props: {
data: response.data
},
}
} catch {
res.statusCode = 404;
return {
props: {}
};
}
};

How to create a custom Hooks in reactjs hooks?

I was trying to create a custom Hooks for handling input HTTP request from any component by simply calling the useCustomHooks but its getting failed and error is
Can not use keyword 'await' outside an async function
All i made is a handler that triggers http request custom component method
import { useState } from 'react';
import axios from "axios";
const useHttpReqHandler = () => {
const [result, setResult] = useState();
const apiMethod = async ({url , data , method}) => {
let options = {
method,
url,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
},
data
};
let response = await axios(options);
const UpdatedData = await response.data;
console.log(UpdatedData)
setResult(UpdatedData);
}
return [result, apiMethod];
};
export default useHttpReqHandler;
Now i can use this hook in my code and on any event handler just call callAPI returned from the hook like this
const MyFunc = () => {
const [apiResult, apiMethod] = useHttpReqHandler();
const captchValidation = () => {
const x = result.toString();;
const y = inputValue.toString();;
if ( x === y) {
apiMethod({url: 'some url here', data: {"email": email}, method: 'post'});
alert("success")
}
else {
alert("fail")
}
}
Is is a correct approch ? as i am beginner in Reactjs
Here is a working version:
useHttpReqHandler.jsx
import { useState } from 'react';
import axios from "axios";
const useHttpReqHandler = () => {
const [apiResult, setApiResult] = useState();
const apiMethod = async ({url , data , method}) => {
let options = {
method,
url,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
},
data
};
let response = await axios(options);
let responseOK = response && response.status === 200 && response.statusText === 'OK';
if (responseOK) {
const data = await response.data;
console.log(data)
setApiResult(data);
}
}
return [apiResult, apiMethod];
};
export default useHttpReqHandler;
What's important here:
await is called inside an async function (apiMethod)
The result is stored in a local state (apiResult)
The function returns an array [apiResult, apiMethod]
How to use it:
const [apiResult, apiMethod] = useHttpReqHandler();
apiMethod({url: 'some url here', data: {"email": email}, method: 'post'});
Render the result:
return {apiResult};
In my opinion, it is better to use .then with Axios. and try to create for each method different functions "Get/Post...", why because in the GET method you need to useEffect, but it can not be the same case in POST method. in GET method useHttpReqHandler.js
import { useEffect, useState } from "react";
import axios from "axios";
// GET DATA
const useHttpReqHandler = (url) => {
const [httpData, setHttpData] = useState();
useEffect(() => {
axios
.get(url)
.then((axiosData) => {
// Axios DATA object
setHttpData(axiosData.data);
// you can check what is in the object by console.log(axiosData);
// also you can change the state, call functions...
})
.catch((error) => {
console.log(error);
});
}, []);
return httpData;
};
export default useHttpReqHandler;
in your main file
import useHttpReqHandler from "...."
const MyFunc = () => {
const getData = useHttpReqHandler("URL");
return (
<div>
...
</div>
)
}
I hope it helps
the same thing will be with POSt, PUT, DELETE ... you will create functions for each method that will handle the Http req

Resources