Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

redux refresh token middleware

I have a middleware that can go to the refresh token before the next action runs and then run the other action when the access token expires.

But if I make more than one request at a time and the access token is over, I am trying to get as much refresh token as I am requesting. I am checking the isLoading property in state to prevent this. But after the request, isLoading value is true in the reducer, it seems to be false in the middleware, so it requests again and again.

I am sending refreshTokenPromise in fetching_refresh_token action, but I never get state.refreshTokenPromise, it is always undefined.

I definitely have a problem with the state.

So here is my question, how can I access the changing state value in middleware?

Refresh token middleware: (this version hits the endpoint multiple times)

import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
  FETCHING_REFRESH_TOKEN,
  FETCHING_REFRESH_TOKEN_SUCCESS,
  FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';

export default function tokenMiddleware({ dispatch, getState }) {
  return next => async (action) => {
    if (typeof action === 'function') {
      const state = getState();
      if (state) {
        const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
        if (expiresIn && isExpired(JSON.parse(expiresIn))) {
          if (!state.refreshToken.isLoading) {
            return refreshToken(dispatch).then(() => next(action));
          }
          return state.refreshTokenPromise.then(() => next(action));
        }
      }
    }
    return next(action);
  };
}

async function refreshToken(dispatch) {
  const clientId = await AsyncStorage.getItem('CLIENT_ID');
  const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
  const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');

  const userObject = {
    grant_type: 'refresh_token',
    client_id: JSON.parse(clientId),
    client_secret: JSON.parse(clientSecret),
    refresh_token: refreshToken1,
  };

  const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');

  const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
    await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
    await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
    await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));

    dispatch({
      type: FETCHING_REFRESH_TOKEN_SUCCESS,
      data: res,
    });

    return res ? Promise.resolve(res) : Promise.reject({
      message: 'could not refresh token',
    });
  }).catch((err) => {
    dispatch({
      type: FETCHING_REFRESH_TOKEN_FAILURE,
    });

    throw err;
  });

  dispatch({
    type: FETCHING_REFRESH_TOKEN,
    refreshTokenPromise,
  });

  return refreshTokenPromise;
}

function isExpired(expiresIn) {
  return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}

Refresh token reducer:

import {
  FETCHING_REFRESH_TOKEN,
  FETCHING_REFRESH_TOKEN_SUCCESS,
  FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';

const initialState = {
  token: [],
  isLoading: false,
  error: false,
};

export default function refreshTokenReducer(state = initialState, action) {
  switch (action.type) {
    case FETCHING_REFRESH_TOKEN:
      return {
        ...state,
        token: [],
        isLoading: true,
      };
    case FETCHING_REFRESH_TOKEN_SUCCESS:
      return {
        ...state,
        isLoading: false,
        token: action.data,
      };
    case FETCHING_REFRESH_TOKEN_FAILURE:
      return {
        ...state,
        isLoading: false,
        error: true,
      };
    default:
      return state;
  }
}

In the meantime, when I send it to the getState to refreshToken function, I get to the changing state value in the refreshToken. But in this version, the refresh token goes to other actions without being refreshed.

Monkey Patched version: (this version only makes 1 request)

import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
  FETCHING_REFRESH_TOKEN,
  FETCHING_REFRESH_TOKEN_SUCCESS,
  FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';

export default function tokenMiddleware({ dispatch, getState }) {
  return next => async (action) => {
    if (typeof action === 'function') {
      const state = getState();
      if (state) {
        const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
        if (expiresIn && isExpired(JSON.parse(expiresIn))) {
          if (!state.refreshTokenPromise) {
            return refreshToken(dispatch, getState).then(() => next(action));
          }
          return state.refreshTokenPromise.then(() => next(action));
        }
      }
    }
    return next(action);
  };
}

async function refreshToken(dispatch, getState) {
  const clientId = await AsyncStorage.getItem('CLIENT_ID');
  const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
  const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');

  const userObject = {
    grant_type: 'refresh_token',
    client_id: JSON.parse(clientId),
    client_secret: JSON.parse(clientSecret),
    refresh_token: refreshToken1,
  };

  if (!getState().refreshToken.isLoading) {
    const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');

    const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
      await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
      await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
      await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));

      dispatch({
        type: FETCHING_REFRESH_TOKEN_SUCCESS,
        data: res,
      });

      return res ? Promise.resolve(res) : Promise.reject({
        message: 'could not refresh token',
      });
    }).catch((err) => {
      dispatch({
        type: FETCHING_REFRESH_TOKEN_FAILURE,
      });

      throw err;
    });

    dispatch({
      type: FETCHING_REFRESH_TOKEN,
      refreshTokenPromise,
    });

    return refreshTokenPromise;
  }
}

function isExpired(expiresIn) {
  return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}

Thank you.

like image 673
ccoeder Avatar asked Oct 15 '17 19:10

ccoeder


2 Answers

I solved this problem using axios middlewares. I think is pretty nice.

import { AsyncStorage } from 'react-native';
import Config from 'react-native-config';
import axios from 'axios';
import { store } from '../store';
import { refreshToken } from '../actions/refreshToken'; // eslint-disable-line

const instance = axios.create({
  baseURL: Config.API_URL,
});

let authTokenRequest;

function resetAuthTokenRequest() {
  authTokenRequest = null;
}

async function getAuthToken() {
  const clientRefreshToken = await AsyncStorage.getItem('clientRefreshToken');

  if (!authTokenRequest) {
    authTokenRequest = store.dispatch(refreshToken(clientRefreshToken));

    authTokenRequest.then(
      () => {
        const {
          token: { payload },
        } = store.getState();

        // save payload to async storage
      },
      () => {
        resetAuthTokenRequest();
      },
    );
  }

  return authTokenRequest;
}

instance.interceptors.response.use(
  response => response,
  async (error) => {
    const originalRequest = error.config;

    if (
      error.response.status === 401
      && !originalRequest._retry // eslint-disable-line no-underscore-dangle
    ) {
      return getAuthToken()
        .then(() => {
          const {
            token: {
              payload: { 'access-token': accessToken, client, uid },
            },
          } = store.getState();

          originalRequest.headers['access-token'] = accessToken;
          originalRequest.headers.client = client;
          originalRequest.headers.uid = uid;
          originalRequest._retry = true; // eslint-disable-line no-underscore-dangle

          return axios(originalRequest);
        })
        .catch(err => Promise.reject(err));
    }

    return Promise.reject(error);
  },
);

export default instance;

If you have a problem, do not hesitate to ask.

like image 72
ccoeder Avatar answered Nov 23 '22 23:11

ccoeder


you could benefit from redux-sagas

https://github.com/redux-saga/redux-saga

redux-sagas is just background runner which monitors your actions and can react when some specific action is met. You can listen for all actions and react to all or you can react to only latest as mentioned in comments

https://redux-saga.js.org/docs/api/#takelatestpattern-saga-args

while redux-thunk is just another way to create actions on the go and wait for some I/O to happen and then create some more actions when I/O is done. It's more like synced code pattern and redux-sagas is more like multi-threaded. On main thread you have your app running and on background thread you have sagas monitors and reactions

like image 40
Lukas Liesis Avatar answered Nov 24 '22 00:11

Lukas Liesis