UNPKG

security-express

Version:
440 lines (437 loc) 14.2 kB
import { Application, NextFunction, Request, Response } from "express" export type Handle = (req: Request, res: Response, next: NextFunction) => void export type Authorize = (privilege: string, action?: number) => Handle export type MultiAuthorize<T> = (v: T[], privilege: string, action?: number) => Handle export type Authorizes<T> = (v: T[], privilege: string, action?: number) => Handle export type Check<T> = (v: T[]) => Handle export interface Token { secret: string expires: number } export type TokenConfig = Token export type TokenConf = Token export interface StatusError { status: number body: string } export type GetToken<P> = (req: Request, res: Response) => Promise<PayloadToken<P>> export interface StringToken { token: string error?: string } export interface PayloadToken<P> { payload?: P token?: string error?: string end?: boolean } export function handleToken<P>( res: Response, secret: string, name: string, t: StringToken, verify: (token: string, secret: string) => Promise<P>, buildErr: (err: any) => StatusError, ): Promise<PayloadToken<P>> { if (t.error) { res.status(401).end(t.error) return Promise.resolve({ error: t.error, end: true }) } else { return verify(t.token, secret) .then((payload) => { res.locals[name] = payload return Promise.resolve({ payload, token: t.token }) }) .catch((err) => { const { status, body } = buildErr(err) res.status(status).end(body) return Promise.resolve({ token: t.token, error: t.error, end: true }) }) } } export function fromCookies(req: Request, name: string): StringToken { if (!req.cookies) { return { token: "", error: `Require cookies` } } else { const token = req.cookies[name] if (!token) { return { token: "", error: `Require '${name}' in cookies` } } else { return { token } } } } export function fromAuthorization(req: Request, prefix: string): StringToken { const data = req.headers["authorization"] if (data) { if (!data.startsWith(prefix)) { return { token: "", error: `Authorization must start with '${prefix.trim()}'` } } else { const token = data.substring(prefix.length) return { token } } } else { return { token: "", error: `Require 'Authorization' in header` } } } export interface TokenHandler<P> { getToken(req: Request, res: Response): Promise<PayloadToken<P>> } export class LocalsToken<P> implements TokenHandler<P> { token: string constructor(token?: string) { this.token = token ? token : "token" this.getToken = this.getToken.bind(this) } getToken(req: Request, res: Response): Promise<PayloadToken<P>> { const payload = res.locals[this.token] return Promise.resolve({ payload }) } } // tslint:disable-next-line:max-classes-per-file export class CookiesToken<P> implements TokenHandler<P> { token: string payload: string buildError: (err: any) => StatusError constructor( public secret: string, public verify: (token: string, secret: string) => Promise<P>, buildErr?: (err: any) => StatusError, payload?: string, token?: string, ) { this.token = token ? token : "token" this.payload = payload ? payload : "token" this.buildError = buildErr ? buildErr : buildError this.getToken = this.getToken.bind(this) } getToken(req: Request, res: Response): Promise<PayloadToken<P>> { const t = fromCookies(req, this.token) return handleToken<P>(res, this.secret, this.payload, t, this.verify, this.buildError) } } // tslint:disable-next-line:max-classes-per-file export class AuthorizationToken<P> implements TokenHandler<P> { prefix: string payload: string buildError: (err: any) => StatusError constructor( public secret: string, public verify: (token: string, secret: string) => Promise<P>, buildErr?: (err: any) => StatusError, payload?: string, prefix?: string, ) { this.prefix = prefix ? prefix : "Bearer " this.payload = payload ? payload : "token" this.buildError = buildErr ? buildErr : buildError this.getToken = this.getToken.bind(this) } getToken(req: Request, res: Response): Promise<PayloadToken<P>> { const t = fromAuthorization(req, this.prefix) return handleToken<P>(res, this.secret, this.payload, t, this.verify, this.buildError) } } // tslint:disable-next-line:max-classes-per-file export class TokenService<P> implements TokenHandler<P> { prefix: string token: string payload: string buildError: (err: any) => StatusError constructor( public secret: string, public verify: (token: string, secret: string) => Promise<P>, buildErr?: (err: any) => StatusError, payload?: string, token?: string, prefix?: string, ) { this.prefix = prefix ? prefix : "Bearer " this.token = token ? token : "token" this.payload = payload ? payload : "token" this.buildError = buildErr ? buildErr : buildError this.getToken = this.getToken.bind(this) } getToken(req: Request, res: Response): Promise<PayloadToken<P>> { let t = fromCookies(req, this.token) if (t.error) { t = fromAuthorization(req, this.prefix) } return handleToken<P>(res, this.secret, this.payload, t, this.verify, this.buildError) } } export function useToken<P>( secret: string, verify: (token: string, secret: string) => Promise<P>, buildErr?: (err: any) => StatusError, cookie?: boolean, payload?: string, token?: string, prefix?: string, ): GetToken<P> { if (cookie === true) { return new CookiesToken(secret, verify, buildErr, payload, token).getToken } else if (cookie === false) { return new AuthorizationToken(secret, verify, buildErr, payload, prefix).getToken } else { return new TokenService(secret, verify, buildErr, payload, token, prefix).getToken } } export const getToken = useToken // tslint:disable-next-line:max-classes-per-file export class Handler<P> { prefix: string token: string payload: string constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, prefix?: string, token?: string, payload?: string) { this.prefix = prefix ? prefix : "Bearer " this.token = token ? token : "token" this.payload = payload ? payload : "token" this.handle = this.handle.bind(this) } handle(): (req: Request, res: Response, next: NextFunction) => void { return (req: Request, res: Response, next: NextFunction) => { let t = fromCookies(req, this.token) if (t.error) { t = fromAuthorization(req, this.prefix) } if (t.error) { next() } else { this.verify(t.token, this.secret) .then((payload) => { res.locals[this.payload] = payload next() }) .catch((err) => { next() }) } } } } export const AuthorizationHandler = Handler // tslint:disable-next-line:max-classes-per-file export class AuthorizationChecker<P> { getToken: GetToken<P> constructor(gt: GetToken<P>) { this.getToken = gt this.check = this.check.bind(this) this.require = this.require.bind(this) } require(): Handle { return (req: Request, res: Response, next: NextFunction) => { this.getToken(req, res).then((t) => { if (!t.end) { next() } }) } } check(): Handle { return this.require() } } export function exist<T>(obj: T | T[], arr: T[]): boolean { if (Array.isArray(obj)) { for (const o of obj) { for (const v of arr) { // tslint:disable-next-line:triple-equals if (o == v) { return true } } } } else { for (const v of arr) { // tslint:disable-next-line:triple-equals if (obj == v) { return true } } } return false } // tslint:disable-next-line:max-classes-per-file export class Checker<T, P> { key: string getToken: GetToken<P> constructor(gt: GetToken<P>, key?: string) { this.getToken = gt this.key = key ? key : "id" this.check = this.check.bind(this) } check(v: T[]): Handle { return (req: Request, res: Response, next: NextFunction) => { this.getToken(req, res).then((t) => { if (!t.end) { if (t.payload) { const obj = (t.payload as any)[this.key] if (!obj) { res.status(403).end("Payload must contain " + this.key) } else { if (exist<T>(obj as any, v)) { next() } else { res.status(403).end("invalid " + this.key) } } } else { res.status(403).end("Payload cannot be undefined") } } }) } } } // tslint:disable-next-line:max-classes-per-file export class Authorizer<P> { payloadId: string userId: string permissions: string exact: boolean getToken: GetToken<P> buildError: (err: any) => StatusError constructor( gt: GetToken<P>, public privilege: (userId: string, privilegeId: string) => Promise<number>, buildErr?: (err: any) => StatusError, exact?: boolean, payloadId?: string, userId?: string, permissions?: string, ) { this.getToken = gt this.buildError = buildErr ? buildErr : buildError this.payloadId = payloadId ? payloadId : "id" this.userId = userId ? userId : "userId" this.permissions = permissions ? permissions : "permissions" this.exact = exact !== undefined ? exact : true this.authorize = this.authorize.bind(this) } authorize(privilege: string, action?: number): Handle { return (req: Request, res: Response, next: NextFunction) => { this.getToken(req, res).then((t) => { if (!t.end) { const payload = t.payload if (payload === undefined) { res.status(401).end("Payload cannot be undefined") } else { const userId = (payload as any)[this.payloadId] if (!userId) { res.status(403).end("Payload must contain " + this.payloadId) } else { this.privilege(userId, privilege) .then((p) => { if (p === none) { res.status(403).end("no permission for " + userId) } else { res.locals[this.userId] = userId res.locals[this.permissions] = p if (!action) { next() } else { if (this.exact) { // tslint:disable-next-line:no-bitwise const sum = action & p if (sum === action) { return next() } else { res.status(403).end("no permission") } } else { if (p >= action) { return next() } else { res.status(403).end("no permission") } } } } }) .catch((err) => { const { status, body } = this.buildError(err) res.status(status).end(body) }) } } } }) } } } export const none = 0 export const read = 1 export const write = 2 export const approve = 4 export const all = 2147483647 // tslint:disable-next-line:max-classes-per-file export class PrivilegeLoader { constructor(public sql: string, public query: <T>(sql: string, args?: any[]) => Promise<T[]>) { this.privilege = this.privilege.bind(this) } privilege(userId: string, privilegeId: string): Promise<number> { return this.query<any>(this.sql, [userId, privilegeId]).then((v) => { if (!v || v.length === 0) { return none } const keys = Object.keys(v[0]) if (keys.length === 0) { return all } const k: string = keys[0] let permissions = 0 let ok = false for (const p of v) { const x = p[k] if (typeof x === "number") { // tslint:disable-next-line:no-bitwise permissions = permissions | x ok = true } } return ok ? permissions : all }) } } export function buildError(err: any): StatusError { return { status: 401, body: "Invalid token: " + toString(err) } } export function toString(err: any): string { return typeof err === "string" ? err : JSON.stringify(err) } export function get(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void { if (secure) { app.get(path, authorize, handle) } else { app.get(path, handle) } } export function post(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void { if (secure) { app.post(path, authorize, handle) } else { app.post(path, handle) } } export function put(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void { if (secure) { app.put(path, authorize, handle) } else { app.put(path, handle) } } export function patch(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void { if (secure) { app.patch(path, authorize, handle) } else { app.patch(path, handle) } } export function del(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void { if (secure) { app.delete(path, authorize, handle) } else { app.delete(path, handle) } }