nfp_sites/base/authentication/security.mjs

71 lines
2.0 KiB
JavaScript

import crypto from 'crypto'
import { HttpError } from 'flaska'
import { decode, encode } from '../util.mjs'
import config from '../config.mjs'
export const RankLevels = {
Normal: 1,
Manager: 10,
Admin: 100,
}
const issuer = config.get('mssql:connectionUser')
const secret = config.get('jwtsecret')
export function verifyValidToken(parts, minLevel) {
if (parts.length !== 4) {
throw new HttpError(401, 'Authentication token invalid')
}
const hmac = crypto.createHmac('sha256', secret)
hmac.update([parts[0], parts[1], parts[2]].join('.'))
let apiSignature = encode(hmac.digest())
if (apiSignature !== parts[3]) {
throw new HttpError(401, 'Authentication token invalid signature')
}
let header
let body
try {
header = JSON.parse(decode(parts[0]).toString('utf8'))
body = JSON.parse(decode(parts[1]).toString('utf8'))
} catch (err) {
throw new HttpError(401, 'Authentication token invalid json')
}
if (header.alg !== 'HS256') {
throw new HttpError(401, 'Authentication token invalid alg')
}
let unixNow = Math.floor(Date.now() / 1000)
// Validate token, add a little skew support for issued_at
if (body.iss !== issuer || !body.iat || !body.exp
|| body.iat > unixNow + 300 || body.exp <= unixNow) {
throw new HttpError(403, 'Authentication token expired or invalid')
}
if (body.rank < minLevel) {
throw new HttpError(401, 'User does not have access to this resource')
}
return body
}
export function authenticate(minLevel = RankLevels.Manager) {
return function(ctx) {
if (!ctx.req.headers.authorization) {
throw new HttpError(401, 'Authentication token missing')
}
if (!ctx.req.headers.authorization.startsWith('Bearer ')) {
throw new HttpError(401, 'Authentication token invalid')
}
let parts = ctx.req.headers.authorization.slice(7).split('.')
ctx.state.auth_user = verifyValidToken(parts, minLevel)
ctx.state.auth_token = [parts[0], parts[1], parts[2]].join('.')
}
}