mirror of
https://github.com/cheveguerra/whaticket-community.git
synced 2026-04-17 19:37:02 +00:00
feat: start using refresh tokens and better session handler
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/node": "5.22.3",
|
"@sentry/node": "5.22.3",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^2.16.1",
|
"date-fns": "^2.16.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/bluebird": "^3.5.32",
|
"@types/bluebird": "^3.5.32",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/cors": "^2.8.7",
|
"@types/cors": "^2.8.7",
|
||||||
"@types/express": "^4.17.8",
|
"@types/express": "^4.17.8",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/jsonwebtoken": "^8.5.0",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
secret: "mysecret",
|
secret: "mysecret",
|
||||||
expiresIn: "7d"
|
expiresIn: "15m",
|
||||||
|
refreshSecret: "myanothersecret",
|
||||||
|
refreshExpiresIn: "7d"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
import AuthUserService from "../services/UserServices/AuthUserSerice";
|
import AuthUserService from "../services/UserServices/AuthUserSerice";
|
||||||
|
import { SendRefreshToken } from "../helpers/SendRefreshToken";
|
||||||
|
import { RefreshTokenService } from "../services/AuthServices/RefreshTokenService";
|
||||||
|
|
||||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
|
||||||
const { token, user } = await AuthUserService({ email, password });
|
const { token, user, refreshToken } = await AuthUserService({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
SendRefreshToken(res, refreshToken);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
token,
|
token,
|
||||||
@@ -14,3 +22,20 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
|
|||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const token: string = req.cookies.jrt;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new AppError("Invalid session. Please login.", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newToken, refreshToken } = await RefreshTokenService(token);
|
||||||
|
|
||||||
|
SendRefreshToken(res, refreshToken);
|
||||||
|
|
||||||
|
return res.json({ token: newToken });
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Users", "tokenVersion", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Users", "tokenVersion");
|
||||||
|
}
|
||||||
|
};
|
||||||
23
backend/src/helpers/CreateTokens.ts
Normal file
23
backend/src/helpers/CreateTokens.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { sign } from "jsonwebtoken";
|
||||||
|
import authConfig from "../config/auth";
|
||||||
|
import User from "../models/User";
|
||||||
|
|
||||||
|
export const createAccessToken = (user: User): string => {
|
||||||
|
const { secret, expiresIn } = authConfig;
|
||||||
|
|
||||||
|
return sign(
|
||||||
|
{ usarname: user.name, profile: user.profile, id: user.id },
|
||||||
|
secret,
|
||||||
|
{
|
||||||
|
expiresIn
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRefreshToken = (user: User): string => {
|
||||||
|
const { refreshSecret, refreshExpiresIn } = authConfig;
|
||||||
|
|
||||||
|
return sign({ id: user.id, tokenVersion: user.tokenVersion }, refreshSecret, {
|
||||||
|
expiresIn: refreshExpiresIn
|
||||||
|
});
|
||||||
|
};
|
||||||
5
backend/src/helpers/SendRefreshToken.ts
Normal file
5
backend/src/helpers/SendRefreshToken.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
export const SendRefreshToken = (res: Response, token: string): void => {
|
||||||
|
res.cookie("jrt", token, { httpOnly: true });
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ const isAuth = (req: Request, res: Response, next: NextFunction): void => {
|
|||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
if (!authHeader) {
|
if (!authHeader) {
|
||||||
throw new AppError("Token not provided.", 403);
|
throw new AppError("Token was not provided.", 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, token] = authHeader.split(" ");
|
const [, token] = authHeader.split(" ");
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class User extends Model<User> {
|
|||||||
@Column
|
@Column
|
||||||
passwordHash: string;
|
passwordHash: string;
|
||||||
|
|
||||||
|
@Default(0)
|
||||||
|
@Column
|
||||||
|
tokenVersion: number;
|
||||||
|
|
||||||
@Default("admin")
|
@Default("admin")
|
||||||
@Column
|
@Column
|
||||||
profile: string;
|
profile: string;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Router, Request, Response } from "express";
|
import { Router } from "express";
|
||||||
import * as SessionController from "../controllers/SessionController";
|
import * as SessionController from "../controllers/SessionController";
|
||||||
import isAuth from "../middleware/isAuth";
|
|
||||||
import * as UserController from "../controllers/UserController";
|
import * as UserController from "../controllers/UserController";
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
@@ -9,8 +8,6 @@ authRoutes.post("/signup", UserController.store);
|
|||||||
|
|
||||||
authRoutes.post("/login", SessionController.store);
|
authRoutes.post("/login", SessionController.store);
|
||||||
|
|
||||||
authRoutes.get("/check", isAuth, (req: Request, res: Response) => {
|
authRoutes.post("/refresh_token", SessionController.update);
|
||||||
res.status(200).json({ authenticated: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default authRoutes;
|
export default authRoutes;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "dotenv/config";
|
|||||||
import "express-async-errors";
|
import "express-async-errors";
|
||||||
import express, { Request, Response, NextFunction } from "express";
|
import express, { Request, Response, NextFunction } from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
@@ -18,7 +19,13 @@ Sentry.init({ dsn: process.env.SENTRY_DSN });
|
|||||||
const upload = multer(uploadConfig);
|
const upload = multer(uploadConfig);
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors());
|
app.use(
|
||||||
|
cors({
|
||||||
|
credentials: true,
|
||||||
|
origin: process.env.FRONTEND_URL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(cookieParser());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(Sentry.Handlers.requestHandler());
|
app.use(Sentry.Handlers.requestHandler());
|
||||||
app.use(upload.single("media"));
|
app.use(upload.single("media"));
|
||||||
|
|||||||
47
backend/src/services/AuthServices/RefreshTokenService.ts
Normal file
47
backend/src/services/AuthServices/RefreshTokenService.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { verify } from "jsonwebtoken";
|
||||||
|
import AppError from "../../errors/AppError";
|
||||||
|
import ShowUserService from "../UserServices/ShowUserService";
|
||||||
|
import authConfig from "../../config/auth";
|
||||||
|
import {
|
||||||
|
createAccessToken,
|
||||||
|
createRefreshToken
|
||||||
|
} from "../../helpers/CreateTokens";
|
||||||
|
|
||||||
|
interface RefreshTokenPayload {
|
||||||
|
id: string;
|
||||||
|
tokenVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
newToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefreshTokenService = async (token: string): Promise<Response> => {
|
||||||
|
let decoded;
|
||||||
|
|
||||||
|
console.log(token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
decoded = verify(token, authConfig.refreshSecret);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError("Session expire. Please login.", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, tokenVersion } = decoded as RefreshTokenPayload;
|
||||||
|
|
||||||
|
const user = await ShowUserService(id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError("No user found with this ID.", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.tokenVersion !== tokenVersion) {
|
||||||
|
throw new AppError("Session revoked. Please login.", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newToken = createAccessToken(user);
|
||||||
|
const refreshToken = createRefreshToken(user);
|
||||||
|
|
||||||
|
return { newToken, refreshToken };
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { sign } from "jsonwebtoken";
|
|
||||||
|
|
||||||
import User from "../../models/User";
|
import User from "../../models/User";
|
||||||
import AppError from "../../errors/AppError";
|
import AppError from "../../errors/AppError";
|
||||||
import authConfig from "../../config/auth";
|
import {
|
||||||
|
createAccessToken,
|
||||||
|
createRefreshToken
|
||||||
|
} from "../../helpers/CreateTokens";
|
||||||
|
|
||||||
interface Request {
|
interface Request {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -12,6 +13,7 @@ interface Request {
|
|||||||
interface Response {
|
interface Response {
|
||||||
user: User;
|
user: User;
|
||||||
token: string;
|
token: string;
|
||||||
|
refreshToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthUserService = async ({
|
const AuthUserService = async ({
|
||||||
@@ -30,18 +32,13 @@ const AuthUserService = async ({
|
|||||||
throw new AppError("Incorrect user/password combination.", 401);
|
throw new AppError("Incorrect user/password combination.", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { secret, expiresIn } = authConfig;
|
const token = createAccessToken(user);
|
||||||
const token = sign(
|
const refreshToken = createRefreshToken(user);
|
||||||
{ usarname: user.name, profile: user.profile, id: user.id },
|
|
||||||
secret,
|
|
||||||
{
|
|
||||||
expiresIn
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
token
|
token,
|
||||||
|
refreshToken
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const ShowUserService = async (
|
|||||||
id: string | number
|
id: string | number
|
||||||
): Promise<User | undefined> => {
|
): Promise<User | undefined> => {
|
||||||
const user = await User.findByPk(id, {
|
const user = await User.findByPk(id, {
|
||||||
attributes: ["name", "id", "email", "profile"]
|
attributes: ["name", "id", "email", "profile", "tokenVersion"]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -182,6 +182,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/cookie-parser@^1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5"
|
||||||
|
integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
|
||||||
"@types/cors@^2.8.7":
|
"@types/cors@^2.8.7":
|
||||||
version "2.8.7"
|
version "2.8.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.7.tgz#ab2f47f1cba93bce27dfd3639b006cc0e5600889"
|
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.7.tgz#ab2f47f1cba93bce27dfd3639b006cc0e5600889"
|
||||||
@@ -939,6 +946,14 @@ content-type@~1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||||
|
|
||||||
|
cookie-parser@^1.4.5:
|
||||||
|
version "1.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49"
|
||||||
|
integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==
|
||||||
|
dependencies:
|
||||||
|
cookie "0.4.0"
|
||||||
|
cookie-signature "1.0.6"
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||||
|
|||||||
@@ -13,38 +13,58 @@ const useAuth = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
api.defaults.headers.Authorization = `Bearer ${JSON.parse(token)}`;
|
api.defaults.headers.Authorization = `Bearer ${JSON.parse(token)}`;
|
||||||
setIsAuth(true);
|
setIsAuth(true);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
const checkAuth = async () => {
|
setLoading(false);
|
||||||
if (
|
|
||||||
history.location.pathname === "/login" ||
|
api.interceptors.request.use(
|
||||||
history.location.pathname === "/signup"
|
config => {
|
||||||
) {
|
const token = localStorage.getItem("token");
|
||||||
setLoading(false);
|
if (token) {
|
||||||
return;
|
config.headers["Authorization"] = `Bearer ${JSON.parse(token)}`;
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await api.get("/auth/check");
|
|
||||||
if (res.status === 200) {
|
|
||||||
setIsAuth(true);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setLoading(false);
|
|
||||||
setIsAuth(false);
|
|
||||||
console.log(err);
|
|
||||||
if (err.response && err.response.data && err.response.data.error) {
|
|
||||||
toast.error(err.response.data.error);
|
|
||||||
}
|
}
|
||||||
|
setIsAuth(true);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
Promise.reject(error);
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
checkAuth();
|
|
||||||
}, [history.location.pathname]);
|
api.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async error => {
|
||||||
|
const originalRequest = error.config;
|
||||||
|
if (error.response.status === 403 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
|
const { data } = await api.post("/auth/refresh_token");
|
||||||
|
if (data) {
|
||||||
|
localStorage.setItem("token", JSON.stringify(data.token));
|
||||||
|
api.defaults.headers.Authorization = `Bearer ${data.token}`;
|
||||||
|
}
|
||||||
|
return api(originalRequest);
|
||||||
|
}
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("username");
|
||||||
|
localStorage.removeItem("profile");
|
||||||
|
localStorage.removeItem("userId");
|
||||||
|
api.defaults.headers.Authorization = undefined;
|
||||||
|
setIsAuth(false);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleLogin = async (e, user) => {
|
const handleLogin = async (e, user) => {
|
||||||
|
// setLoading(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const { data } = await api.post("/auth/login", user);
|
const { data } = await api.post("/auth/login", user);
|
||||||
@@ -55,6 +75,7 @@ const useAuth = () => {
|
|||||||
api.defaults.headers.Authorization = `Bearer ${data.token}`;
|
api.defaults.headers.Authorization = `Bearer ${data.token}`;
|
||||||
setIsAuth(true);
|
setIsAuth(true);
|
||||||
toast.success(i18n.t("auth.toasts.success"));
|
toast.success(i18n.t("auth.toasts.success"));
|
||||||
|
// setLoading(false);
|
||||||
history.push("/tickets");
|
history.push("/tickets");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@@ -65,6 +86,7 @@ const useAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = e => {
|
const handleLogout = e => {
|
||||||
|
// setLoading(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsAuth(false);
|
setIsAuth(false);
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
@@ -72,6 +94,7 @@ const useAuth = () => {
|
|||||||
localStorage.removeItem("profile");
|
localStorage.removeItem("profile");
|
||||||
localStorage.removeItem("userId");
|
localStorage.removeItem("userId");
|
||||||
api.defaults.headers.Authorization = undefined;
|
api.defaults.headers.Authorization = undefined;
|
||||||
|
// setLoading(false);
|
||||||
history.push("/login");
|
history.push("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios from "axios";
|
|||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: process.env.REACT_APP_BACKEND_URL,
|
baseURL: process.env.REACT_APP_BACKEND_URL,
|
||||||
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Reference in New Issue
Block a user