Skip to content

Commit

Permalink
Implemented Session Cookies with CSRF Protection
Browse files Browse the repository at this point in the history
  • Loading branch information
R1c4rdCo5t4 committed Jun 21, 2024
1 parent b462485 commit 71c1444
Show file tree
Hide file tree
Showing 34 changed files with 113 additions and 61 deletions.
1 change: 1 addition & 0 deletions code/client/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const firebaseConfig = {
};

const app = initializeApp(firebaseConfig);

const auth = getAuth(app);
const googleAuthProvider = new GoogleAuthProvider();
const githubAuthProvider = new GithubAuthProvider();
Expand Down
20 changes: 10 additions & 10 deletions code/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import Header from '@ui/components/header/Header';
import Workspace from '@ui/pages/workspace/Workspace';
import NotFound from '@ui/pages/notfound/NotFound';
import './App.scss';
import { ErrorProvider } from '@ui/contexts/error/ErrorContext';
import { ErrorProvider } from '@/contexts/error/ErrorContext';
import Sidebar from '@ui/components/sidebar/Sidebar';
import { WorkspaceProvider } from '@ui/contexts/workspace/WorkspaceContext';
import { WorkspaceProvider } from '@/contexts/workspace/WorkspaceContext';
import Workspaces from '@ui/pages/workspaces/Workspaces';
import { CommunicationProvider } from '@ui/contexts/communication/CommunicationContext';
import { CommunicationProvider } from '@/contexts/communication/CommunicationContext';
import Home from '@ui/pages/home/Home';
import AuthProvider from '@ui/contexts/auth/AuthContext';
import AuthProvider from '@/contexts/auth/AuthContext';
import Profile from '@ui/pages/profile/Profile';
import Landing from '@ui/pages/landing/Landing';

function App() {
return (
<div className="app">
<ErrorProvider>
<AuthProvider>
<CommunicationProvider>
<Router>
<CommunicationProvider>
<Router>
<AuthProvider>
<Header />
<div className="content">
<Routes>
Expand Down Expand Up @@ -66,9 +66,9 @@ function App() {
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</Router>
</CommunicationProvider>
</AuthProvider>
</AuthProvider>
</Router>
</CommunicationProvider>
</ErrorProvider>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Cookies from 'js-cookie';
import { auth, githubAuthProvider, googleAuthProvider } from '@config';
import { createContext, ReactNode, useEffect, useState } from 'react';
import { signInWithPopup, signOut, User, type AuthProvider as Provider } from 'firebase/auth';
import useError from '@ui/contexts/error/useError';
import { signInWithPopup, signOut, User, type AuthProvider as Provider, inMemoryPersistence } from 'firebase/auth';
import useError from '@/contexts/error/useError';
import useAuthService from '@services/auth/useAuthService';
import { useNavigate } from 'react-router-dom';

export type AuthContextType = {
currentUser: User | null;
Expand All @@ -29,14 +30,17 @@ export function AuthProvider({ children }: AuthProviderProps) {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const { publishError } = useError();
const { registerUser } = useAuthService();
const { sessionLogin, sessionLogout } = useAuthService();
const navigate = useNavigate();

const loginWithProvider = async (provider: Provider) => {
try {
auth.setPersistence(inMemoryPersistence); // for httpOnly cookies, do not persist any state client side
const { user } = await signInWithPopup(auth, provider);
await registerUser(user.uid, { name: user.displayName!, email: user.email! });
const token = await user.getIdToken(true);
Cookies.set('token', token, { expires: 1, secure: true, sameSite: 'Strict' });
const userInfo = { name: user.displayName!, email: user.email! };
const idToken = await user.getIdToken();
const csrfToken = Cookies.get('csrfToken')!;
await sessionLogin(idToken, csrfToken, user.uid, userInfo);
} catch (e) {
publishError(e as Error);
}
Expand All @@ -47,15 +51,16 @@ export function AuthProvider({ children }: AuthProviderProps) {
const loginWithGithub = () => loginWithProvider(githubAuthProvider);

const logout = async () => {
console.log('logging out');
await sessionLogout();
await signOut(auth);
Cookies.remove('token');
window.location.href = '/';
navigate('/');
};

const deleteAccount = async () => {
await sessionLogout();
await currentUser?.delete();
Cookies.remove('token');
window.location.href = '/';
navigate('/');
};

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from 'react';
import { AuthContext, AuthContextType } from '@ui/contexts/auth/AuthContext';
import { AuthContext, AuthContextType } from '@/contexts/auth/AuthContext';

export function useAuth(): AuthContextType {
return useContext(AuthContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Communication } from '@services/communication/communication';
import { useContext } from 'react';
import { CommunicationContext } from '@ui/contexts/communication/CommunicationContext';
import { CommunicationContext } from '@/contexts/communication/CommunicationContext';

export function useCommunication(): Communication {
return useContext(CommunicationContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from 'react';
import { ErrorContext } from '@ui/contexts/error/ErrorContext';
import { ErrorContext } from '@/contexts/error/ErrorContext';

const useError = () => useContext(ErrorContext);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { useState, createContext, useEffect } from 'react';
import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import useError from '@ui/contexts/error/useError';
import { useCommunication } from '@/contexts/communication/useCommunication';
import useError from '@/contexts/error/useError';
import { useParams } from 'react-router-dom';
import useWorkspaceService from '@services/workspace/useWorkspaceService';
import useResources from '@domain/workspaces/useResources';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from 'react';
import { WorkspaceContext } from '@ui/contexts/workspace/WorkspaceContext';
import { WorkspaceContext } from '@/contexts/workspace/WorkspaceContext';

const useWorkspace = () => useContext(WorkspaceContext);

Expand Down
2 changes: 1 addition & 1 deletion code/client/src/domain/workspaces/tree/useWorkspaceTree.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Resource } from '@notespace/shared/src/workspace/types/resource';
import { Resources } from '@ui/contexts/workspace/WorkspaceContext';
import { Resources } from '@/contexts/workspace/WorkspaceContext';

function useWorkspaceTree() {
const [nodes, setNodes] = useState<Resources>({});
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/domain/workspaces/tree/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TreeNode } from '@domain/workspaces/tree/types';
import { Resources } from '@ui/contexts/workspace/WorkspaceContext';
import { Resources } from '@/contexts/workspace/WorkspaceContext';

export function getTree(id: string, nodes: Resources): TreeNode {
const node = nodes[id];
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/domain/workspaces/useResources.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ResourceType, Resource } from '@notespace/shared/src/workspace/types/resource';
import useResourceService from '@services/resource/useResourceService';
import useSocketListeners from '@services/communication/socket/useSocketListeners';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { useCommunication } from '@/contexts/communication/useCommunication';
import useWorkspaceTree from '@domain/workspaces/tree/useWorkspaceTree';

function useResources() {
Expand Down
4 changes: 2 additions & 2 deletions code/client/src/domain/workspaces/useWorkspaces.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { WorkspaceInputModel, WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import useSocketListeners from '@services/communication/socket/useSocketListeners';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { useCommunication } from '@/contexts/communication/useCommunication';
import useWorkspaceService from '@services/workspace/useWorkspaceService';
import useError from '@ui/contexts/error/useError';
import useError from '@/contexts/error/useError';

function useWorkspaces() {
const { socket } = useCommunication();
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/domain/workspaces/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import { DocumentResource, ResourceType } from '@notespace/shared/src/workspace/types/resource';
import { Resources } from '@ui/contexts/workspace/WorkspaceContext';
import { Resources } from '@/contexts/workspace/WorkspaceContext';

export function sortWorkspaces(workspaces: WorkspaceMeta[], column: string, ascending: boolean): WorkspaceMeta[] {
return workspaces.sort((a, b) => {
Expand Down
13 changes: 8 additions & 5 deletions code/client/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { User, UserData } from '@notespace/shared/src/users/types';
import { HttpCommunication } from '@services/communication/http/httpCommunication';
import Cookies from 'js-cookie';

function authService(http: HttpCommunication) {
async function registerUser(id: string, data: UserData) {
await http.post('/users', { id, ...data });
async function sessionLogin(idToken: string, csrfToken: string, id: string, data: UserData) {
await http.post('/users/login', { idToken, csrfToken, id, ...data });
}

async function sessionLogout() {
await http.post('/users/logout');
}

async function getUser(id: string): Promise<User> {
Expand All @@ -17,11 +20,11 @@ function authService(http: HttpCommunication) {

async function deleteUser(id: string) {
await http.delete(`/users/${id}`);
Cookies.remove('token');
}

return {
registerUser,
sessionLogin,
sessionLogout,
getUser,
updateUser,
deleteUser,
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/services/auth/useAuthService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { useCommunication } from '@/contexts/communication/useCommunication';
import authService from '@services/auth/authService';

function useAuthService() {
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/services/resource/useResourceService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { useCommunication } from '@/contexts/communication/useCommunication';
import { useParams } from 'react-router-dom';
import resourceService from '@services/resource/resourceService';

Expand Down
2 changes: 1 addition & 1 deletion code/client/src/services/workspace/useWorkspaceService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { useCommunication } from '@/contexts/communication/useCommunication';
import workspaceService from '@services/workspace/workspaceService';

function useWorkspaceService() {
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/components/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './Header.scss';
import { useAuth } from '@ui/contexts/auth/useAuth';
import { useAuth } from '@/contexts/auth/useAuth';
import { Link } from 'react-router-dom';

function Header() {
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IoMenu, IoTime } from 'react-icons/io5';
import { Link } from 'react-router-dom';
import { RiMenuFold2Line, RiMenuFoldLine, RiTeamFill } from 'react-icons/ri';
import useWorkspace from '@ui/contexts/workspace/useWorkspace';
import useWorkspace from '@/contexts/workspace/useWorkspace';
import useSidebarState from '@ui/components/sidebar/hooks/useSidebarState';
import WorkspaceTree from '@ui/components/sidebar/components/workspace-tree/WorkspaceTree';
import { IoMdSettings } from 'react-icons/io';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import { getTree } from '@domain/workspaces/tree/utils';
import { ResourceType } from '@notespace/shared/src/workspace/types/resource';
import { DragEvent, useState } from 'react';
import { Resources, WorkspaceOperations } from '@ui/contexts/workspace/WorkspaceContext';
import { Resources, WorkspaceOperations } from '@/contexts/workspace/WorkspaceContext';
import './WorkspaceTree.scss';

type WorkspaceTreeProps = {
Expand Down
4 changes: 2 additions & 2 deletions code/client/src/ui/pages/document/Document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Editor from '@ui/pages/document/components/editor/Editor';
import useFugue from '@domain/editor/fugue/useFugue';
import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import useError from '@ui/contexts/error/useError';
import { useCommunication } from '@/contexts/communication/useCommunication';
import useError from '@/contexts/error/useError';
import useDocumentService from '@services/resource/useResourceService';
import useConnectors from '@domain/editor/connectors/useConnectors';
import { DocumentResource } from '@notespace/shared/src/workspace/types/resource';
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/pages/landing/Landing.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAuth } from '@ui/contexts/auth/useAuth';
import { useAuth } from '@/contexts/auth/useAuth';
import { useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
import googleIcon from '@assets/images/google-icon.png';
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import useAuthService from '@services/auth/useAuthService';
import { User } from '@notespace/shared/src/users/types';
import { formatDate } from '@/utils/utils';
import { useAuth } from '@ui/contexts/auth/useAuth';
import { useAuth } from '@/contexts/auth/useAuth';
import './Profile.scss';

function Profile() {
Expand Down
4 changes: 2 additions & 2 deletions code/client/src/ui/pages/workspace/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ResourceType } from '@notespace/shared/src/workspace/types/resource';
import DocumentView from '@ui/pages/workspace/components/DocumentView';
import useError from '@ui/contexts/error/useError';
import useWorkspace from '@ui/contexts/workspace/useWorkspace';
import useError from '@/contexts/error/useError';
import useWorkspace from '@/contexts/workspace/useWorkspace';
import { useEffect, useState } from 'react';
import DataTable from '@ui/components/table/DataTable';
import { FaPlus } from 'react-icons/fa';
Expand Down
4 changes: 2 additions & 2 deletions code/client/src/ui/pages/workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import useWorkspaces from '@domain/workspaces/useWorkspaces';
import WorkspaceView from '@ui/pages/workspaces/components/WorkspaceView';
import CreateWorkspaceDialog from '@ui/pages/workspaces/components/CreateWorkspaceDialog';
import useError from '@ui/contexts/error/useError';
import useError from '@/contexts/error/useError';
import DataTable from '@ui/components/table/DataTable';
import { MdDelete } from 'react-icons/md';
import { useEffect, useState } from 'react';
import { sortWorkspaces } from '@domain/workspaces/utils';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { useCommunication } from '@/contexts/communication/useCommunication';
import './Workspaces.scss';

function Workspaces() {
Expand Down
30 changes: 27 additions & 3 deletions code/server/src/controllers/http/handlers/usersHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ import { UsersService } from '@services/UsersService';
import { httpResponse } from '@controllers/http/utils/httpResponse';
import { UserData } from '@notespace/shared/src/users/types';
import { enforceAuth } from '@controllers/http/middlewares/authMiddleware';
import admin from 'firebase-admin';

function usersHandlers(service: UsersService) {
const registerUser = async (req: Request, res: Response) => {
const { id, ...data } = req.body;
const sessionLogin = async (req: Request, res: Response) => {
const { id, idToken, csrfToken, ...data } = req.body;
// guard against CSRF attacks
if (csrfToken !== req.cookies.csrfToken) {
httpResponse.unauthorized(res).send();
return;
}
// session login - create session cookie, verifying ID token in the process
try {
const expiresIn = 60 * 60 * 24 * 5 * 1000; // 5 days
const sessionCookie = await admin.auth().createSessionCookie(idToken, { expiresIn });
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie('session', sessionCookie, options);
} catch (e) {
httpResponse.unauthorized(res).send();
return;
}

// register user in database if not already registered
try {
const user = await service.getUser(id);
if (user) {
Expand All @@ -22,6 +40,11 @@ function usersHandlers(service: UsersService) {
}
};

const sessionLogout = async (req: Request, res: Response) => {
res.clearCookie('session');
httpResponse.noContent(res).send();
};

const getUser = async (req: Request, res: Response) => {
const { id } = req.params;
const user = await service.getUser(id);
Expand All @@ -47,7 +70,8 @@ function usersHandlers(service: UsersService) {
};

const router = PromiseRouter({ mergeParams: true });
router.post('/', registerUser);
router.post('/login', sessionLogin);
router.post('/logout', sessionLogout);
router.get('/:id', getUser);
router.get('/', getUsers);
router.put('/:id', enforceAuth, updateUser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,4 @@ export default function errorMiddleware(error: Error, req: Request, res: Respons
const message = response.statusCode === 500 ? 'Internal server error' : error.message;
response.send({ error: message });
ErrorLogger.logError(error.message);
console.error(error.stack);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NextFunction, Request, Response } from 'express';
import { ControllersLogger } from '@src/utils/logging';

const logger = ControllersLogger('ws');

export default async function loggingMiddleware(req: Request, res: Response, next: NextFunction) {
logger.logInfo(`Request: ${req.method} ${req.url}`);
next();
}
Loading

0 comments on commit 71c1444

Please sign in to comment.