security-koa
Version:
security-koa
456 lines (453 loc) • 16.5 kB
text/typescript
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);
}
}