Skip to content

Commit

Permalink
feat: first commit
Browse files Browse the repository at this point in the history
New project
  • Loading branch information
justinkalland committed Jun 13, 2020
0 parents commit a4a7215
Show file tree
Hide file tree
Showing 12 changed files with 12,196 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.idea/
.vscode/
node_modules/
dist/
tmp/
temp/
coverage/
.env
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
}
11,024 changes: 11,024 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "jwt-guard",
"version": "0.0.0",
"description": "Middleware for guarding with JWT roles and claims.",
"repository": "github:justinkalland/jwt-guard",
"homepage": "https://github.com/justinkalland/jwt-guard#readme",
"bugs": {
"url": "https://github.com/justinkalland/jwt-guard/issues"
},
"main": "index.js",
"scripts": {
"test": "jest",
"lint": "eslint .",
"validate": "run-s test lint",
"build": "rm -rf dist/ && tsc",
"prerelease": "git checkout master && git pull origin master && npm run validate",
"release": "standard-version"
},
"author": "Justin Kalland <justin@kalland.com>",
"license": "MIT",
"devDependencies": {
"@commitlint/cli": "^8.3.5",
"@commitlint/config-conventional": "^8.3.4",
"@types/express": "^4.17.6",
"@types/http-errors": "^1.6.3",
"@types/jest": "^26.0.0",
"@types/jsonwebtoken": "^8.5.0",
"@types/supertest": "^2.0.9",
"eslint-config-jk-ts": "^1.5.0",
"express": "^4.17.1",
"jest": "^26.0.1",
"npm-run-all": "^4.1.5",
"standard-version": "^8.0.0",
"supertest": "^4.0.2",
"ts-jest": "^26.1.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.5"
},
"dependencies": {
"http-errors": "^1.7.3",
"jsonwebtoken": "^8.5.1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": "eslint",
"*.ts": "eslint"
},
"eslintConfig": {
"extends": "jk-ts"
},
"files": [
"dist/index.js",
"dist/index.d.ts",
"dist/lib"
],
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
32 changes: 32 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Token from './lib/token'

declare module 'express-serve-static-core' {
interface Request {
token?: Token
}
}

interface Options {
secretOrPublicKey: Buffer | string
}

export default function (options: Options) {
return function (req, res, next) {
if (req.method === 'OPTIONS') {
return next()
}

let encodedJwt: string
if (req.headers?.authorization !== undefined) {
const parts = req.headers.authorization.split(' ')

if (parts.length === 2 && parts[0] === 'Bearer') {
encodedJwt = parts[1]
}
}

req.token = new Token(encodedJwt, options.secretOrPublicKey)

next()
}
}
99 changes: 99 additions & 0 deletions src/lib/require.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Token from './token'
import createError from 'http-errors'

enum Status {
Fresh,
Passing,
Failed,
Passed
}

class Require {
private readonly _token: Token
private _status: Status = Status.Fresh

constructor (token: Token) {
this._token = token
}

role = (name: string): Require => {
const currentStatus = this._status

if (currentStatus === Status.Passed || currentStatus === Status.Failed) {
return this
}

const hasRole = this._token.roles?.includes(name)

if (!hasRole) {
this._status = Status.Failed
} else {
this._status = Status.Passing
}

return this
}

claim = (name: string, value: string | number | boolean): Require => {
const currentStatus = this._status

if (currentStatus === Status.Passed || currentStatus === Status.Failed) {
return this
}

const compareValue = this._token.claims?.[name]

if (compareValue === undefined || compareValue !== value) {
this._status = Status.Failed
} else {
this._status = Status.Passing
}

return this
}

get and (): Require {
return this
}

get or (): Require {
const currentStatus = this._status

if (currentStatus === Status.Passed) {
return this
}

if (currentStatus === Status.Passing) {
this._status = Status.Passed
return this
}

this._status = Status.Fresh

return this
}

get check (): boolean {
if (!this._token.valid) {
return false
}

const currentStatus = this._status

if (currentStatus === Status.Passing || currentStatus === Status.Passed) {
return true
}

return false
}

guard = (message?: string): void => {
this._token.requireValid()

if (!this.check) {
throw new createError.Forbidden(message)
}
}
}

export default Require
76 changes: 76 additions & 0 deletions src/lib/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Require from './require'
import jwt from 'jsonwebtoken'
import createError from 'http-errors'

export enum TokenStatus {
Valid,
Missing,
Expired,
Invalid,
NotValidYet
}

interface DecodedJwtPayload {
roles?: string[]
}

class Token {
readonly roles: string[]
readonly claims: object
readonly status: TokenStatus

constructor (encodedJwt?: string, secretOrPublicKey?: Buffer | string) {
if (encodedJwt === undefined) {
this.status = TokenStatus.Missing
return
}

try {
const payload: DecodedJwtPayload | string = jwt.verify(encodedJwt, secretOrPublicKey)
if (typeof payload === 'string') {
throw new Error('Does not support string payloads')
}

this.roles = payload.roles
this.claims = payload

this.status = TokenStatus.Valid
} catch (err) {
switch (err.name) {
case 'TokenExpiredError':
this.status = TokenStatus.Expired
break
case 'NotBeforeError':
this.status = TokenStatus.NotValidYet
break
default:
this.status = TokenStatus.Invalid
}
}
}

get require (): Require {
return new Require(this)
}

get valid (): boolean {
return this.status === TokenStatus.Valid
}

requireValid = (): void => {
switch (this.status) {
case TokenStatus.Valid:
return
case TokenStatus.Missing:
throw new createError.Unauthorized('Token missing')
case TokenStatus.Expired:
throw new createError.Unauthorized('Token expired')
case TokenStatus.NotValidYet:
throw new createError.Unauthorized('Token not active yet')
case TokenStatus.Invalid:
throw new createError.Unauthorized('Token invalid')
}
}
}

export default Token

0 comments on commit a4a7215

Please sign in to comment.