UNPKG

security-koa

Version:

security-koa

456 lines (453 loc) 16.5 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.substr(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 MultiAuthorizer<T, P> { user: string; key: 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, key?: string, user?: string, exact?: boolean) { this.getToken = gt; this.buildError = (buildErr ? buildErr : buildError); this.user = (user ? user : 'id'); this.key = (key ? key : 'userType'); this.exact = (exact !== undefined ? exact : true); this.authorize = this.authorize.bind(this); } authorize(v: T[], privilege: string, action?: number): 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)) { const userId = (t.payload as any)[this.user]; if (!userId) { res.status(403).end('Payload must contain ' + this.user); } else { this.privilege(userId, privilege).then(p => { if (p === none) { res.status(403).end('no permission for ' + userId); } else { 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); }); } } else { res.status(403).end('invalid ' + this.key); } } } else { res.status(401).end('Payload cannot be undefined'); } } }); }; } } // tslint:disable-next-line:max-classes-per-file export class Authorizer<P> { key: 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, key?: string) { this.getToken = gt; this.buildError = (buildErr ? buildErr : buildError); this.key = (key ? key : 'id'); 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.key]; if (!userId) { res.status(403).end('Payload must contain ' + this.key); } else { this.privilege(userId, privilege).then(p => { if (p === none) { res.status(403).end('no permission for ' + userId); } else { 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); } }