Axios interceptor async call to get token throws error - reactjs

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.

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

`setRequestHeader` fails to execute with source code as a header value, axios and react

I've encountered a very strange problem, implementing axios interceptors for handling the expired token and refreshing it.
Setting
I'm implementing the JWT authentication with access and refresh tokens.
When the request is being sent to the API route that requires JWT authentication, request interceptor is here to make sure the headers contain an Authorization with Bearer token. The response interceptor checks if the new access token is needed, sends a request to refresh it, and finally updates the axios instance with the new config.
I wrote the code following the Dave Gray's video, but with TypeScript.
Problem
When testing this code, I set the refresh token lifetime to be very long, while setting the access token lifetime to be 5 seconds. After it expires, when the request to the protected route is happening, everything goes according to the plan—the logs from the backend contain two successfully completed requests: (1) to the protected route with 401 response and then (2) the refresh request.
At this point, I see the DOMException in the browser console (Chrome and Safari), which states that setRequestHeader fails to execute because a source code function is not a valid header value. Which, of course, it is not! The piece of code is this.
Code
const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: { "Content-Type": "application/json" },
withCredentials: true,
});
interface IRequestConfig extends AxiosRequestConfig {
sent?: boolean;
}
const useAxiosPrivate = () => {
const { auth } = useAuth()!;
const refresh = useRefreshToken();
React.useEffect(() => {
const requestInterceptor = axiosPrivate.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = config.headers ?? {};
if (!config.headers["Authorization"]) {
config.headers["Authorization"] = `Bearer ${auth?.token}`;
}
return config;
},
async (error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error);
}
);
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as IRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig?.sent) {
const newAccessToken = await refresh();
prevRequestConfig.sent = true;
prevRequestConfig.headers = prevRequestConfig.headers!;
prevRequestConfig.headers[
"Authorization"
] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequestConfig);
}
return Promise.reject(error);
}
);
return () => {
axiosPrivate.interceptors.request.eject(requestInterceptor);
axiosPrivate.interceptors.response.eject(responseInterceptor);
};
}, [auth, refresh]);
return axiosPrivate;
};
Error
DOMException: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function (header, parser) {
header = normalizeHeader(header);
if (!header) return undefined;
const key = findKey(this, header);
if (key) {
const value = this[key];
if (!parser) {
return value;
}
if (parser === true) {
return parseTokens(value);
}
if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isFunction(parser)) {
return parser.call(this, value, key);
}
if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isRegExp(parser)) {
return parser.exec(value);
}
throw new TypeError('parser must be boolean|regexp|function');
}
}' is not a valid HTTP header field value.
Research
So far, I've only found one similar issue in the internet, which has links to some others. One of them gives me a hint, that it may be the problem with how axios reads the configuration given to an axios instance.
I'm not sure if the problem is indeed somewhere in axios. I'll be extremely grateful for any useful thoughts on this problem!
I had the same problem, I solved it by manually giving value to axiosPrivate instead of axiosPrivate(prevRequestConfig).
const responseIntercept = axiosPrivate.interceptors.response.use(
response => response,
async (error)=>{
const prevRequest = error?.config;
if (error?.response?.status === 403 && !prevRequest?.sent){
const newAccessToken = await refresh();
// console.log(prevRequest);
return axiosPrivate({
...prevRequest,
headers: {...prevRequest.headers, Authorization: `Bearer ${newAccessToken}`},
sent: true
});
}
return Promise.reject(error);
}
);
Thanks to Daniel Dan's solution I could modify Dave's tutorial code:
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as AxiosRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig.sent) {
prevRequestConfig.sent = true;
const newAccessToken = await refresh();
/* --- The modified line --- */
prevRequestConfig.headers = { ...prevRequestConfig.headers };
/* ------------------------- */
prevRequestConfig.headers[
"Authorization"
] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequestConfig);
}
return Promise.reject(error);
}
);
Just Do This in your response interceptor
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as IRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig?.sent) {
const newAccessToken = await refresh();
prevRequestConfig.sent = true;
prevRequestConfig.headers["Authorization"] = `Bearer ${newAccessToken}`;
return axiosPrivate({
...prevRequestConfig,
...{
headers: prevRequestConfig.headers.toJSON(),
},
});
}
return Promise.reject(error);
}
);
When re-sending the request with updated creds, i.e axiosPrivate(config), the headers property needs to be a plain javascript Object but instead it is converted internally to be an AxiosInstance object.
To fix it, just pass a plain Javascript object to the headers property of your prevRequestConfig object.

React refresh token interceptor

I have a problem with refreshing a token. After I updated the token, a request should be made with my configurations, but this does not happen. The token is updated, but the second time the getIserInfo method is executed, it does not work.
My axios interceptors:
import axios from "axios";
import {getToken, logOut, refreshToken, setUser} from "#/services/auth.service";
const HTTP = axios.create({baseURL: process.env.REACT_APP_API_ENDPOINT});
HTTP.interceptors.request.use(
config => {
const token = getToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
)
HTTP.interceptors.response.use(
response => response,
async error => {
debugger;
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const response = await refreshToken();
const {data} = await response;
setUser(data);
} catch (e) {
logOut();
}
return HTTP(originalRequest);
}
return Promise.reject(error);
}
)
export default HTTP
After executing the token, the getUserInfo method should have worked again
Works after removing headers
delete originalRequest["headers"]
return HTTP(originalRequest);
The problem was in axios 1.0+. After removing and switching to version 0.27, everything worked. It's most likely a bug on their part.

Firebase Passwordless Email Authentication Error in Expo App

I am setting up passwordless Auth in my Expo app using the Firebase SDK. I've gotten to the point where emails are being sent to the user's desired address with a redirect link back to the app. When the user clicks the link, they are indeed redirected but they are not being authenticated. I am receiving a generic error in the console :
ERROR: [Error: An internal error has occurred.]
But I know that my credentials are passing through properly as I have logged them out when the function runs:
isSignInWithEmailLink:true, url: exp://10.0.0.27:19000?apiKey=AIzaSyAmpd5DdsjOb-MNfVH3MgF1Gn2nT3TBcnY&oobCode=7FJTfBjM28gkn6GfBSAdgAk7wOegg9k4D5poVcylhSYAAAF8BO5gHQ&mode=signIn&lang=en
I am calling useEffect on this function:
useEffect(() => {
signInWithEmailLink();
}, []);
Send Link To Email (WORKING)
const sendSignInLinkToEmail = (email) => {
return auth
.sendSignInLinkToEmail(email, {
handleCodeInApp: true,
url: proxyUrl,
})
.then(() => {
return true;
});
};
User clicks on a link from the email to redirect to the app to Authenticate (NOT WORKING)
const signInWithEmailLink = async () => {
const url = await Linking.getInitialURL();
if (url) {
handleUrl(url);
}
Linking.addEventListener('url', ({ url }) => {
handleUrl(url);
});
};
(RETURNING ERROR)
const handleUrl = async (url) => {
const isSignInWithEmailLink = auth.isSignInWithEmailLink(url);
console.log('isSignInWithEmailLink: ', isSignInWithEmailLink, 'url', url);
if (isSignInWithEmailLink) {
try {
await auth.signInWithEmailLink(email, url);
} catch (error) {
console.log('ERROR:', error);
}
}
};
Have you enabled email sign in in your firebase console?
Are you storing the email in localStorage? It looks undefined in your logic.
Your listener should be in the useEffect hook.
I've code my code working looking like this:
const handleGetInitialURL = async () => {
const url = await Linking.getInitialURL()
if (url) {
handleSignInUrl(url)
}
}
const handleDeepLink = (event: Linking.EventType) => {
handleSignInUrl(event.url)
}
useEffect(() => {
handleGetInitialURL()
Linking.addEventListener('url', handleDeepLink)
return () => {
Linking.removeEventListener('url', handleDeepLink)
}
}, [])
You should use the onAuthStateChanged within useEffect rather than try and log the user in at that point in time. useEffect is used when you need your page to re-render based on changes.
For example:
useEffect(() => {
// onAuthStateChanged returns an unsubscriber
const unsubscribeAuth = auth.onAuthStateChanged(async authenticatedUser => {
try {
await (authenticatedUser ? setUser(authenticatedUser) : setUser(null));
setIsLoading(false);
} catch (error) {
console.log(error);
}
});
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, []);
You should invoke the user sign in method through other means such as a button to sign in, or validate user credentials at some other point within your app.
custom function:
const onLogin = async () => {
try {
if (email !== '' && password !== '') {
await auth.signInWithEmailAndPassword(email, password);
}
} catch (error) {
setLoginError(error.message);
}
};
Source: https://blog.jscrambler.com/how-to-integrate-firebase-authentication-with-an-expo-app

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