-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New project
- Loading branch information
0 parents
commit a4a7215
Showing
12 changed files
with
12,196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.idea/ | ||
.vscode/ | ||
node_modules/ | ||
dist/ | ||
tmp/ | ||
temp/ | ||
coverage/ | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node' | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.