/* eslint-disable @typescript-eslint/no-var-requires */ import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); import { User } from '../models'; import { createToken, issueTokens, clearTokens } from '../helpers/auth'; import { NODE_ENV, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_SECRET } from '../config'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; } } const clientPublicKeys: any = {}; /** * Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol * @param req * @param res * @returns */ export const login1 = async (req: Request, res: Response) => { try { const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ email }).select('+salt +verifier'); if (!user) throw new Error('Failed to find user'); const server = new jsrp.server(); server.init( { salt: user.salt, verifier: user.verifier }, () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); clientPublicKeys[email] = { clientPublicKey, serverBInt: bigintConversion.bigintToBuf(server.bInt) }; return res.status(200).send({ serverPublicKey, salt: user.salt }); } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to start authentication process' }); } }; /** * Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted) * private key * @param req * @param res * @returns */ export const login2 = async (req: Request, res: Response) => { try { const { email, clientProof } = req.body; const user = await User.findOne({ email }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); if (!user) throw new Error('Failed to find user'); const server = new jsrp.server(); server.init( { salt: user.salt, verifier: user.verifier, b: clientPublicKeys[email].serverBInt }, async () => { server.setClientPublicKey(clientPublicKeys[email].clientPublicKey); // compare server and client shared keys if (server.checkClientProof(clientProof)) { // issue tokens const tokens = await issueTokens({ userId: user._id.toString() }); // store (refresh) token in httpOnly cookie res.cookie('jid', tokens.refreshToken, { httpOnly: true, path: '/', sameSite: 'strict', secure: NODE_ENV === 'production' ? true : false }); // return (access) token in response return res.status(200).send({ token: tokens.token, publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, tag: user.tag }); } return res.status(400).send({ message: 'Failed to authenticate. Try again?' }); } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to authenticate. Try again?' }); } }; /** * Log out user * @param req * @param res * @returns */ export const logout = async (req: Request, res: Response) => { try { await clearTokens({ userId: req.user._id.toString() }); // clear httpOnly cookie res.cookie('jid', '', { httpOnly: true, path: '/', sameSite: 'strict', secure: NODE_ENV === 'production' ? true : false }); } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to logout' }); } return res.status(200).send({ message: 'Successfully logged out.' }); }; /** * Return user is authenticated * @param req * @param res * @returns */ export const checkAuth = async (req: Request, res: Response) => res.status(200).send({ message: 'Authenticated' }); /** * Return new token by redeeming refresh token * @param req * @param res * @returns */ export const getNewToken = async (req: Request, res: Response) => { try { const refreshToken = req.cookies.jid; if (!refreshToken) { throw new Error('Failed to find token in request cookies'); } const decodedToken = ( jwt.verify(refreshToken, JWT_REFRESH_SECRET) ); const user = await User.findOne({ _id: decodedToken.userId }).select('+publicKey'); if (!user) throw new Error('Failed to authenticate unfound user'); if (!user?.publicKey) throw new Error('Failed to authenticate not fully set up account'); const token = createToken({ payload: { userId: decodedToken.userId }, expiresIn: JWT_AUTH_LIFETIME, secret: JWT_AUTH_SECRET }); return res.status(200).send({ token }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ message: 'Invalid request' }); } };