I have a simple React app with a "/login" page in it. After the user logs in, I set the token with localstorage.setItem() and then navigate the user to the main page "/". In there I have different components, and those componenst use different API calls with the token.
All my API calls are in a "services.js" file.
Problem: After log in, the api calls are getting fired, and localstorage.getItem(token) returning null (i guess it is asnyc and takes some time to set the data), 401 Unauthorized
How can I make my app to wait for the localstorage item? What's the best way to acccess my bearer token in the services.js file, where all my api calls lay?
Login component:
const [token, setToken] = useContext(UserContext);
const navigate = useNavigate();
const asyncLocalStorage = {
getItem: async function () {
await null;
return localStorage.getItem('esteticaToken');
}
};
const submitLogin = async () => {
await logIn({username:username, password:pw})
.then((response)=>{
if (response.status === 401) {
setErrorMessage("Hibás felhasználónév, vagy jelszó!")
return
}
setLoading(false)
setToken(response.token);
//I try to wait for the token, and only after navigate to the main page (not working)
if (asyncLocalStorage.getItem() !== null) {
navigate("/");
}
})
.catch(()=>setLoading(false))
};
services.js:
//Main page API calls
export async function getProjects(){
let config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + localStorage.getItem('esteticaToken') //this is null
}
}
const { data } = await axios.get(API_URL+'/projects',config)
return data;
}
export async function getHits(){
let config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + localStorage.getItem('esteticaToken')
}
}
const { data } = await axios.get(API_URL+'/hits',config)
return data;
}
My main component, where I call for the api's
export default function MainPage(){
//React Query firing the api call from services.js after log in
const { isFetching: loading, data:dataProjects } = useQuery('projects', getAllProjects)
return(
...rest of the app
)
Related
I am making a React Application with a GraphQL backend. My code is as follows:
App.js
import { UserContext } from "./UserContext"
function App() {
const userQuery = useQuery(USER)
...
return {
<UserContext.Provider value={userQuery.data}>
LoginForm.js
import { UserContext } from "./UserContext"
function LoginForm() {
...
useEffect(() => {
if (result.data) {
const token = result.data.login.value
localStorage.setItem("user-token", token)
navigate("/")
}
}, [result.data])
index.js
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("mouldy-peppers-user-token")
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
But when the LoginForm navigates back to "/", the app component re-renders and user is null. Until I refresh the page, then it gets the logged-in user. The authorisation data sent with every graphQL query uses the local storage token. I would like to know how to get the query to use the new auth data without having to refresh the page.
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
After the user log into my application using Auth0, I'm getting other user settings from another api, however, this call does not seem to work, in fact it doesn't seem to like me adding the access_token from auth0.
I always end up with an error of: Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
The code that is called after login in:
export default observer(function LoginMenu() {
const { currentUserStore: { login }} = useStore();
const { loginWithRedirect, isAuthenticated, logout } = useAuth0();
useEffect(() => {
if (isAuthenticated) {
login()
}
}, [isAuthenticated])
const handleLogin = async () => {
await loginWithRedirect({
prompt: "login",
appState: {
returnTo: "/callback",
},
});
}
....
})
Login function:
login = async () => {
this.loading = true;
try {
console.log("Calling API to get currentUser");
var user = await agent.CurrentUserApi.get();
console.log("currentUser: ", user);
} catch(error) {
runInAction(() => this.loading = false);
throw error;
}
}
Agent interceptor:
axios.interceptors.request.use(config => {
const { getAccessTokenSilently } = useAuth0();
config.headers = config.headers ?? {};
const token = getAccessTokenSilently();
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config;
})
From everything I can see, the issue is related to how the interceptor is working, without the interceptor the api call is made, however, without a access token, so the call fails to authenticate.
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]);
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 !