import type { Account } from './account/types' import type { Context } from './types' import { ObjectId } from 'mongodb' import type { WithId } from 'mongodb' import { http } from '@edge/misc-utils' import jwt from 'jsonwebtoken' import type { NextFunction, Request, Response } from 'express' /** * Authentication context. * This provides functionality for signing and verifying JWTs using static configuration. * * @see https://jwt.io/introduction */ export type Auth = ReturnType /** AuthRequest is an Express Request with some additional properties. */ export type AuthRequest = Request & { /** * Account document set by authentication middleware. * If this value exists, then the the request is authenticated with this account. * * @see Auth.verifyRequestMiddleware */ account?: WithId } /** * An AuthRequestHandler is essentially the same as an Express RequestHandler, but has access to additional request * properties when `verifyRequest` is used earlier in request processing. * * @see AuthRequest */ export type AuthRequestHandler = (req: AuthRequest, res: Response, next: NextFunction) => void | Promise /** * Create an authentication context. * This provides functionality for signing and verifying JWTs using static configuration. * * @see https://jwt.io/introduction */ function createAuth(ctx: Context) { const { expiresIn, secret } = ctx.config.auth.jwt /** Sign a JWT containing an account ID. */ function sign(accountId: ObjectId | string): Promise { return new Promise((res, rej) => { jwt.sign({ _id: accountId.toString() }, secret, { expiresIn }, (err, enc) => { if (err) return rej(err) if (enc) return res(enc) rej(new Error('no result')) }) }) } /** Verify and decode a JWT containing an account ID. */ function verify(token: string): Promise { return new Promise((res, rej) => { jwt.verify(token, secret, (err, dec) => { if (err) return rej(err) try { const id = new ObjectId((dec as { _id: string })._id) res(id) } catch (err) { rej(err) } }) }) } /** * Express middleware to verify and decode a JWT provided in an HTTP request's authorization header. * * If an account ID is successfully decoded from the JWT, this will attempt to load the account automatically before * further request processing. * The account is attached to the request and tacitly available to subsequent request handlers that implement the * AuthRequestHandler type. * * If an invalid token is provided or the account does not exist, an error will be passed along, blocking request * handling. */ const verifyRequestMiddleware: AuthRequestHandler = async (req, res, next) => { // Read header and skip processing if bearer token missing or malformed const header = req.header('authorization') if (!header) return next() if (!/^[Bb]earer /.test(header)) return next() try { // Read and verify token const token = header.substring(7) const _id = await verify(token) // Load account const account = await ctx.model.account.collection.findOne({ _id }) if (!account) return http.unauthorized(res, next) req.account = account next() } catch (err) { return next(err) } } return { sign, verify, verifyRequestMiddleware } } export default createAuth