diff --git a/src/helpers/cookies.ts b/src/helpers/cookies.ts new file mode 100644 index 0000000..23ff81d --- /dev/null +++ b/src/helpers/cookies.ts @@ -0,0 +1,40 @@ +import { Response } from 'express'; + +import { Optional } from '../types'; +import { ValidationError } from '../errors'; + +const setCookie = (res: Response, params: { + name: string, + value: string, + expiresAt?: Optional<Date>, + path?: string, +}) => { + params.path = params.path || '/'; + if (params.expiresAt) { + try { + params.expiresAt = new Date(params.expiresAt); + } catch (error) { + throw new ValidationError(`Invalid expiresAt: ${error}`); + } + } + + let cookie = `${params.name}=${params.value}; Path=${params.path}; HttpOnly; SameSite=Strict`; + if (params.expiresAt) { + cookie += `; Expires=${params.expiresAt.toUTCString()}`; + } + + res.setHeader('Set-Cookie', cookie); +} + +const clearCookie = (res: Response, name: string) => { + setCookie(res, { + name, + value: '', + expiresAt: new Date(0), + }); +} + +export { + clearCookie, + setCookie, +}; diff --git a/src/routes/api/Route.ts b/src/routes/api/Route.ts index 611ee5f..d1b677f 100644 --- a/src/routes/api/Route.ts +++ b/src/routes/api/Route.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import Route from '../Route'; import { Unauthorized, } from '../../errors'; +import { clearCookie } from '../../helpers/cookies'; abstract class ApiRoute extends Route { protected version: string; @@ -13,7 +14,9 @@ abstract class ApiRoute extends Route { protected static handleError(req: Request, res: Response, error: Error) { // Handle API unauthorized errors with a 401+JSON response instead of a redirect if (error instanceof Unauthorized) { - res.status(401).json({ + res.status(401) + clearCookie(res, 'session'); + res.json({ error: error.message, }); diff --git a/src/routes/api/v1/Auth.ts b/src/routes/api/v1/Auth.ts index 69b1190..f3f881c 100644 --- a/src/routes/api/v1/Auth.ts +++ b/src/routes/api/v1/Auth.ts @@ -4,6 +4,7 @@ import { Request, Response } from 'express'; import ApiV1Route from './Route'; import { ValidationError } from '../../../errors'; import { AuthInfo, authenticate } from '../../../auth'; +import { clearCookie, setCookie } from '../../../helpers/cookies'; class Auth extends ApiV1Route { constructor() { @@ -47,7 +48,12 @@ class Auth extends ApiV1Route { } session = await $repos.userSessions.create(user.id, expiresAtDate); - res.setHeader('Set-Cookie', `session=${session.getToken()}; Path=/; HttpOnly; SameSite=Strict`); + setCookie(res, { + name: 'session', + value: session.getToken(), + expiresAt: expiresAtDate, + }); + res.json({ session: { token: session.getToken(), @@ -69,7 +75,7 @@ class Auth extends ApiV1Route { await session.destroy(); } - res.setHeader('Set-Cookie', 'session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0'); + clearCookie(res, 'session'); res.status(204).send(); } }