Build and NPM package status | |
---|---|
Description
jwtproxy provides a simple middleware component with a few switches that allow verification of JWT
using either symmetric / shared keys or RSA keys. Relying upon two other libraries - jsonwebtoken
and jwks-rsa
it further simplifies and extends the configration of the needs.
Overview - jwtproxy
Often there is a need for a simple JWT
validation or verification within a Node.js, or Express.js application. While there are options such as reverse proxies like:
The initial need was to simplify a needed deployment for basic JWT
validation and not initially overcomplicate a Kubernetes deployment with added side-car containers, ingress containers, etc. While some Cloud providers have some of these capabilities ready to go, this component simplifies the initial need and allows evolution of the concern before biting off on something much more significant.
It can also be enabled or disabled via a configuration or environment feature switch.
The initial goals were:
- must be an Express middleware component that provides a simple way to include or excluded within the pipeline
- further simplifiy and externalize settings as needed leaning on
dotenv
and.env
files for local, and environment variables for runtime - while injected into the pipeline should be easy to disable, modify, etc.
Note: more details are in Overview.
Setup and installation
There is an example ExpressJS application in the ./example
folder. Written in TypeScript (both the component and sample Express Js App) it is fully transpiled to JavaScript.
Adding to Express
Like any middleware for ExpressJS, this should be inserted early on in the pipeline. Since it reads the raw NodeJS HTTP Headers it can be before other middleware that parses cookies, the body, etc.
In the following, we allow two algorithms, and exclude two paths from checking if the JWT
is in the Authorization
header. And, while we do state RS256
, we actually only use HS256
as that is the symmetric algoritm used. We are not setting up a Jwks URL
for retreival of keys.
Example setup
import jwtProxy, { JwtProxyOptions } from 'jwtproxy'
const sharedSecret = 'notagreatsecret'
const options: JwtProxyOptions = {
algorithms: [ 'RS256','HS256'],
excluded: ['/status', '/sign'],
secretOrKey: sharedSecret
}
const proxy = jwtProxy(options),
this.app.use(proxy)
Quick start with sample
NOTE: Make sure you have NodeJs 10x or better and npm version 5.4 or greater.
Navigate to ./example
folder and execute the following:
# the ci command uses the package-lock.json or shrinkwrap to do a deterministic install
npm ci
npm run build
npm start
At this point you should see something similar:
$ npm run fresh
> example@1.0.0 fresh E:\g\npm-projs\jwtproxy\example
> npx npm-run-all ci build start
npx: installed 58 in 6.288s
> example@1.0.0 ci E:\g\npm-projs\jwtproxy\example
> npm ci
npm WARN prepare removing existing node_modules/ before installation
added 60 packages in 2.925s
> example@1.0.0 build E:\g\npm-projs\jwtproxy\example
> tsc --build .
> example@1.0.0 start E:\g\npm-projs\jwtproxy\example
> node server.js
App listening on the http://localhost:5000
one line startup
The package.json
has a simple npm script
that combines all three -- just run the following as an alternative if you want to save a few keystrokes.
npm run fresh
Port in use
If you see something like the following, that means the port is already used by some other process.
> example@1.0.0 start E:\g\npm-projs\jwtproxy\example
> node server.js
events.js:183
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE 127.0.0.1:5000
at Server.setupListenHandle [as _listen2] (net.js:1360:14)
at listenInCluster (net.js:1401:12)
at GetAddrInfoReqWrap.doListen [as callback] (net.js:1510:7)
at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:72:10)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! example@1.0.0 start: `node server.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the example@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
To address this set an environment variable either in a .env
file like the following, or with your operating system shell method -- export PORT=5500
or in PowerShell $env:PORT=5500
. In bash/zsh you can also just pass this on the command line as PORT=5500 npm start
PORT=5500
HOST=localhost
Either create or modifying existing Express JS application
jwptproxy
npm install npm i jwtproxy
modify your express app
Follow the instructions above in [Example setup](Example setup) for the modifications.
Sequence Diagram Jwt Proxy
sequenceDiagram
participant User
participant App
participant Module
participant KeyHelper
participant JwksHost
App->>+Module: Options?
Note left of Module: optional and if present <br/> than NO env variables are used.
User->>+App: api(h:jwt)
App->>+Module: verify(jwt)
Module->>Module: decode(kid,claims)
Note Right of Module: decode but not verify <br/>
Module->>+KeyHelper: getKey(decoded token, kid)
alt Local or Remote
KeyHelper->>+JwksHost: getKeys()
else
KeyHelper->>KeyHelper: getKey(kid, filePath)
Note right of KeyHelper: developer and test experience <br/> without relying on <br/>external Jwks Host [A]
end
KeyHelper-->>-Module: key for kid
Module-->>-App: done
App-->>-User: ok/not ok
Options object
An option object is optionaly injected at the start. The presence of the options
object implies that NO environment variables are to be used. This permits couple of scenarios
- Explicit paths that need something different - so the injection can override what an
env
variable might provide. Regardless, even in that situation, the code that creates theoptions
object can also pull fromenv
. - Developer and Test scenarios - Test frameworks need to create valid tokens, and the verification needs some key data offline and disconnected at least during unit testing. For develper experience, this allows ease of use whithout the need to standup a signing authority and Jwks Endpoint just to try out the module.
/** Options for THIS middlware */
export interface JwtProxyOptions {
disable?: boolean|undefined,
secretOrKey?: string,
audience?: string,
issuer?: string,
jwksUrl?: string,
algorithms?: Algorithm[],
excluded?: string[]
}
option | purpose | example |
---|---|---|
disable | completely disable the middleware and do NO checks | disable = true |
secretOrKey | either the shared key or if jwksUrl is populated, this can be blank |
secretOrKey='notagreatkey' |
audience | a list of audiences in one line separated by ; - if ANY of the aud match, then the verify passes |
audience = 'shipping;receiving;marketing' |
issuer | for jwks RSA verification of the issuer on the JWT
|
issuer = 'foo.bar.com' |
jwksUrl | the HTTP endpoint used for retrieval of keysets then used to match the key name present in the JWT see jwks
|
jwksUrl = 'https://YOUR_DOMAIN/.well-known/jwks.json' |
algorithms | any of the supported algorithms within jsonwebtoken see Algorithms
|
algorithms = ['RS256', 'HS258'] |
exluded | array of paths that are specifically exluded by exact match on Request.URL not including the query string |
exluded= ['/status', '/health', '/issue'] |
Disable check
The following uses env or options and if EITHER are set to disable, the middleware is turned OFF.
const envDisabled: boolean = process.env.JWTP_DISABLE == null ? false : process.env.JWTP_DISABLE.toLowerCase() === 'true';
const optDisabled = proxyOptions?.disable as boolean;
if (optDisabled || envDisabled)
{
logger('jwt proxy disabled - no jwt verification will occure');
return async (req:Request, res:Response, next:NextFunction): Promise<void> =>{
next();
return;
}
}
Rule for Options
secretOrKey?: string,
audience?: string, - 'if present also validate'
issuer?: string, - 'if present also validate'
jwksUrl?: string,
algorithms?: Algorithm[], - 'if there is an alt alg to use for token verification - note that HS256 is default'
excluded?: string[] - 'paths to exlude in routes'
{ exclude?: ,
alg?: ,
jwksurl: 'this can be `string|function`,
iss? : ,
aud? : }
alg
if algorithms
is supplied, this becomes a verification aspect. Jwt tokens supply the algorithms
as part of the header, along with the kid
. We use the algorithms
supplied to as the constrained set - if the alg
on JWT token is not in the set it fails validation.
jwksurl
if the jwksurl
is a string, it is parsed to ensure its valid url. If so, then the JwksHost
is to be called for the keyset and key for kid
.
if jwksurl
is a string and not a url, it is assumed to be the verification key - in RS256
this is the public key material in PEM format.
If the jwksurl
is a function then it implements a known interface (TBD) that provides a callback that delivers the secret or key for the kid
Notes: from the diagram
- [A] Key information provided as part of the
options
object or anenvironment
variable can be fullURL
or file reference to localPEM
file that contains thepublic key
to verify the singature.
Environment Variables.
The presence of an options
object precludes any env
variables to be used for processing. If the options
object is absent or empty, the env
variables are used for configuration.
Note: all of the
jwtproxy
variables are prefixed withJWTP_
- JWTP_ALG: string
- JWTP_URL: string
- JWTP_ISS: string
- JWTP_AUD: string = 'myaudience'
- JWTP_EXCLUDE: string = /path1,path2/ --
It is NOT expected in the env
use case that the JWTP_URL
will be used as a function
type. Either a pure PEM
public key for signature verification or a proper URL
to a Jwks Host endpoint that provide key material.