I have a react/redux application that fetches a token from an api server. After the user authenticates I'd like to make all axios requests have that token as an Authorization header without having to manually attach it to every request in the action. I'm fairly new to react/redux and am not sure on the best approach and am not finding any quality hits on google.
Here is my redux setup:
// actions.js
import axios from 'axios';
export function loginUser(props) {
const url = `https://api.mydomain.com/login/`;
const { email, password } = props;
const request = axios.post(url, { email, password });
return {
type: LOGIN_USER,
payload: request
};
}
export function fetchPages() {
/* here is where I'd like the header to be attached automatically if the user
has logged in */
const request = axios.get(PAGES_URL);
return {
type: FETCH_PAGES,
payload: request
};
}
// reducers.js
const initialState = {
isAuthenticated: false,
token: null
};
export default (state = initialState, action) => {
switch(action.type) {
case LOGIN_USER:
// here is where I believe I should be attaching the header to all axios requests.
return {
token: action.payload.data.key,
isAuthenticated: true
};
case LOGOUT_USER:
// i would remove the header from all axios requests here.
return initialState;
default:
return state;
}
}
My token is stored in redux store under state.session.token.
I'm a bit lost on how to proceed. I've tried making an axios instance in a file in my root directory and update/import that instead of from node_modules but it's not attaching the header when the state changes. Any feedback/ideas are much appreciated, thanks.
There are multiple ways to achieve this. Here, I have explained the two most common approaches.
1. You can use axios interceptors to intercept any requests and add authorization headers.
// Add a request interceptor
axios.interceptors.request.use(function (config) {
const token = store.getState().session.token;
config.headers.Authorization = token;
return config;
});
2. From the documentation of axios you can see there is a mechanism available which allows you to set default header which will be sent with every request you make.
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
So in your case:
axios.defaults.headers.common['Authorization'] = store.getState().session.token;
If you want, you can create a self-executable function which will set authorization header itself when the token is present in the store.
(function() {
String token = store.getState().session.token;
if (token) {
axios.defaults.headers.common['Authorization'] = token;
} else {
axios.defaults.headers.common['Authorization'] = null;
/*if setting null does not remove `Authorization` header then try
delete axios.defaults.headers.common['Authorization'];
*/
}
})();
Now you no longer need to attach token manually to every request. You can place the above function in the file which is guaranteed to be executed every time (e.g: File which contains the routes).
Create instance of axios:
// Default config options
const defaultOptions = {
baseURL: <CHANGE-TO-URL>,
headers: {
'Content-Type': 'application/json',
},
};
// Create instance
let instance = axios.create(defaultOptions);
// Set the AUTH token for any request
instance.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
config.headers.Authorization = token ? `Bearer ${token}` : '';
return config;
});
Then for any request the token will be select from localStorage and will be added to the request headers.
I'm using the same instance all over the app with this code:
import axios from 'axios';
const fetchClient = () => {
const defaultOptions = {
baseURL: process.env.REACT_APP_API_PATH,
method: 'get',
headers: {
'Content-Type': 'application/json',
},
};
// Create instance
let instance = axios.create(defaultOptions);
// Set the AUTH token for any request
instance.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
config.headers.Authorization = token ? `Bearer ${token}` : '';
return config;
});
return instance;
};
export default fetchClient();
The best solution to me is to create a client service that you'll instantiate with your token an use it to wrap axios.
import axios from 'axios';
const client = (token = null) => {
const defaultOptions = {
headers: {
Authorization: token ? `Token ${token}` : '',
},
};
return {
get: (url, options = {}) => axios.get(url, { ...defaultOptions, ...options }),
post: (url, data, options = {}) => axios.post(url, data, { ...defaultOptions, ...options }),
put: (url, data, options = {}) => axios.put(url, data, { ...defaultOptions, ...options }),
delete: (url, options = {}) => axios.delete(url, { ...defaultOptions, ...options }),
};
};
const request = client('MY SECRET TOKEN');
request.get(PAGES_URL);
In this client, you can also retrieve the token from the localStorage / cookie, as you want.
Similarly, we have a function to set or delete the token from calls like this:
import axios from 'axios';
export default function setAuthToken(token) {
axios.defaults.headers.common['Authorization'] = '';
delete axios.defaults.headers.common['Authorization'];
if (token) {
axios.defaults.headers.common['Authorization'] = `${token}`;
}
}
We always clean the existing token at initialization, then establish the received one.
The point is to set the token on the interceptors for each request
import axios from "axios";
const httpClient = axios.create({
baseURL: "http://youradress",
// baseURL: process.env.APP_API_BASE_URL,
});
httpClient.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
config.headers.Authorization = token ? `Bearer ${token}` : '';
return config;
});
If you want to call other api routes in the future and keep your token in the store then try using redux middleware.
The middleware could listen for the an api action and dispatch api requests through axios accordingly.
Here is a very basic example:
actions/api.js
export const CALL_API = 'CALL_API';
function onSuccess(payload) {
return {
type: 'SUCCESS',
payload
};
}
function onError(payload) {
return {
type: 'ERROR',
payload,
error: true
};
}
export function apiLogin(credentials) {
return {
onSuccess,
onError,
type: CALL_API,
params: { ...credentials },
method: 'post',
url: 'login'
};
}
middleware/api.js
import axios from 'axios';
import { CALL_API } from '../actions/api';
export default ({ getState, dispatch }) => next => async action => {
// Ignore anything that's not calling the api
if (action.type !== CALL_API) {
return next(action);
}
// Grab the token from state
const { token } = getState().session;
// Format the request and attach the token.
const { method, onSuccess, onError, params, url } = action;
const defaultOptions = {
headers: {
Authorization: token ? `Token ${token}` : '',
}
};
const options = {
...defaultOptions,
...params
};
try {
const response = await axios[method](url, options);
dispatch(onSuccess(response.data));
} catch (error) {
dispatch(onError(error.data));
}
return next(action);
};
Sometimes you get a case where some of the requests made with axios are pointed to endpoints that do not accept authorization headers. Thus, alternative way to set authorization header only on allowed domain is as in the example below. Place the following function in any file that gets executed each time React application runs such as in routes file.
export default () => {
axios.interceptors.request.use(function (requestConfig) {
if (requestConfig.url.indexOf(<ALLOWED_DOMAIN>) > -1) {
const token = localStorage.token;
requestConfig.headers['Authorization'] = `Bearer ${token}`;
}
return requestConfig;
}, function (error) {
return Promise.reject(error);
});
}
Try to make new instance like i did below
var common_axios = axios.create({
baseURL: 'https://sample.com'
});
// Set default headers to common_axios ( as Instance )
common_axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// Check your Header
console.log(common_axios.defaults.headers);
How to Use it
common_axios.get(url).......
common_axios.post(url).......
export const authHandler = (config) => {
const authRegex = /^\/apiregex/;
if (!authRegex.test(config.url)) {
return store.fetchToken().then((token) => {
Object.assign(config.headers.common, { Authorization: `Bearer ${token}` });
return Promise.resolve(config);
});
}
return Promise.resolve(config);
};
axios.interceptors.request.use(authHandler);
Ran into some gotchas when trying to implement something similar and based on these answers this is what I came up with. The problems I was experiencing were:
If using axios for the request to get a token in your store, you need to detect the path before adding the header. If you don't, it will try to add the header to that call as well and get into a circular path issue. The inverse of adding regex to detect the other calls would also work
If the store is returning a promise, you need to return the call to the store to resolve the promise in the authHandler function. Async/Await functionality would make this easier/more obvious
If the call for the auth token fails or is the call to get the token, you still want to resolve a promise with the config
Related
I have an axios apiClient and I'm trying to get the email stored in localStorage into the header. In my component I tried using
response.headers['email']
and assigning it to an email variable but I'm getting undefined. I'm getting the email in localStorage but not able to get it in the component. Any help will be greatly appreciated.
Axios
const apiClient = axios.create({
baseURL: `${API}`,
withCredentials: false,
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
apiClient.interceptors.request.use(function (config) {
let token = JSON.parse(localStorage.getItem('token'));
let email = JSON.parse(localStorage.getItem('email'));
if (token) {
config.headers.Authorization = `Bearer ${token}`;
config.headers.email = `${email}`;
}
return config;
}, function (err) {
return Promise.reject(err);
});
Here is the method in my component where I need the email data
methods: {
getOrders(){
service.getAllOrders()
.then(response => {
this.email = response.headers['email'];
console.log("email:", this.email)
})
}
}
getAllOrders() does an axios get.
You set the request interceptor but you're checking the response header, and those are two different objects. The response is from the server and won't be affected by a request interceptor.
You can create a different interceptor for the response:
apiClient.interceptors.response.use((response) => {
response.headers.email = JSON.parse(localStorage.getItem('email'));
return response;
});
I have one api.js which exports by default an axios.create() instance:
import axios from 'axios'
import Cookies from 'js-cookie'
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Cookies.get('Token')}`,
Organization: Cookies.get('Organization'),
Company: Cookies.get('Company')
}
})
export default api
Then I import this in multiple files like this:
//api/users.js
import api from './api.js'
const methods = {
postUser (params) {
return api.post('/users', params)
},
getUser (id) {
return api.get('/users/' + id)
}
}
export default methods
However there will be some functions that should update the Cookies Organization and Company and I was wondering if is possible to update the default api instance and automatically update it in all imports that use it. I know a simple page refresh would work but I'm building a SPA and I would like to prevent screen to be manually refreshed.
You can add the headers dynamically, that way the cookies will be read on every request.
import axios from 'axios'
import Cookies from 'js-cookie'
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL,
timeout: 10000,
// Static headers
headers: {
'Content-Type': 'application/json',
},
transformRequest: [function (data, headers) {
// You may modify the headers object here
headers['Authorization'] = `Bearer ${Cookies.get('Token')}`
headers['Organization'] = Cookies.get('Organization')
headers['Company'] = Cookies.get('Company')
// Do not change data
return data;
}],
})
export default api
I would suggest to read about interceptor for axios. (https://github.com/axios/axios#interceptors)
A very basic example would be the following.
Lets assume your webservice would return a response http status 401 header.
You'd intercept the response with the following:
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// happy case its 2XX
return response;
}, async (error) => {
if (error.response.status === 401) {
// do some logic to retrieve a new JWT or Cookie.get()
const jwt = Cookies.get('Token');
const config = error.config;
config.headers['Authorization'] = `Bearer ${jwt}`;
}
return await axios.request(config);
});
The next request will then have an authorization header attached to the request header.
I'm building a NextJS app, and I'm trying the access a cookie so I can use it to set a Http Header for GraphQL Request, I am using apollo-link-context. This is the code to create the ApolloClient
function createApolloClient(initialState = {}) {
const httpLink = new HttpLink({ uri: `${baseUrl}/graphql`, credentials: 'same-origin', fetch })
const authLink = setContext((_, prevCtx) => {
let token = ''
if (typeof window === 'undefined') token = getCookieFromServer(authCookieName, REQ)
else token = getCookieFromBrowser(authCookieName)
return ({ headers: { 'Auth-Token': token } })
})
const client = new ApolloClient({
ssrMode: typeof window === 'undefined',
cache: new InMemoryCache().restore(initialState),
link: authLink.concat(httpLink)
})
return client
}
The issue here is that the getCookieFromServer function expects an Express Request as the second argument, so it can extract the cookie from req.headers.cookie, and I have no idea where I can get it from there.
I finally found a way. Whenever I send a request from the server (in PageComponent.getInitialProps), I set the header in the context, then I can access it from setContext:
PageComponent.getInitialProps = async (ctx) => {
...
const token = getCookieFromServer(authCookieName, ctx.req)
const { data } = await client.query({
query,
context: { headers: { 'Auth-Token': token } }
})
...
}
Then in setContext:
const authLink = setContext((_, prevCtx) => {
let headers = prevCtx.headers || {}
if (!headers['Auth-Token']) {
const token = getCookieFromBrowser(authCookieName)
headers = { ...headers, 'Auth-Token': token }
}
return ({ headers })
})
So if the header is already present in the previous context (which is the case when sent from the server), just use it. If it is not present (when sent from the browser), get the cookie from the browser and set it.
I hope it will help somebody one day.
In my VUE components, I use this async method to fetch data from API:
Components:
methods: {
async fetch() {
// console.log("##### WAIT ####");
const { data } = await staffRepository.getItems(this.teamId)
// console.log("##### END WAIT ####");
this.staffs = data
},
},
As you can see I use a custom repository to have a single axios code, this repository is imported in my previous component.
staffRepository:
export default {
getItems(nationId) {
return Repository.get(`page/${nationId}`)
},
}
And finally the main repository having the axios code:
Repository:
import axios from 'axios/index'
const baseDomain = 'https://my end point'
const baseURL = `${baseDomain}`
...
const headers = {
'X-CSRF-TOKEN': token,
// 'Access-Control-Allow-Origin': '*', // IF you ADD it add 'allowedHeaders' to ai server config/cors.php
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtoken}`,
}
export default axios.create({
baseURL,
withCredentials: withCredentials,
headers: headers,
})
This code works very nice when the jwtoken is a valid and NOT EXIPRED token.
The problem is when the token is expired or not found and my laravel 5.8 API returns the status code 401 (or other).
GET https://api.endpoint 401 (Unauthorized)
A good solution could catch the status code in staffRepository, the one having the get method.
MySolution: (not working)
getItems(nationId) {
return Repository.get(`page/${nationId}`)
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error.response.status) // <-- it works!
})
},
This could be nice because in error case the error in console is 401
But I can't use this solution because I have 2 nested promises: this one and the async fetch() into the component.
How can I fix it still using my repository environment?
I would suggest using the returned promise in your component, to make things more explicit:
methods: {
fetch() {
let data = null
staffRepository
.getItems(this.teamId)
.then(data => {
// do something with data
this.staffs = data
})
.catch(e => {
// do something with error, or tell the user
})
},
},
Edit - this will work perfectly fine, as your method in Repository will return a promise by default if you are using axios.
Try this: API code, where HTTP is an axios instance
export const get = (path: string): Promise<any> => {
return new Promise((resolve, reject) => {
HTTP.get(`${path}`)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(handleError(error));
});
});
};
// ***** Handle errors *****/
export function handleError(error) {
if (error.response) {
const status = error.response.status;
switch (status) {
case 400:
// do something
break;
case 401:
// do something, maybe log user out
break;
case 403:
break;
case 500:
// server error...
break;
default:
// handle normal errors here
}
}
return error; // Return the error message, or whatever you want to your components/vue files
}
The best practice solution is to use axios's interceptors:
import axios from "axios";
import Cookies from "js-cookie";
export default (options = {}) => {
let client = options.client || axios.create({ baseURL: process.env.baseUrl });
let token = options.token || Cookies.get("token");
let refreshToken = options.refreshToken || Cookies.get("refreshToken");
let refreshRequest = null;
client.interceptors.request.use(
config => {
if (!token) {
return config;
}
const newConfig = {
headers: {},
...config
};
newConfig.headers.Authorization = `Bearer ${token}`;
return newConfig;
},
e => Promise.reject(e)
);
client.interceptors.response.use(
r => r,
async error => {
if (
!refreshToken ||
error.response.status !== 401 ||
error.config.retry
) {
throw error;
}
if (!refreshRequest) {
refreshRequest = client.post("/auth/refresh", {
refreshToken
});
}
const { data } = await refreshRequest;
const { token: _token, refreshToken: _refreshToken } = data.content;
token = _token;
Cookies.set("token", token);
refreshRequest = _refreshToken;
Cookies.set("refreshToken", _refreshToken);
const newRequest = {
...error.config,
retry: true
};
return client(newRequest);
}
);
return client;
};
Take a look at client.interceptors.response.use. Also you should have a refreshToken. We are intercepting 401 response and sending post request to refresh our token, then waiting for a new fresh token and resending our previous request. It's very elegant and tested solution that fits my company needs, and probably will fit your needs too.
To send request use:
import api from './api'
async function me() {
try {
const res = await api().get('/auth/me')
// api().post('/auth/login', body) <--- POST
if (res.status === 200) { alert('success') }
} catch(e) {
// do whatever you want with the error
}
}
Refresh token: The refresh token is used to generate a new access
token. Typically, if the access token has an expiration date, once it
expires, the user would have to authenticate again to obtain an access
token. With refresh token, this step can be skipped and with a request
to the API get a new access token that allows the user to continue
accessing the application resources.
I might need some help how to use axios interceptors here. This is a part of the login screen. I want to have the interceptor refresh the token when its missing or its expired. I am not even sure it is the right place to have this code as token refresh will needs to be happening after the login too.
I have two axios instances. One for the API and one for auth0. I would need a way to connect them so they work together and reliable.
import token from "../../network/axios-auth0";
import client from "../../network/axios-client";
//Gets token and updates the redux token value
getToken() {
token.post("token", {
client_id: config.clientId,
client_secret: config.clientSecret,
audience: config.clientAudience,
grant_type: "client_credentials"
})
.then(response => {
this.props.onUpdateToken(response.data.access_token);
this.getGroup(response.data.access_token)
})
.catch(function (error) {
console.log(error);
})
}
//Gets token from redux and gets data
getGroup() {
client.interceptors.request.use(function (config) {
//call getToken() for new token or check old token
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
client.get("groups/5ca21cd1cb9134222e397f14", {data: {}, headers: {"Authorization":"Bearer " + this.props.bearerToken}})
.then(response => {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
}
onLoginPress(){
this.getToken();
}
render() {
return (
<View>
<TextInput placeholder={"user"}/>
<TextInput placeholder={"pass"}/>
<Button title={"login"} onPress={() => this.onLoginPress()}/>
</View>
)
}
const mapStateToProps = state => {
return {
token: state.values.token
};
};
const mapDispatchToProps = dispatch => {
return {
onUpdateToken: (value: string) => dispatch(updateToken(value))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
I mean the interceptor will be needed in every axios call (to prevent a request getting stuck because of old token) So it would be best to attach it to the instance. But then I don't know how to update the token in redux from the file where the instance is defined. It's probably not the best practice either. Here is one of the 2 instances defined.
import axios from "axios"
import config from "../config"
const client = axios.create({
baseURL: config.apiUrl,
headers: {
"Content-type": "application/json"
}
});
client.defaults.headers.get["Content-Type"] = "application/json";
export default client;
How would one do this professionally. I don't want to copy the interceptor for each call in the app.