security-express
Version:
security-express
440 lines (437 loc) • 14.2 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.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)
}
}