kylecorbelli / redux-token-auth

Redux actions and reducers to integrate easily with Devise Token Auth
MIT License
154 stars 80 forks source link

Setting the headers on global axios does not work #28

Open mmahalwy opened 6 years ago

mmahalwy commented 6 years ago

Seems that either:

  1. Versions are not aligned
  2. Different modules of axios in the client
  3. Not sure :S
fkotsian commented 6 years ago

@mmahalwy I had a similar issue; As far as I can tell you can set the headers, but your underlying problem is that devise_token_auth itself changes the token on each request to the API. Therefore the headers change at each request.

Solution: Other packages like redux-auth (now outdated) wrap fetch/axios in order to re-set the headers on each request. You could do that yourself or set change_headers_on_each_request to false (though it should be noted that this allows anyone who intercepts a token to use it to make requests):

# config/initializers/devise_token_auth.rb
config.change_headers_on_each_request = false

Hope helps!

mmahalwy commented 6 years ago

@fkotsian yeah, that might be a solution. I think perhaps this library should be request-library agnostic (maybe I want to use fetch? or superagent? That way, it should be the onus on the application developer to decide how to store the credentials and default it in the headers.

fkotsian commented 6 years ago

Hm. Just kidding now. I'm looking and I don't seem to have access to the headers on axios at all. 👎

fkotsian commented 6 years ago

For future reference: copied solution from here - axios/unable to set or override global headers

// services/axios.js
import axios from 'axios'

export default function withStoredHeaders() {
  return axios.create({
    headers: {
      'access-token': localStorage.getItem('access-token'),
      'token-type': localStorage.getItem('token-type'),
      'client': localStorage.getItem('client'),
      'expiry': localStorage.getItem('expiry'),
      'uid': localStorage.getItem('uid'),
    }
  })
}

// subsequent: 
import axios from 'services/axios'
axios().get(...)

Yes this creates new axios instances on each action, would love thoughts on how to clean up.

mmahalwy commented 6 years ago

@fkotsian I'd recommend doing this actually:

// ./utils/auth.js
export const setupInterceptors = () => {
  axios.interceptors.request.use(
    (config) => {
      // eslint-disable-next-line
      config.headers = {
        ...config.headers,
        'access-token': store.get('auth')['access-token'],
        client: store.get('auth').client,
        uid: store.get('auth').uid,
      };

      return config;
    },
    error =>
      // Do something with request error
      Promise.reject(error),
  );

  axios.interceptors.response.use(
    (response) => {
      if (response.headers['access-token']) {
        setAuthHeaders(response.headers);
        persistAuthHeadersInDeviceStorage(response.headers);
      }

      return response;
    },
    error =>
      // Do something with response error
      Promise.reject(error),
  );
};

// wherever you'd like to use this
setupInterceptors();

Granted, I ended up rewriting some of this package for my own use in my own app, so I am using the store library and not localStorage directly.

fkotsian commented 6 years ago

Whoa, had no idea about interceptors! I like it!

rg-najera commented 6 years ago

Just in case someone was trying both approaches, this is what I came up with.

A few differences with my approach (might be what @mmahalwy had in mind):

  1. The interceptor is setup to work with a custom instance, therefore will automatically included if you use the instance everywhere in your app.

  2. I use storage from redux-persist/lib/storage/index - since that is already included as a module in this package instead of 'store'

  3. I use a custom version of setAuthHeaders and persistAuthHeadersInDeviceStorage where the values will only be added if they are returned as a response from the api server. This is important if you are changing headers on every request. Similar approach to my PR https://github.com/kylecorbelli/redux-token-auth/pull/46

utils/api.js

import axios from 'axios';
import storage from 'redux-persist/lib/storage/index';
import { persistor, store } from '../configureStore';
import {
  setAuthHeaders,
  persistAuthHeadersInDeviceStorage
} from './auth-tools';

/* Setup Keys Constant */
const authHeaderKeys = [
  'access-token',
  'token-type',
  'client',
  'expiry',
  'uid'
];

/* Utility Function to create header object from Storage
 * @returns {object}
 */
async function getHeadersFromStorage() {
  return {
    'access-token': await storage.getItem('access-token'),
    client: await storage.getItem('client'),
    uid: await storage.getItem('uid'),
    expiry: await storage.getItem('expiry')
  };
}

/*
 * The function that subscribes to our redux store changes
 * Sets up new api headers for the global axios settings.
 * https://github.com/axios/axios#global-axios-defaults
 * Caveat - this will change the headers for every axios request
 * not just the one used for the api instance.
 */

function setNewApiHeaders() {
  getHeadersFromStorage().then(headers => {
    authHeaderKeys.forEach(key => {
      axios.defaults.headers.common[key] = headers[key];
    });
  });
}

/*
 * Adds a change listener. https://redux.js.org/api/store#subscribe-listener
 * It will be called any time an action is dispatched, and some
 * part of the state tree may potentially have changed. You may then
 * call getState() to read the current state tree inside the callback.
 */

store.subscribe(() => setNewApiHeaders());

/*
 * Setups up a new instance of axios.
 * https://github.com/axios/axios#creating-an-instance
 *
 */
const api = axios.create({
  // baseURL: '/api/v1',
  headers: {
    'Content-Type': 'application/json'
  }
});

/*
 * Axios Request Interceptor.
 * Before each request, use the values in local storage to set new headers.
 */

api.interceptors.request.use(
  async config => {
    const newConfig = config;
    const newHeaders = await getHeadersFromStorage();
    newConfig.headers = {
      ...config.headers, // Spread config headers
      ...newHeaders // Spread new headers
    };
    return newConfig;
  },
  error =>
    // Reject the promise
    Promise.reject(error)
);

/*
 * Axios Respose Interceptor.
 * After each response, use the values in local storage to set new headers.
 */

api.interceptors.response.use(
  async response => {
    // Only change headers when access-token is returned in response
    if (response.headers['access-token']) {
      await setAuthHeaders(response.headers);
      await persistAuthHeadersInDeviceStorage(response.headers);
    }

    return response;
  },
  error =>
    // Reject the promise
    Promise.reject(error)
);

// Uncomment (for testing) to use the api in the console
if (typeof window !== 'undefined') {
  window.consoleApi = api;
}

export default api;

And these are the two customized tools setAuthHeaders & persistAuthHeadersInDeviceStorage that will persist the headers in storage as well as set the axios.defaults.headers.common value

authtools.js

import axios from 'axios';
import storage from 'redux-persist/lib/storage/index';

const authHeaderKeys = [
  'access-token',
  'token-type',
  'client',
  'uid',
  'expiry'
];

export const setAuthHeaders = headers => {
  authHeaderKeys.forEach(key => {
    storage.getItem(key).then(fromStorage => {
      const value = headers[key] || fromStorage;
      axios.defaults.headers.common[key] = value;
    });
  });
};

export const persistAuthHeadersInDeviceStorage = headers => {
  authHeaderKeys.forEach(key => {
    storage.getItem(key).then(fromStorage => {
      const value = headers[key] || fromStorage;
      storage.setItem(key, value);
    });
  });
};