Search code examples
reactjsreduxreact-reduxredux-thunk

Can I verify token in reducer without violating React design principles?


I am trying to figure out the structure of my jwt token authentication with react/redux. The way I have it set up now, I use a middleware that saves the token in localstorage when the action ATTEMPT_LOGIN_SUCCESS is dispatched (Note: The security of localstorage is besides the point).

For the initial state of whether the user is logged in, I currently get the token from local storage and verify it inside of my login reducer. My question is whether or not that is a place that I should indeed put that logic.

jwt-handler middleware for saving token:

import {ATTEMPT_LOGIN_SUCCESS} from "./dataTypes/login"

const jwtHandler = store => next => action => {

    switch(action.type){
        case ATTEMPT_LOGIN_SUCCESS:
            const token = action.payload;

            localStorage.setItem("token", token);
    }

    return next(action);
}

export default jwtHandler;

Login reducer (code in question is from the top to the initial state):

import {
    ATTEMPT_LOGIN_BEGIN, ATTEMPT_LOGIN_FAIL, ATTEMPT_LOGIN_SUCCESS
} from "../dataTypes/login";

import authenticateToken from "./authenticateToken";

const token = localStorage.getItem("token");
const isValid = authenticateToken(token); //Decodes and checks date to ensure valid

if(!isValid){
    localStorage.setItem("token", "");
}

const initialState = {
    loading:false,
    errorMessage: "",
    success:false,
    isLoggedIn: isValid
};

const loginReducer = (state = initialState, action) => {
    switch (action.type) {
        case ATTEMPT_LOGIN_BEGIN:
            return {
                ...state,
                loading:true,
                errorMessage:""
            };
        case ATTEMPT_LOGIN_SUCCESS:
            return {
                ...state,
                loading:false,
                success:true,
                token:action.payload
            };
        case ATTEMPT_LOGIN_FAIL:
            return {
                ...state,
                loading:false,
                errorMessage:action.payload
            };
        default:
            return state;
    }
};

export default loginReducer;

I feel like there may be a better place to put my is logged in logic, but I have yet to find a good answer online.


Solution

  • I prefer your middleware pattern, but bit more centralised:

    NOTE: this is opinionated and just an example sketch

    // middleware.js
    import {
        INIT_SESSION, ATTEMPT_LOGIN_BEGIN, ATTEMPT_LOGIN_FAIL, ATTEMPT_LOGIN_SUCCESS, LOGOUT
    } from "../dataTypes/login";
    
    import authenticateToken from "./authenticateToken";
    
    const jwtHandler = store => next => action => {
    
        switch(action.type){
            // INIT_SESSION handled only here not in any reducer
            case INIT_SESSION:
                const token = localStorage.getItem("token");
                const isValid = authenticateToken(token);
                if(valid){
                store.dispatch({type:ATTEMPT_LOGIN_SUCCESS, payload: token});
                } else {
                 localStorage.removeItem("token");
                }
                break;
    
            case ATTEMPT_LOGIN_SUCCESS:
                const newToken = action.payload;
                localStorage.setItem("token", newToken);
               break;
            case LOGOUT:
              localStorage.removeItem("token");
               break;
            default:
    
        }
    
        return next(action);
    }
    
    export default jwtHandler;
    
    
    // index.js
        const store = createStore();
        store.dispatch({type: INIT_SESSION});
    
        ReactDOM.render(<Provider store={store}>//...
    
    
    // reducer.js stays clean...