In my application, I m using React with Redux and axios.
I am trying to refresh my JWT token if needed via axios interceptors.
I am able to intercept the requests, check if my access token is valid, and if it is expired I am able to request a new pair of tokens using my refresh token.
However, after I obtain my new access and refresh token, my app does not continue with the original request, but it stops and tries to render my components with errors (given that I am missing the response from the original request).
I believe there is some error in how I manage my async functions
I configured an axios instance as follows
in axiosHelper.js
import axios from 'axios';
import jwt_decode from "jwt-decode"
import dayjs from 'dayjs'
let authTokens = localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null
const instance = axios.create({ baseURL: 'http://localhost:8000'});
instance.interceptors.request.use(async (req) => {
if(!authTokens){
authTokens = localStorage.getItem('authTokens') ?
JSON.parse(localStorage.getItem('authTokens')) : null
}
const user = jwt_decode(authTokens.access)
const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1
if (!isExpired) return req
const {tokens} = await axios.post(`/refresh-url/`, {
refresh: authTokens.refresh
})
console.log('refreshed1')
const data = authTokens
data.refresh = tokens.refresh
data.access = tokens.access
console.log('refreshed2')
localStorage.setItem('authTokens',JSON.stringify(data))
req.headers.Authorization = `Bearer ${tokens.access}`
console.log('refreshed3')
return req
}, error => {
return Promise.reject(error);
});
export default instance;
In my actions I import the instance as in this example
in actions.js
import axios from '../axiosHelper.js '
...(here my imports and actions)...
const action = async () =>{
const config = { headers: {
'Content-type': 'application/json',
Authorization: `Bearer ${token.access}` // token from the redux store
}
const { data } = await axios.get(
`/api/my-url/`,
config
)
...(send data to reducer)...
}
then in my components, I call my actions via dispatch in useEffect.
in Home.js
const Home = (props) => {
const dispatch = useDispatch()
useEffect(() =>{
dispatch(action())
}, [dispatch])
return (<my component/>)
}
I believe the problem has something to do with dispatch. If the access token is valid, then everything works fine. However, if it is expired, my interceptor will send the request to refresh the tokens (which is successful), but as soon as the tokens are returned, my component tries to render and the rest of the code in the axios interceptor is ignored (I checked this with console log ...), and the original request is not sent (I checked that it does not arrive at my backend server).
I would really appreciate some help as I cannot figure out hoe to solve the problem!
I solved the issue by importing the redux store into my axiosHelper.js script, and then using store.dispatch(refreshaction(authTokens.refresh)) inside my axios interceptor.
Related
In the code below, I will call LoginAPI for authorization and writes token as a state(Login.e2e.ts ).By the way using axios interceptors.request in my axios.ts file.
My question is;
If I use below code logic, when I send request with customAxios in my project,everytime await LoginAPI.API.Signin.run() will run for every API request. Soon,I can have 100 API call. I don t want run every time await LoginAPI.API.Signin.run(),because I can take 429 error.
The new logic should be like this;
I want to take a token first and then use it until it expires. If the token expired then send a new request and get a new token. How can do this using JavaScript or TypeScript?
This is my Login.e2e.ts file
import api from "api/core"
import { expect } from "#playwright/test";
export const LoginAPI = {
States: {
token: {} as string
},
API: {
Signin: {
notes: "user login",
run: async () => {
let res: any = await api.test.LoginPost(process.env.NAME, process.env.PASS)
LoginAPI.States.token = res.data.token
expect(res.status).toBe(200)
},
},
},
};
This is my axios.ts file
import axios from "axios";
import { LoginAPI } from "../playwright/tests/login/login.api";
const customAxios = axios.create({
baseURL: process.env.ENV === '1' ? "https://test1" : process.env.ENV === '2' ? "https://test2" : "https://test3",
});
customAxios.interceptors.request.use(
async (config) => {
await LoginAPI.API.Signin.run()
if (config.headers) {
config.headers['Authorization'] = `Bearer ${LoginAPI.States.token}`;
return config;
}
return config;
},
(error) => {
Promise.reject(error);
}
);
export default customAxios
take a token first and then use it until it expires. If the token expired then send a new request and get a new token.The above code should be changed the this logic
I would suggest you to log in once and get cookies from the browser.context() and save them as JSON file then use this cookie state/session for the rest of the test. That way you won't have to log in every time for new tests or test suites.
More information using storageState(options) here in official docs.
Example of using storageState(options) in your global setup and teardown here in official docs.
So this is driving me nuts because it was working at one point, and I cannot find anything I changed that would make it not work.
Here is my simple axios post request. Based on my debugging, this is successfully making the network request and the server is returning a 422 error, which I expect (testing invalid login credentials). However, when attempting to log the axios reponse to the console, I get "undefined". It is also returning a resolved promise.
const login = (loginParams) => {
return axiosLoggedOut.post("/login", loginParams);
};
Here is the code for the axios instance I am using. I am only intercepting the request in this, and I know the request it making it to the server, so I don't think the issue would be in here:
import Cookies from "js-cookie";
import axios from 'axios';
const axiosLoggedOut = axios.create({
baseURL: "http://localhost",
withCredentials: true,
});
const requestTypes = ['post', 'put', 'delete'];
const onRequest = (config) => {
if (requestTypes.includes(config.method) && !Cookies.get('XSRF-TOKEN')) {
return setCSRFToken().then(() => config);
}
return config;
}
const setCSRFToken = () => {
return axiosLoggedOut.get('/sanctum/csrf-cookie');
}
axiosLoggedOut.interceptors.request.use(onRequest, null);
export default axiosLoggedOut;
Am I missing something obvious here?
I'm using NextJS 12.0.10 with next-redux-wrapper 7.0.5
And Axios custom instance to hold user JWT token saved in local storage and inject it with every request also to interceptors incoming error's in each response
The problem with this is that I simply cannot use the Axios instance inside the Next data fetching methods
Because there is no way to bring user JWT Token from local storage when invoking the request inside the server
Also, I cannot track the request in case of failure and send the refresh token quickly
I tried to use cookies but getStaticProps don't provide the req or resp obj
Should I use getServerSideProps always
axios.js
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 20000,
headers: {
common: {
Authorization: !isServer()
? localStorage.getItem("access_token")
? "JWT " + localStorage.getItem("access_token")
: null
: null,
accept: "application/json",
},
},
});
login-slice.js
export const getCurrentUser = createAsyncThunk(
"auth/getCurrentUser",
async (_, thunkApi) => {
try {
const response = await axiosInstance.get("api/auth/user/");
await thunkApi.dispatch(setCurrentUser(response.data));
return response.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
toast.error(error.message);
return thunkApi.rejectWithValue(error.message);
}
}
);
Page.jsx
export const getStaticProps = wrapper.getStaticProps((store) => async (ctx) => {
try {
await store.dispatch(getCurrentUser());
} catch (e) {
console.log("here", e);
}
return {
props: {},
};
});
Server side rendered technology is a one-way street if you follow the standard practise. You won't get any local details - being it cookies, local store or local states back to the server.
I would let the server build the DOM as much as it makes sense (ie with empty user data) and let the client fetch the data via useEffect.
All the endpoints in the backend require Authorization header. This header is stored in SecureStore.
Problem Statement
I want to load the Authorization header ( JWT Token ), for every API call after logging in.
Now this requires an async operation i.e.authStorage.getToken.
This is my client.js ( the apisauce client ).
client.js
import { create } from "apisauce";
import authStorage from "../auth/storage";
import IP from "../config/network";
const restoreToken = async () => {
return await authStorage.getToken("idToken");
};
const apiClient = (auth_token = "") =>
create({
baseURL: "http://" + IP + ":8990",
headers: { Authorization: auth_token }, // This I've added later
});
export default apiClient;
This is the PostsApi which uses apiClient to make the calls.
PostsApi.js
import apiClient from "./client";
const endpoint = "/api/";
const bookmarkEndpoint = "/bookmark/";
const getPosts = (last_id = 0, limit = 10) => {
return apiClient.get(endpoint + "?last_id=" + last_id + "&limit=" + limit);
};
const toggleBookmark = (item_id) => {
return apiClient.get(bookmarkEndpoint + "?item_id=" + item_id);
};
export default {
getPosts,
toggleBookmark,
};
My Understanding
I understand that if I can add the header in client.js itself, it would be injected everytime there's an API call.
I've tried :
const restoreToken = async () => {
return await authStorage.getToken("idToken");
};
But I am not sure how to call this async operation in client.js
Bonus Question
This token ( idToken ) would be reloaded every hour, so it's best to get the token from SecureStore everytime instead of saving it once.
Thanks.
Accepted answer and what worked for me
Worked for me
apisauce's setHeader : Documented here
Accepted answer is a detailed drilling of the axios setting up of headers. So if someone's using axios client directly they can see the accepted answer else, if you're an apisauce user, use the setHeader functionality provided with the library.
Cheers.
You will have to store your token with the state (can be redux or local state).
During save/refresh/reload the token, you will have set headers of the HTTP client.
You can set header using below command (example)
export const setAuthToken = (token) => {
apiClient.defaults.headers.common['Authorization'] = ''
delete apiClient.defaults.headers.common['Authorization']
if (token) {
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
}
Call the above function to set a token during reload/refresh/creation of token.
const restoreToken = async () => {
return await authStorage.getToken("idToken").then(token => setAuthToken(token));
};
This is how my simple function to synchronize the data looks like:
Function
import { getData } from './api/index'
export default async function synchronize (navigator) {
const data = await getData()
// ... then store data to local db...
}
I'm fetching some data from the server using an RESTful API:
getData
import { Alert, AsyncStorage } from 'react-native'
async function getData () {
try {
const lastSynched = await AsyncStorage.getItem('data.lastSynched')
const date = lastSynched ? Number(Date.parse(lastSynched)) / 1000 : 0
const token = await AsyncStorage.getItem('auth.token')
const uriBase = 'http://localhost:3000'
let response = await fetch(`${uriBase}/get-data/${date}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'x-access-token': token
}
})
let responseJson = await response.json()
return responseJson
} catch (error) {
Alert.alert('Error', 'Could not synchronize data')
}
}
export default getData
But now I'm using apollo graphQL and I do not understand how to get the data using a query as I'm using here a function (synchronize()) - not a component.
I think good start will be from this link. Here you have good examples how to use Apollo client to execute query and fetch data.
Maybe I don't understand properly what is issue but here is high level of Apollo usage.
First you will need to create Apollo client and supply at least URI to GraphQL endpoint.
import ApolloClient from "apollo-boost";
const client = new ApolloClient({
uri: "https://w5xlvm3vzz.lp.gql.zone/graphql"
});
Once you created client you should than execute your query with previously created client like in following:
import gql from "graphql-tag";
client
.query({
query: gql`
{
rates(currency: "USD") {
currency
}
}
`
})
.then(result => console.log(result));
Make sure that you installed apollo-boost react-apollo graphql-tag graphql packages. Also make sure that you wrap your query into GraphQL tag like this because it will compile your query.