web-dev-qa-db-fra.com

Comment utiliser Redux pour actualiser le jeton JWT?

Notre React Native Redux app utilise des jetons JWT pour l'authentification. De nombreuses actions nécessitent de tels jetons et beaucoup d'entre elles sont distribuées simultanément, par exemple lors du chargement de l'application.

Par exemple.

componentDidMount() {
    dispath(loadProfile());
    dispatch(loadAssets());
    ...
}

loadProfile et loadAssets nécessitent JWT. Nous enregistrons le jeton dans l'état et AsyncStorage. Ma question est de savoir comment gérer l'expiration des jetons.

À l'origine, j'allais utiliser un middleware pour gérer l'expiration des jetons

// jwt-middleware.js

export function refreshJWTToken({ dispatch, getState }) {

  return (next) => (action) => {
    if (isExpired(getState().auth.token)) {
      return dispatch(refreshToken())
          .then(() => next(action))
          .catch(e => console.log('error refreshing token', e));
    }
    return next(action);
};

}

Le problème que j'ai rencontré était que l'actualisation du jeton se produira pour les actions loadProfile et loadAssets car au moment où elles seront distribuées, le jeton sera expiré. Idéalement, je voudrais "suspendre" les actions qui nécessitent une authentification jusqu'à ce que le jeton soit actualisé. Existe-t-il un moyen de le faire avec un middleware?

42
lanan

J'ai trouvé un moyen de résoudre ce problème. Je ne suis pas sûr que ce soit la meilleure approche et il y a probablement des améliorations qui pourraient y être apportées.

Mon idée originale reste: le rafraîchissement JWT est dans le middleware. Ce middleware doit précéder thunk si thunk est utilisé.

...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);

Ensuite, dans le code du middleware, nous vérifions si le jeton a expiré avant toute action asynchrone. S'il est expiré, nous vérifions également si nous actualisons déjà le jeton - pour pouvoir avoir un tel contrôle, nous ajoutons une promesse de nouveau jeton à l'État.

import { refreshToken } from '../actions/auth';

export function jwt({ dispatch, getState }) {

    return (next) => (action) => {

        // only worry about expiring token for async actions
        if (typeof action === 'function') {

            if (getState().auth && getState().auth.token) {

                // decode jwt so that we know if and when it expires
                var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;

                if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {

                    // make sure we are not already refreshing the token
                    if (!getState().auth.freshTokenPromise) {
                        return refreshToken(dispatch).then(() => next(action));
                    } else {
                        return getState().auth.freshTokenPromise.then(() => next(action));
                    }
                }
            }
        }
        return next(action);
    };
}

La partie la plus importante est la fonction refreshToken. Cette fonction doit envoyer une action lors de l'actualisation du jeton afin que l'état contienne la promesse du nouveau jeton. De cette façon, si nous envoyons plusieurs actions asynchrones qui utilisent l'authentification de jeton simultanément, le jeton n'est actualisé qu'une seule fois.

export function refreshToken(dispatch) {

    var freshTokenPromise = fetchJWTToken()
        .then(t => {
            dispatch({
                type: DONE_REFRESHING_TOKEN
            });

            dispatch(saveAppToken(t.token));

            return t.token ? Promise.resolve(t.token) : Promise.reject({
                message: 'could not refresh token'
            });
        })
        .catch(e => {

            console.log('error refreshing token', e);

            dispatch({
                type: DONE_REFRESHING_TOKEN
            });
            return Promise.reject(e);
        });



    dispatch({
        type: REFRESHING_TOKEN,

        // we want to keep track of token promise in the state so that we don't try to refresh
        // the token again while refreshing is in process
        freshTokenPromise
    });

    return freshTokenPromise;
}

Je me rends compte que c'est assez compliqué. Je suis également un peu inquiet à propos de la répartition des actions dans refreshToken qui n'est pas une action en soi. Veuillez me faire part de toute autre approche que vous connaissez qui gère l'expiration du jeton JWT avec redux.

35
lanan

Au lieu "d'attendre" la fin d'une action, vous pouvez plutôt conserver une variable de magasin pour savoir si vous récupérez toujours des jetons:

Réducteur d'échantillon

const initialState = {
    fetching: false,
};
export function reducer(state = initialState, action) {
    switch(action.type) {
        case 'LOAD_FETCHING':
            return {
                ...state,
                fetching: action.fetching,
            }
    }
}

Maintenant, le créateur d'action:

export function loadThings() {
    return (dispatch, getState) => {
        const { auth, isLoading } = getState();

        if (!isExpired(auth.token)) {
            dispatch({ type: 'LOAD_FETCHING', fetching: false })
            dispatch(loadProfile());
            dispatch(loadAssets());
       } else {
            dispatch({ type: 'LOAD_FETCHING', fetching: true })
            dispatch(refreshToken());
       }
    };
}

Ceci est appelé lorsque le composant est monté. Si la clé d'authentification est périmée, elle enverra une action pour définir fetching sur true et actualisera également le jeton. Notez que nous n'allons pas encore charger le profil ou les actifs.

Nouveau composant:

componentDidMount() {
    dispath(loadThings());
    // ...
}

componentWillReceiveProps(newProps) {
    const { fetching, token } = newProps; // bound from store

    // assuming you have the current token stored somewhere
    if (token === storedToken) {
        return; // exit early
    }

    if (!fetching) {
        loadThings()
    } 
}

Notez que maintenant vous essayez de charger vos objets sur le montage, mais également sous certaines conditions lors de la réception d'accessoires (cela sera appelé lorsque le magasin changera afin que nous puissions y conserver fetching). Lorsque la récupération initiale échoue, cela déclenchera le refreshToken. Lorsque cela est fait, il définira le nouveau jeton dans le magasin, mettant à jour le composant et donc appelant componentWillReceiveProps. S'il ne récupère pas encore (pas sûr que cette vérification soit nécessaire), il chargera les choses.

18
ZekeDroid

J'ai fait un simple emballage autour de redux-api-middleware pour reporter les actions et actualiser le jeton d'accès.

middleware.js

import { isRSAA, apiMiddleware } from 'redux-api-middleware';

import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'


export function createApiMiddleware() {
  const postponedRSAAs = []

  return ({ dispatch, getState }) => {
    const rsaaMiddleware = apiMiddleware({dispatch, getState})

    return (next) => (action) => {
      const nextCheckPostoned = (nextAction) => {
          // Run postponed actions after token refresh
          if (nextAction.type === TOKEN_RECEIVED) {
            next(nextAction);
            postponedRSAAs.forEach((postponed) => {
              rsaaMiddleware(next)(postponed)
            })
          } else {
            next(nextAction)
          }
      }

      if(isRSAA(action)) {
        const state = getState(),
              token = refreshToken(state)

        if(token && isAccessTokenExpired(state)) {
          postponedRSAAs.Push(action)
          if(postponedRSAAs.length === 1) {
            return  rsaaMiddleware(nextCheckPostoned)(refreshAccessToken(token))
          } else {
            return
          }
        }

        return rsaaMiddleware(next)(action);
      }
      return next(action);
    }
  }
}

export default createApiMiddleware();

Je garde les jetons en l'état et j'utilise un simple assistant pour injecter le jeton Acess dans les en-têtes de demande

export function withAuth(headers={}) {
  return (state) => ({
    ...headers,
    'Authorization': `Bearer ${accessToken(state)}`
  })
}

Alors redux-api-middleware les actions restent pratiquement inchangées

export const echo = (message) => ({
  [RSAA]: {
      endpoint: '/api/echo/',
      method: 'POST',
      body: JSON.stringify({message: message}),
      headers: withAuth({ 'Content-Type': 'application/json' }),
      types: [
        ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
      ]
  }
})

J'ai écrit le article et partagé le exemple de projet , qui montre le flux de travail du jeton d'actualisation JWT en action

5
kmmbvnr