From b2e16ce25d69bba53948048b89ce7f497a9d2fa7 Mon Sep 17 00:00:00 2001 From: adarshm11 Date: Thu, 4 Jun 2026 18:39:26 -0400 Subject: [PATCH] cookie setup --- api/main_endpoints/routes/Auth.js | 24 +++++++++- api/main_endpoints/util/passport.js | 9 +++- api/main_endpoints/util/token-functions.js | 2 + api/package-lock.json | 39 ++++++++++++++++ api/package.json | 1 + api/util/SceHttpServer.js | 2 + src/APIFunctions/Auth.js | 46 ++++++++++++------- src/Components/Navbar/AdminNavbar.js | 5 +- src/Components/Navbar/NavBarWrapper.js | 5 +- src/Pages/Login/Login.js | 1 - src/Pages/Overview/Overview.js | 3 +- .../Profile/MemberView/DeleteAccountModal.js | 5 +- src/index.js | 5 ++ 13 files changed, 121 insertions(+), 26 deletions(-) diff --git a/api/main_endpoints/routes/Auth.js b/api/main_endpoints/routes/Auth.js index f92555b97..daa7dbaa7 100644 --- a/api/main_endpoints/routes/Auth.js +++ b/api/main_endpoints/routes/Auth.js @@ -193,6 +193,14 @@ router.post('/login', async (req, res) => { details: { email: user.email } }).catch(logger.error); + res.cookie('jwtToken', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 2 * 60 * 60 * 1000 + }); + res.json({ token: `JWT ${token}` }); } catch (error) { @@ -209,7 +217,21 @@ router.post('/verify', async function(req, res) { if (decoded.status !== OK) { return res.sendStatus(decoded.status); } - res.status(OK).json(decoded.token); + // Return the cookie's token in the body so the React app can keep + // attaching Authorization: Bearer headers for API calls. External + // callers using a header to authenticate get back their own token. + const cookieToken = req.cookies && req.cookies.jwtToken; + const headerToken = req.headers.authorization + && req.headers.authorization.startsWith('Bearer ') + ? req.headers.authorization.split('Bearer ')[1] + : null; + const rawToken = cookieToken || headerToken; + res.status(OK).json({ ...decoded.token, token: rawToken ? `JWT ${rawToken.replace(/^JWT\s/, '')}` : undefined }); +}); + +router.post('/logout', function(req, res) { + res.clearCookie('jwtToken', { path: '/' }); + res.sendStatus(OK); }); router.post('/generateHashedId', async (req, res) => { diff --git a/api/main_endpoints/util/passport.js b/api/main_endpoints/util/passport.js index d5308128a..0b39e4394 100644 --- a/api/main_endpoints/util/passport.js +++ b/api/main_endpoints/util/passport.js @@ -3,9 +3,16 @@ const ExtractJwt = require('passport-jwt').ExtractJwt; const User = require('../models/User'); const { secretKey } = require('../../config/config.json'); +const cookieOrHeaderExtractor = function(req) { + if (req && req.cookies && req.cookies.jwtToken) { + return req.cookies.jwtToken; + } + return ExtractJwt.fromAuthHeaderWithScheme('jwt')(req); +}; + module.exports = function(passport) { const options = {}; - options.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('jwt'); + options.jwtFromRequest = cookieOrHeaderExtractor; options.secretOrKey = secretKey; passport.use( diff --git a/api/main_endpoints/util/token-functions.js b/api/main_endpoints/util/token-functions.js index 22db1475d..6b521b9ba 100644 --- a/api/main_endpoints/util/token-functions.js +++ b/api/main_endpoints/util/token-functions.js @@ -25,6 +25,8 @@ async function decodeToken(request, requiredAccessLevel = membershipState.NON_ME try { if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) { token = request.headers.authorization.split('Bearer ')[1]; + } else if (request.cookies && request.cookies.jwtToken) { + token = request.cookies.jwtToken; } else if (request.query && request.query.token) { token = request.query.token; } diff --git a/api/package-lock.json b/api/package-lock.json index 331aec1b9..5996b6f32 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -12,6 +12,7 @@ "axios": "^0.21.2", "bcryptjs": "^2.4.3", "bluebird": "^3.7.2", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^4.17.1", "form-data": "^4.0.0", @@ -1273,6 +1274,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -5092,6 +5115,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, + "cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "requires": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/api/package.json b/api/package.json index f680fd1ad..de41e316f 100644 --- a/api/package.json +++ b/api/package.json @@ -31,6 +31,7 @@ "axios": "^0.21.2", "bcryptjs": "^2.4.3", "bluebird": "^3.7.2", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^4.17.1", "form-data": "^4.0.0", diff --git a/api/util/SceHttpServer.js b/api/util/SceHttpServer.js index 0d6579ee4..560b22379 100644 --- a/api/util/SceHttpServer.js +++ b/api/util/SceHttpServer.js @@ -1,5 +1,6 @@ const express = require('express'); const bodyParser = require('body-parser'); +const cookieParser = require('cookie-parser'); const cors = require('cors'); const http = require('http'); const mongoose = require('mongoose'); @@ -35,6 +36,7 @@ class SceHttpServer { this.app.locals.email = 'test@test.com'; this.app.use(cors()); + this.app.use(cookieParser()); this.app.use( bodyParser.json({ // support JSON-encoded request bodies diff --git a/src/APIFunctions/Auth.js b/src/APIFunctions/Auth.js index ce58636a8..3a5f699ab 100644 --- a/src/APIFunctions/Auth.js +++ b/src/APIFunctions/Auth.js @@ -67,6 +67,7 @@ export async function loginUser(email, password) { try { const res = await fetch(url.href, { method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json' }, @@ -88,37 +89,50 @@ export async function loginUser(email, password) { } /** - * Checks if the user is signed in by evaluating a jwt token in local storage. + * Checks if the user is signed in by verifying the auth cookie with the API. * @returns {UserApiResponse} Containing information for * whether the user is signed or not */ export async function checkIfUserIsSignedIn() { let status = new UserApiResponse(); - const token = window.localStorage - ? window.localStorage.getItem('jwtToken') - : ''; - - // If there is not token in local storage, - // we cant do anything and return - if (!token) { - status.error = true; - return status; - } - const url = new URL('/api/Auth/verify', BASE_API_URL); try { const res = await fetch(url.href, { method: 'POST', + credentials: 'include', headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` + 'Content-Type': 'application/json' } }); if (res.ok) { - const result = await res.json(); - status.responseData = result; + const { token, ...rest } = await res.json(); + status.responseData = rest; status.token = token; + } else { + status.error = true; + } + } catch(err) { + status.error = true; + status.responseData = err; + } + return status; +} + +/** + * Logs the user out by calling the backend to clear the auth cookie. + * @returns {ApiResponse} Whether the logout call succeeded. + */ +export async function logoutUser() { + let status = new ApiResponse(); + const url = new URL('/api/Auth/logout', BASE_API_URL); + try { + const res = await fetch(url.href, { + method: 'POST', + credentials: 'include' + }); + if (!res.ok) { + status.error = true; } } catch(err) { status.error = true; diff --git a/src/Components/Navbar/AdminNavbar.js b/src/Components/Navbar/AdminNavbar.js index e18b75c14..87e10b500 100644 --- a/src/Components/Navbar/AdminNavbar.js +++ b/src/Components/Navbar/AdminNavbar.js @@ -1,6 +1,7 @@ import React from 'react'; import { useSCE } from '../context/SceContext'; import { membershipState } from '../../Enums'; +import { logoutUser } from '../../APIFunctions/Auth'; export default function UserNavBar(props) { const { user, setAuthenticated } = useSCE(); @@ -15,9 +16,9 @@ export default function UserNavBar(props) { return className; }; - function handleLogout() { + async function handleLogout() { setAuthenticated(false); - window.localStorage.removeItem('jwtToken'); + await logoutUser(); window.location.reload(); } diff --git a/src/Components/Navbar/NavBarWrapper.js b/src/Components/Navbar/NavBarWrapper.js index bc096fea3..6eb3e7740 100755 --- a/src/Components/Navbar/NavBarWrapper.js +++ b/src/Components/Navbar/NavBarWrapper.js @@ -2,6 +2,7 @@ import React from 'react'; import UserNavbar from './UserNavbar'; import AdminNavbar from './AdminNavbar'; import { useSCE } from '../context/SceContext'; +import { logoutUser } from '../../APIFunctions/Auth'; function NavBarWrapper({ enableAdminNavbar = false, @@ -10,10 +11,10 @@ function NavBarWrapper({ }) { const { user, setUser, setAuthenticated } = useSCE(); - function handleLogout() { + async function handleLogout() { setAuthenticated(false); setUser({}); - window.localStorage.removeItem('jwtToken'); + await logoutUser(); window.location.reload(); } diff --git a/src/Pages/Login/Login.js b/src/Pages/Login/Login.js index ea117fb7b..2f1d59f5c 100644 --- a/src/Pages/Login/Login.js +++ b/src/Pages/Login/Login.js @@ -15,7 +15,6 @@ export default function Login() { const loginStatus = await loginUser(email, password); if (!loginStatus.error) { setAuthenticated(true); - window.localStorage.setItem('jwtToken', loginStatus.token); if (queryParams.get('redirect')) { window.location.href = queryParams.get('redirect'); return; diff --git a/src/Pages/Overview/Overview.js b/src/Pages/Overview/Overview.js index fd5ca50eb..198f2d1a5 100644 --- a/src/Pages/Overview/Overview.js +++ b/src/Pages/Overview/Overview.js @@ -4,6 +4,7 @@ const svg = require('./SVG'); import { getAllUsers, deleteUserByID, getNewPaidMembersThisSemester } from '../../APIFunctions/User'; import { formatFirstAndLastName } from '../../APIFunctions/Profile'; import { getAllUsersValidVerifiedAndSubscribed } from '../../APIFunctions/User'; +import { logoutUser } from '../../APIFunctions/Auth'; // import { membershipState } from '../../Enums'; import ConfirmationModal from '../../Components/DecisionModal/ConfirmationModal.js'; @@ -39,7 +40,7 @@ export default function Overview() { } if (userToDel._id === user._id) { // logout - window.localStorage.removeItem('jwtToken'); + await logoutUser(); window.location.reload(); return window.alert('Self-deprecation is an art'); } diff --git a/src/Pages/Profile/MemberView/DeleteAccountModal.js b/src/Pages/Profile/MemberView/DeleteAccountModal.js index cb6022c57..0748707d8 100644 --- a/src/Pages/Profile/MemberView/DeleteAccountModal.js +++ b/src/Pages/Profile/MemberView/DeleteAccountModal.js @@ -1,5 +1,6 @@ import React from 'react'; import { deleteUserByID } from '../../../APIFunctions/User'; +import { logoutUser } from '../../../APIFunctions/Auth'; import { useSCE } from '../../../Components/context/SceContext'; export default function DeleteAccountModal(props) { @@ -14,8 +15,8 @@ export default function DeleteAccountModal(props) { if (!apiResponse.error) { bannerCallback('Account Deleted', 'success'); - setTimeout(() => { - window.localStorage.removeItem('jwtToken'); + setTimeout(async () => { + await logoutUser(); window.location.reload(); }, 2000); } else { diff --git a/src/index.js b/src/index.js index 312e36477..3a83076b7 100755 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,11 @@ import { checkIfUserIsSignedIn } from './APIFunctions/Auth'; import { SceContext } from './Components/context/SceContext'; import SearchModal from './Components/ShortcutKeyModal/SearchModal'; +// Auth migrated from localStorage to httpOnly cookies; sweep any stale entry. +if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.removeItem('jwtToken'); +} + function App() { const [authenticated, setAuthenticated] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(true);