Skip to content Skip to sidebar Skip to footer

In Terms Of A Stateless Front-end Client, How Secure Is This Jwt Logic?

I don't think it matters for the purpose of this question what my exact setup is, but I just noticed this in my React and React Native apps, and suddenly realized they are not actu

Solution 1:

Your solution is not optimal as you stated you don't really check the validity of the user's token.

Let me detail how you can handle it:

1. Check token at start time

  1. Wait for the redux-persist to finish loading and injecting in the Provider component
  2. Set the Login component as the parent of all the other components
  3. Check if the token is still valid 3.1. Yes: Display the children 3.2. No: Display the login form

2. When the user is currently using the application

You should use the power of middlewares and check the token validity in every dispatch the user makes.

If the token is expired, dispatch an action to invalidate the token. Otherwise, continue as if nothing happened.

Take a look at the middleware token.js below.


I wrote a whole sample of code for your to use and adapt it if needed.

The solution I propose below is router agnostic. You can use it if you use react-router but also with any other router.

App entry point: app.js

See that the Login component is on top of the routers

importReactfrom'react';

import { Provider } from'react-redux';
import { browserHistory } from'react-router';
import { syncHistoryWithStore } from'react-router-redux';

import createRoutes from'./routes'; // Contains the routesimport { initStore, persistReduxStore } from'./store';
import { appExample } from'./container/reducers';

importLoginfrom'./views/login';

const store = initStore(appExample);

exportdefaultclassAppextendsReact.Component {
  constructor(props) {
    super(props);
    this.state = { rehydrated: false };
  }

  componentWillMount() {
    persistReduxStore(store)(() =>this.setState({ rehydrated: true }));
  }

  render() {
    const history = syncHistoryWithStore(browserHistory, store);
    return (
      <Providerstore={store}><Login>
          {createRoutes(history)}
        </Login></Provider>
    );
  }
}

store.js

The key to remember here is to use redux-persist and keep the login reducer in the local storage (or whatever storage).

import { createStore, applyMiddleware, compose, combineReducers } from'redux';
import { persistStore, autoRehydrate } from'redux-persist';
import localForage from'localforage';
import { routerReducer } from'react-router-redux';

import reducers from'./container/reducers';
import middlewares from'./middlewares';

const reducer = combineReducers({
  ...reducers,
  routing: routerReducer,
});

exportconstinitStore = (state) => {
  const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const store = createStore(
    reducer,
    {},
    composeEnhancers(
      applyMiddleware(...middlewares),
      autoRehydrate(),
    ),
  );

  persistStore(store, {
    storage: localForage,
    whitelist: ['login'],
  });

  return store;
};

exportconstpersistReduxStore = store => (callback) => {
  returnpersistStore(store, {
    storage: localForage,
    whitelist: ['login'],
  }, callback);
};

Middleware: token.js

This is a middleware to add in order to check wether the token is still valid.

If the token is no longer valid, a dispatch is trigger to invalidate it.

import jwtDecode from'jwt-decode';
import isAfter from'date-fns/is_after';

import * as actions from'../container/actions';

exportdefaultfunctioncheckToken({ dispatch, getState }) {
  returnnext =>(action) => {
    const login = getState().login;

    if (!login.isInvalidated) {
      const exp = newDate(jwtDecode(login.token).exp * 1000);
      if (isAfter(newDate(), exp)) {
        setTimeout(() =>dispatch(actions.invalidateToken()), 0);
      }
    }

    returnnext(action);
  };
}

Login Component

The most important thing here is the test of if (!login.isInvalidated).

If the login data is not invalidated, it means that the user is connected and the token is still valid. (Otherwise it would have been invalidated with the middleware token.js)

importReactfrom'react';
import { connect } from'react-redux';

import * as actions from'../../container/actions';

constLogin = (props) => {
  const {
    dispatch,
    login,
    children,
  } = props;

  if (!login.isInvalidated) {
    return<div>children</div>;
  }

  return (
    <formonSubmit={(event) => {
      dispatch(actions.submitLogin(login.values));
      event.preventDefault();
    }}>
      <inputvalue={login.values.email}onChange={event => dispatch({ type: 'setLoginValues', values: { email: event.target.value } })}
      />
      <inputvalue={login.values.password}onChange={event => dispatch({ type: 'setLoginValues', values: { password: event.target.value } })}
      />
      <button>Login</button></form>
  );
};

constmapStateToProps = (reducers) => {
  return {
    login: reducers.login,
  };
};

exportdefaultconnect(mapStateToProps)(Login);

Login actions

exportfunctionsubmitLogin(values) {
  return(dispatch, getState) => {
    dispatch({ type: 'readLogin' });
    returnfetch({}) // !!! Call your API with the login & password !!!
      .then((result) => {
        dispatch(setToken(result));
        setUserToken(result.token);
      })
      .catch(error =>dispatch(addLoginError(error)));
  };
}

exportfunctionsetToken(result) {
  return {
    type: 'setToken',
    ...result,
  };
}

exportfunctionaddLoginError(error) {
  return {
    type: 'addLoginError',
    error,
  };
}

exportfunctionsetLoginValues(values) {
  return {
    type: 'setLoginValues',
    values,
  };
}

exportfunctionsetLoginErrors(errors) {
  return {
    type: 'setLoginErrors',
    errors,
  };
}

exportfunctioninvalidateToken() {
  return {
    type: 'invalidateToken',
  };
}

Login reducers

import { combineReducers } from'redux';
import assign from'lodash/assign';
import jwtDecode from'jwt-decode';

exportdefaultcombineReducers({
  isInvalidated,
  isFetching,
  token,
  tokenExpires,
  userId,
  values,
  errors,
});

functionisInvalidated(state = true, action) {
  switch (action.type) {
    case'readLogin':
    case'invalidateToken':
      returntrue;
    case'setToken':
      returnfalse;
    default:
      return state;
  }
}

functionisFetching(state = false, action) {
  switch (action.type) {
    case'readLogin':
      returntrue;
    case'setToken':
      returnfalse;
    default:
      return state;
  }
}

exportfunctionvalues(state = {}, action) {
  switch (action.type) {
    case'resetLoginValues':
    case'invalidateToken':
      return {};
    case'setLoginValues':
      returnassign({}, state, action.values);
    default:
      return state;
  }
}

exportfunctiontoken(state = null, action) {
  switch (action.type) {
    case'invalidateToken':
      returnnull;
    case'setToken':
      return action.token;
    default:
      return state;
  }
}

exportfunctionuserId(state = null, action) {
  switch (action.type) {
    case'invalidateToken':
      returnnull;
    case'setToken': {
      const { user_id } = jwtDecode(action.token);
      return user_id;
    }
    default:
      return state;
  }
}

exportfunctiontokenExpires(state = null, action) {
  switch (action.type) {
    case'invalidateToken':
      returnnull;
    case'setToken':
      return action.expire;
    default:
      return state;
  }
}

exportfunctionerrors(state = [], action) {
  switch (action.type) {
    case'addLoginError':
      return [
        ...state,
        action.error,
      ];
    case'setToken':
      return state.length > 0 ? [] : state;
    default:
      return state;
  }
}

Feel free to ask me any question or if you need me to explain more on the philosophy.

Post a Comment for "In Terms Of A Stateless Front-end Client, How Secure Is This Jwt Logic?"