In my React Native app, I use Axios Interceptors to set an Auth header Token on every request. Everything works correctly but when I log out and clear the stored token (By PURGING the Redux store) and log in again, the new token doesn't get set on the Axios Authorization Header (Gets empty).
This is how I'm setting the Auth header on Axios with Interceptors:
const App = () => {
const token = useSelector(selectToken); // Token state
const isLoggedOut = useSelector(selectIsLoggedOutState); // User logout state
// Show logout message and purge the store
useEffect(() => {
if (isLoggedOut) {
Toast.show({
type: 'success',
text1: logoutMessage,
});
persistor.purge();
}
}, [isLoggedOut]);
// Intercept on request
api.interceptors.request.use(
config => {
// Set authorization header
if (token) {
config.headers.common['Authorization'] = 'Bearer ' + token;
}
return config;
},
err => {
return Promise.reject(err);
},
);
// REST OF THE CODE
}
The most weird thing is that the header Token gets changed even if token is false in the if block, as if it doesn't even get checked.
I'd really appreciate any answer that might help me fix this weird problem.
Related
I'm having troubled sending an authenticated request to my API immediately after signing in to my Nextjs app using NextAuth. The request that is sent after signing in returns data for and unauthenticated user.
I believe the issue is that React Query is using a previous version of the query function with an undefined jwt (which means its unauthenticated). It makes sense because the query key is not changing so React Query does not think it's a new query, but, I was under the impression that signing in would cause loading to be set to true temporarily then back to false, which would cause React Query to send a fresh request.
I've tried invalidating all the queries in the app using queryClient, but that did not work. I've also used React Query Devtools to invalidate this specific query after signing in but it still returns the unauthenticated request. Only after refreshing the page does it actually send the authenticated request.
// useGetHome.js
const useGetHome = () => {
const [session, loading] = useSession();
console.log(`session?.jwt: ${session?.jwt}`);
return useQuery(
'home',
() => fetcher(`/home`, session?.jwt),
{
enabled: !loading,
},
);
}
// fetcher
const fetcher = (url, token) => {
console.log(`token: ${token}`);
let opts = {};
if (token) {
opts = {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}${url}`, opts);
if (!res.ok) {
const error = await res.json();
throw new Error(error.message);
}
return res.json();
}
// Home.js
const Home = () => {
const { data: home_data, isLoading, error } = useGetHome();
...
return(
...
)
}
Attached is the console immediately after signing in. You can see the the session object contains the jwt after signing in, but in the fetcher function it is undefined.
console after signing in
Any help here is appreciated. Is there a better way to handle authenticated requests using React Query and NextAuth? Thank you!
I have tried a similar situation here and struggled the same thing but the enabled property worked fine for me and it is good to go right now.
https://github.com/maxtsh/music
Just check my repo to see how it works, that might help.
I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},
I am building a SPA in Vue, and using axios interceptors to handle token management. Now, the SPA is not refreshing the token manually, it is receiving the token from the server only when it is refreshed, and then I update localStorage with the new token. I am passing the token in the headers on every API call.
My problem is that when the token comes back after it's been refreshed, I update localStorage in the response interceptor. But subsequent API calls are not aware of this new value in the store.
How can I retry the requests with the new value in localStorage?
I have tried, in the error block of the response interceptor, to grab the value from localStorage and manually update the headers and return the original request but this doesn't seem to work, as the subsequent API calls fail still with the old value.
axios.interceptors.request.use(config => {
const accessToken = window.localStorage.getItem('authToken')
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`
}
return Promise.resolve(config)
})
axios.interceptors.response.use(
response => {
if (response.data.meta && response.data.meta.tokens && response.data.meta.tokens.Bearer) {
const token = response.data.meta.tokens.Bearer
console.log({ 'setting new token': token })
window.localStorage.setItem('authToken', token)
}
return response
},
error => {
console.log(error)
const originalRequest = error.config
if (error.status && error.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const accessToken = window.localStorage.getItem('authToken')
originalRequest.headers.Authorization = `Bearer ${accessToken}`
return axios(originalRequest)
}
return Promise.reject(error)
}
)
I have an vue.js SPA application. My goal is to refresh the token if it was expired via axios interceptors. When user sends the request to api, I need to check token expire time at first, and if it was expired - refresh it and then complete user's request.
I got an refresh function:
const refreshToken = () => {
return new Promise((resolve, reject) => {
return axios.post('/api/auth/token/refresh/').then((response) => {
resolve(response)
}).catch((error) => {
reject(error)
})
})
}
And axios request interceptor:
axios.interceptors.request.use((config) => {
let originalRequest = config
if (jwt.isTokenExpired()) {
return api.refreshToken()
.then(res => {
if (res.data.error == 'TOKEN_BLACKLISTED' && res.headers.authorization) {
//get the token from headers without "Bearer " word
let token = res.headers.authorization.slice(7)
//save the token in localStorage
jwt.setToken(`"${token}"`)
//refresh "Authorization" header with new token
api.setHeader()
return Promise.resolve(originalRequest)
} else {
jwt.destroyToken()
jwt.destroyExpiredTime()
store.dispatch('auth/destroyToken')
router.push({name: 'login'})
return Promise.reject()
}
})
}
return config
}, (err) => {
return Promise.reject(err)
})
But now it goes to infinite loop. How to fix it?
In this case, you'd better make two instances of axios:
the first for authorization-related endpoints (those that do not require an access token), for example, axiosAuth.
In your example - axiosAuth.post('/api/auth/token/refresh/')
the second for the authorized part of your project, for example axiosApi.
In your example - axiosApi.interceptors.request.use
You must install the interceptor for the second instance, in this case the call to refresh_token will not trigger the interceptor in which it is installed, as you would expect
You are making a request in the interceptor. Which means that the token is stil expired when the interceptor is called on the request to the refresh url. So what you could do is to check in your interceptor if the URL is set to your refresh token URL and then just resolve the original request.
How to create middleware which will catch all errors, for example I have request which required token, token can expired or damaged, so I need catch this errors on every request and be able to call queries and mutations.
For example:
On expired token, I must refetch token and repeat request.
On token damaged, I must logout user and refetch all queries.
And type of error witch I need to handle can be many.
In(react-apollo docs)
networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.status === 401) {
logout();
}
next();
}
}]);
I can't access to graphql error, and call queries or mutations.
You can check to see if you have a token before every request is sent. If you do not have a token, you should handle that somewhere else in your application or potentially fetch another straight from the middleware function. You could make higher order component that wraps all of your components that must have a token. If for some reason there is no token, you can fetch another one and store it to localStorage if you are using the browser or asyncstorage if you are using React Native. Once you've assigned it to localStorage or asyncStorage, this middleware code snippet below will check for the token before every request you send, this includes all queries and mutations. If you find that your user doesn't have a token, you could also redirect them in your component them to a page where they must login again and then from there set the token to localstorage or asynstorage. Once again the apollo client's middleware will have access to it that way.
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { checkForSessionToken } from '../../utils/authentication';
const networkInterface = createNetworkInterface({
uri: 'https://localhost:4000'
});
networkInterface.use([{
applyMiddleware(req, next) {
// Create the header object if needed.
if (!req.options.headers) {
req.options.headers = {};
}
// get the authentication token from Async storage
// and assign it to the request object
checkForSessionToken()
.then(SESSION_TOKEN => {
if (SESSION_TOKEN === null || SESSION_TOKEN === undefined {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', SESSION_TOKEN);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
} else {
req.options.headers.Authorization = `Bearer ${SESSION_TOKEN}`;
}
next();
})
.catch(error => {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', token);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
next();
})
}
}]);
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
export default client;