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]);
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. :)