reactjsauthenticationsetintervalbrowser-refresh

Set interval gets clear on browser refresh in my react application


I am using the setInterval function to do an API call to get refresh token after some interval. Every time if I refresh the browser setInterval timer gets clear and starts counting from zero. My access token gets expired and the refresh token never getting a call and user logged out. Is there any way to deal with this?

useEffect(() => {
  const interval = setInterval(() => { 
    setTokenByReSendingAuth(); //dispatch action
  }, 300000);

  return () => clearInterval(interval);
}, [setTokenByReSendingAuth]);

Solution

  • Using MERN:

    Heres your dependencies:

    jwt-decode: https://www.npmjs.com/package/jwt-decode

    passport-jwt: http://www.passportjs.org/packages/passport-jwt/

    passport: https://www.npmjs.com/package/passport

    jsonwebtoken: https://www.npmjs.com/package/jsonwebtoken

    bcryptjs: https://www.npmjs.com/package/bcryptjs

    Create an express server.js like this:

    const express = require("express");
    const cors = require("cors");
    const mongoose = require("mongoose");
    const path = require("path");
    const passport = require("passport");
    const db = require("./config/.env").mongoURI; //uri in .env file
    
    const app = express();
    const port = process.env.PORT || 5000;
    
    app.use(cors());
    app.use(express.json());
    
    mongoose.connect(db, {
      useNewUrlParser: true,
      useCreateIndex: true,
      useUnifiedTopology: true,
    });
    const connection = mongoose.connection;
    connection.once("open", () => {
      console.log("MongoDB database connection established successfully");
    });
    
    const myRouter = require("./routes/example.js");
    
    app.use(passport.initialize()); // used to attatch token to request headers
    require("./config/passport")(passport); 
    
    app.use("/example", myRouter);
    
    if (process.env.NODE_ENV === "production") {
      app.use(express.static("client/build"));
      app.get("*", (req, res) => {
        res.sendFile(path.resolve(__dirname, "client", "build", "index.html"));
      });
    }
    
    app.listen(port, () => {
      console.log(`Server is running on port: ${port}`);
    });

    Install your dependencies:

      "dependencies": {
        "axios": "^0.21.1",
        "bcryptjs": "^2.4.3",
        "concurrently": "^5.3.0",
        "cors": "^2.8.5",
        "dotenv": "^8.2.0",
        "express": "^4.17.1",
        "jsonwebtoken": "^8.5.1",
        "mongoose": "^5.11.8",
        "passport": "^0.4.1",
        "passport-jwt": "^4.0.0",
        "validator": "^13.5.2",
        "nodemon": "^2.0.7"
      },

    Add this to your package.json in whatever directory your server is in:

      "devDependencies": {
        "nodemon": "^2.0.7"
      },
      "scripts": {
        "start": "node server.js",
        "server": "nodemon server.js"
      },

    Now to the Auth part:

    Make a config folder for your passport and URI:

    In a .env file:

    module.exports = {
      mongoURI: "mongodb+srv://",
      secretOrKey: "abunchofrandomcharacterscreatedwithbcrypt",
    };

    Make a passport.js file:

    This adds the user's token to all request headers, it is automatically running since we used it in our server.js file.

    const JwtStrategy = require("passport-jwt").Strategy;
    const ExtractJwt = require("passport-jwt").ExtractJwt;
    const mongoose = require("mongoose");
    const User = mongoose.model("users");
    const keys = require("./.env");
    
    const opts = {};
    opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
    opts.secretOrKey = keys.secretOrKey;
    
    module.exports = (passport) => {
      passport.use(
        new JwtStrategy(opts, (jwt_payload, done) => {
          User.findById(jwt_payload.id)
            .then((user) => {
              if (user) {
                return done(null, user);
              }
              return done(null, false);
            })
            .catch((err) => console.log(err));
        })
      );
    };

    Make a middleware folder for your backend:

    Add an auth.js file:

    const jwt = require("jsonwebtoken");
    const config = require("../config/.env").secretOrKey;
    
    function authUser(req, res, next) {
      const authHeader = req.header("Authorization");
      const token = authHeader && authHeader.split(" ")[1];
      // Check for token
      if (!token)
        return res.status(401).json({ msg: "No token, authorization denied" });
    
      try {
        // Verify token
        const decoded = jwt.verify(token, config);
        // Add user from payload
        req.user = decoded;
        next();
      } catch (e) {
        res.status(400).json({ msg: "Token is not valid" });
      }
    }
    
    module.exports = {
      authUser,
    };

    This file is attached to your routes in the header, like this:

    router.post("/example/get", authUser, (req, res) => { 
      const { reqData } = req.body; //dont ever put user ids in here
        .catch((err) => {
          res.status(400).json({ msg: err });
        });
    });

    The route to login and register should look like this:

    const router = require("express").Router();
    var mongoose = require("mongoose");
    var Schema = mongoose.Schema;
    const { authUser } = require("../middleware/auth"); //used in the header of auth needed requests
    const bcrypt = require("bcryptjs");
    const jwt = require("jsonwebtoken");
    const keys = require("../config/.env");
    const validateRegisterInput = require("../validation/register"); //checks and validates  user register inputs
    const validateLoginInput = require("../validation/login"); //checks and validates user register inputs
    const User = require("../models/user");
    const { Permissions } = require("../models/permissions");
    
    //uses a middleware to validate register inputs, checks if user data exists in db, salts and hashes the password.
    
    router.post("/register", (req, res) => {
      const { errors, isValid } = validateRegisterInput(req.body);
    
      if (!isValid) {
        return res.status(400).json(errors);
      }
    
      User.findOne({ email: req.body.email }).then((user) => {
        if (user) {
          return res.status(400).json({ email: "Email already exists" });
        } else {
          const newUser = new User({
            firstName: req.body.firstName,
            lastName: req.body.lastName,
            email: req.body.email,
            password: req.body.password,
          });
    
          bcrypt.genSalt(10, (err, salt) => {
            bcrypt.hash(newUser.password, salt, (err, hash) => {
              if (err) throw err;
              newUser.password = hash;
              newUser
                .save()
                .then((user) => res.json(user))
                .catch((err) => console.log(err));
            });
          });
        }
      });
    });
    
    //login creds are req through this route, the details are compared to the db user collection, and the user data that matches the decoded password and username will be responed back through the token.
    
    router.post("/login", (req, res) => {
      const { errors, isValid } = validateLoginInput(req.body);
      if (!isValid) {
        return res.status(400).json(errors);
      }
    
      const email = req.body.email;
      const password = req.body.password;
    
      User.findOne({ email }).then((user) => {
        if (!user) {
          return res.status(404).json({ email: "Email not found" });
        }
    
        bcrypt.compare(password, user.password).then((isMatch) => {
          if (isMatch) {
            const payload = {
              id: user.id,
              firstName: user.firstName,
            };
    
            jwt.sign(
              payload,
              keys.secretOrKey,
              {
                expiresIn: 31556926, //expires in a year
              },
              (err, token) => {
                res.json({
                  success: true,
                  token: "Bearer " + token,
                });
              }
            );
          } else {
            return res
              .status(400)
              .json({ passwordincorrect: "Password incorrect" });
          }
        });
      });
    });
    
    module.exports = router;

    That's basically it for the backend auth routing side of things, but for the client to get there token in the browser you need to add this stuff to the client:

    In your index.js add this outside a component to run on every render no matter what:

    This checks to see if there's a jwttoken in the browser, it decodes it and sets the user data into the state to be used globally. It also redirects the user.

    import setAuthToken from "./utils/setAuthToken";
    import jwt_decode from "jwt-decode";
    
    if (localStorage.jwtToken) {
      // Set auth token header auth
      const token = localStorage.jwtToken;
      setAuthToken(token);
      // Decode token and get user info and exp
      const decoded = jwt_decode(token);
      // Set user and isAuthenticated
      store.dispatch(setCurrentUser(decoded)); // using redux, can easily also just use contextApi or something else
      // Check for expired token
      const currentTime = Date.now() / 1000; // to get in milliseconds
      if (decoded.exp < currentTime) {
        // Logout user
        store.dispatch(logoutUser());
    
        // Redirect to login
        window.location.href = "./";
      }
    }

    Create login, register and logout functions:

    import axios from "axios";
    import setAuthToken from "../../utils/setAuthToken";
    import jwt_decode from "jwt-decode";
    
    import { SET_CURRENT_USER } from "./authTypes"; //puts user data into state
    import { showSnackbar } from "../inventory/inventoryActions";
    
    export const registerUser = (userData) => (dispatch) => {
      axios
        .post("/users/register", userData)
        .then(() => {
        console.log("logged in")
        })
        .catch(() => {
         console.log("something wrong")
        });
    };
    
    export const loginUser = (userData) => (dispatch) => {
      axios
        .post("/users/login", userData)
        .then((res) => {
          const { token } = res.data;
          localStorage.setItem("jwtToken", token);
          setAuthToken(token);
          const decoded = jwt_decode(token);
          dispatch(setCurrentUser(decoded));
          dispatch(showSnackbar(`Successfully signed in!`, "success", 3000));
        })
        .catch(() => {
          console.log("somethings wrong")
        });
    };
    
    export const setCurrentUser = (decoded) => { // used in loginUser
      return {
        type: SET_CURRENT_USER,
        payload: decoded,
      };
    };
    
    //removes token from localstorage
    export const logoutUser = () => {
      return (dispatch) => {
        localStorage.removeItem("jwtToken");
        setAuthToken(false);
        dispatch(setCurrentUser({}));
      };
    };

    If you have any private components you only want logged in users to access use this PrivateRoute Component wrapper:

    This redirects any user not logged in to the home page

    import React from "react";
    import { Route, Redirect } from "react-router-dom";
    import { connect } from "react-redux";
    import PropTypes from "prop-types";
    
    const PrivateRoute = ({ component: Component, auth, ...rest }) => (
      <Route
        {...rest}
        render={(props) =>
          auth.isAuthenticated === true ? (
            <Component {...props} />
          ) : (
            <Redirect to="/" />
          )
        }
      />
    );
    
    PrivateRoute.propTypes = {
      auth: PropTypes.object.isRequired,
    };
    
    const mapStateToProps = (state) => ({
      auth: state.auth,
    });
    
    export default connect(mapStateToProps)(PrivateRoute);

    Use it as a react-router-dom element:

    <PrivateRoute exact path="/example" component={privateComponentExample} />
    

    If you have any questions, let me know. :)