jawt
v0.1.2
Published
A WebCrypto based JSON Web Token implementation without dependencies
Downloads
3
Readme
jawt
This is a dependency-less implementation of JSON Web Tokens using WebCrypto for Node.js.
Reasoning
This was started as a project for me to dive deep into JSON Web Tokens and the cryptography involved. This is not currently recommended for production usage! I am fairly new to cryptography and should really be left to the experts. If this library gets peer reviewed by experts and sees a decent amount of "production" usage, only then will I stop discouraging the usage of this library. Even if that's the case, I would still recommend you use a more fleshed out library like jsonwebtoken or jose. Much inspiration was taken from both of these libraries.
Requirements
This requires at least Node.js v17.0.0 because it utilizes the WebCrypto implementation introduced in Node.js v15.0.0. At the time of writing this, it also says this API is experimental (Stability 1), which states:
Experimental. The feature is not subject to Semantic Versioning rules. Non-backward compatible changes or removal may occur in any future release. Use of the feature is not recommended in production environments.
I wanted to pin to WebCrypto because it had a lot of features fleshed out that I was looking for (native JWK support, more standard signing/verifying api). So the actual crypto bits are still left to the professionals. This also should make it fairly easy to make it compatible with the browser once I figure out how to split the webcrypto exports.
This library also utilizes structuredClone
,
which was introduced in version 17.0.0. It could probably easily be polyfiled
with a dependency, but as one of the goals of this library is to not have any
dependencies itself, I've decided to just require node version 17.0.0 or higher.
Also, there is no CommonJS version of this library. I'm sure it's just a simple
build process to split the two, which I'll look into when I spend more time on
this. For now, you should by able to use import('jawt').then(jawt => {})
.
Basic Usage
import { createKeyStoreFromJWKS, jwt } from 'jawt'
async function configureApp () {
// Then import the keys
const keyStore = await createKeyStoreFromJWKS(JSON.parse(process.env.JWKS))
// You can get the public keys in `jwks` format to use for a
// `/.well-known/jwks.json` endpoint
console.log(keyStore.publicJWKS())
const token = await jwt.sign({}, keyStore)
const { payload } = await jwt.verify(token, keyStore)
}
Comparison to other Libraries
| | jsonwebtoken | jose | jawt |
| -------------- | ------------ | ---- | ---- |
| Sign | ✔ | ✔ | ✔ |
| Verify | ✔ | ✔ | ✔ |
| iss
check | ✔ | ✔ | ✔ |
| sub
check | ✔ | ✔ | ✔ |
| aud
check | ✔ | ✔ | ✔ |
| exp
check | ✔ | ✔ | ✔ |
| nbf
check | ✔ | ✔ | ✔ |
| iat
check | ✔ | ✔ | ✔ |
| jti
check | ✔ | ✔ | ✔ |
| typ
check | ? | ✔ | ✔ |
| None algorithm | ✔ | ✔ | |
| HS256 | ✔ | ✔ | ✔ |
| HS384 | ✔ | ✔ | ✔ |
| HS512 | ✔ | ✔ | ✔ |
| PS256 | ✔ | ✔ | ✔ |
| PS384 | ✔ | ✔ | ✔ |
| PS512 | ✔ | ✔ | ✔ |
| RS256 | ✔ | ✔ | ✔ |
| RS384 | ✔ | ✔ | ✔ |
| RS512 | ✔ | ✔ | ✔ |
| ES256 | ✔ | ✔ | ✔ |
| ES256K | | ✔ | |
| ES384 | ✔ | ✔ | ✔ |
| ES512 | ✔ | ✔ | ✔ |
| EdDSA | | ✔ | |
API
generate(algorithm, options) => Promise<Key>
Generates a key to be used for signing/verifying
import { generate } from 'jawt'
// Modulus length options are optional
const rs256Key = await generate('RS256', { modulusLength: 2048 })
const rs384Key = await generate('RS384', { modulusLength: 2048 })
const rs512Key = await generate('RS512', { modulusLength: 2048 })
const ps256Key = await generate('PS256', { modulusLength: 2048 })
const ps384Key = await generate('PS384', { modulusLength: 2048 })
const ps512Key = await generate('PS512', { modulusLength: 2048 })
const es256Key = await generate('ES256')
const es384Key = await generate('ES384')
const es512Key = await generate('ES512')
const hs256Key = await generate('HS256')
const hs384Key = await generate('HS384')
const hs512Key = await generate('HS512')
ES256K
and EdDSA
is not supported by Node's version of WebCrypto at the time
of writing this. If you find they are before I do, an open issue would be most
welcome.
createKeyStore(keys) => KeyStore
Creates a KeyStore to be used for signing and verifying.
import { createKeyStore, generate } from 'jawt'
const keys = await Promise.all([generate('ES512'), generate('RS256')])
const keyStore = createKeyStore(keys)
keyStore.primaryKey() // gets the first key, used for signing
keyStore.get(keys[0].kid) // gets a key by kid, used in verifying
keyStore.keys() // returns a generator that returns each key. Use `Array.from(keyStore.keys())` or `[...keyStore.keys()]` if you need an array
keyStore.publicJWKS() // { keys: [] } returns the public version of the keys in JWK format
keyStore.privateJWKS() // { keys: [] } returns the private version of the keys in JWK format
createKeyStoreFromJWKS(JWKS) => Promise<KeyStore>
Creates a KeyStore from a JSON Web Key Set
import { createKeyStore, createKeyStoreFromJWKS, generate } from 'jawt'
const keys = await Promise.all([generate('ES512'), generate('RS256')])
const keyStore = createKeyStore(keys)
const jwks = keyStore.privateJWKS()
// You could then export it and use it in an environment variable
// console.log(JSON.stringify(jwks))
// Then reimport it
// const jwks = JSON.parse(process.env.JWKS)
const duplicateKeyStore = await createKeyStoreFromJWKS(jwks)
jwt.sign(payload, keyStore, options) => Promise<string>
Sign a payload into a JWT formated string.
import { createKeyStoreFromJWKS, jwt } from 'jawt'
const keyStore = await createKeyStoreFromJWKS(JSON.parse(process.env.JWKS))
const token = await jwt.sign({}, keyStore)
const tokenWithOptions = await jwt.sign({ userId: '123' }, keyStore, {
// Date to use for date based operations
// type: Date
clock: new Date(),
// turns into `iss` claim
// type: string
// Defaults to `undefined`
issuer: 'iss',
// turns into `sub` claim
// type: string
// Defaults to `undefined`
subject: 'sub',
// turns into `aud` claim
// type: string | string[]
// Defaults to `undefined`
audience: 'aud',
// turns into `exp` claim
// type: Date | number
// if it is a number it should be the unix timestamp (seconds) you want it to expire
// Defaults to `undefined`
expiresAt: new Date(),
// turns into `exp` claim
// type: Date | number
// if it is a number it should be the number of seconds you want it to expire relative to the `now` option
// Defaults to `undefined`
expiresIn: 60,
// turns into `nbf` claim.
// type: Date | number
// if it is a number it should be the unix timestamp (seconds) you want the token to be valid after
// Defaults to `undefined`
notBefore: new Date(),
// turns into `iat` claim.
// type: boolean | Date | number
// if it is a boolean, `true` will use the `now` option, `false` will disable sending the claim
// if it is a number it should be the unix timestamp (seconds) you want the token to say it was issued at
// Defaults to `true`
issuedAt: new Date(),
// turns into `jti` claim.
// type: string
// Defaults to `undefined`
jwtId: 'jti'
})
jwt.verify(token, keyStore, options) => Promise<payload>
Validates a token against the keys in the keystore and the expected claims. If
it fails the signature or any of the claims, it will reject the promise with an
error that will have a .code
property that tells you which claim failed.
import { createKeyStore, generate, jwt } from 'jawt'
const key1 = await generate('HS256')
const key2 = await generate('ES512')
const oldKeyStore = createKeyStore([key1])
const newKeyStore = createKeyStore([key2, key1])
const token1 = await jwt.sign({}, oldKeyStore)
const token2 = await jwt.sign({}, newKeyStore)
// You can use the keystore to rotate in new keys. If you sign the JWT with this
// library, it will encode the JWK id (kid) in the JWT header and will use that
// to determine which key to use. If there is no `kid` in the header, it will
// attempt to verify the JWT data against all the keys until finds the key that
// validates against it. It will only check keys whose algorithms match up
// against the `alg` property in the jwt header.
const { payload: payload1 } = await jwt.verify(token1, newKeyStore)
const { payload: payload2 } = await jwt.verify(token2, newKeyStore)
const token3 = await jwt.sign({}, newKeyStore, {
issuer: 'my-issuer',
subject: 'my-subject',
audience: ['audience1', 'audience2'],
expiresIn: 60,
jwtId: '4e351afe-026d-44e0-9630-14fd279e70cf'
})
const { payload: payload3 } = await jwt.verify(token3, newKeyStore, {
// Date to use for date based operations
// type: Date
// Defaults to `new Date()`
clock: new Date(),
// Checks the `iss` claim
// type: string | string[]
// If an array of strings given, the given `iss` claim must be one of the strings
// Defaults to `undefined`
issuer: 'my-issuer',
// Checks the `sub` claim
// type: string
// Defaults to `undefined`
subject: 'my-subject',
// Checks the `aud` claim
// type: string | RegExp | (string|RegExp)[]
// If a string or RegExp is given, the `aud` claim(s) must match the string or RegExp
// if it is an array of strings and/or RegExps, then the `aud` claim(s) must match one of the given strings or RegExp
// Defaults to `undefined`
audience: /^audience\d$/,
// Checks the `jti` claim
// type: string
// Defaults to `undefined`
jwtId: '4e351afe-026d-44e0-9630-14fd279e70cf',
// Number of seconds difference to allow for all clock operations
// type: string
// Defaults to `0`
clockTolerance: 30,
// Maximum number of seconds the token is allows to be old
// type: number
// This is used if you don't want to trust super long-lived tokens. If the `iat`
// claim doesn't exist, then it will fail validation
maxAge: 60
})
jwt.verifySafe(token, keyStore, options) => Promise<result>
This is the same as jwt.verify()
, but instead of throwing an error, it returns
you an object that is either { success: true, payload }
or
{ success: false, error }
. It should be TypeScript friendly, so if you check
result.success
in an if statement, you'll be guaranteed the .payload
or
.error
depending on what you checked for.
import { createKeyStore, errors, generate, jwt } from 'jawt'
import { setTimeout } from 'timers/promises'
const key = await generate('HS256')
const keyStore = createKeyStore([key])
const token = await jwt.sign({}, keyStore, { expiresIn: 1 })
await setTimeout(2 * 1000)
const result = await jwt.verifySafe(token, keyStore)
if (result.success === false) {
if (result.error instanceof errors.TokenExpired) {
console.error('Token Expired')
} else {
console.log('Other token error', result.error.code)
}
} else {
console.log('success', result.payload)
}
Error Codes
MALFORMED_JWT
- This means the JWT didn't have three parts (header, payload, signature), or the header wasn't a JSON object, or the payload wasn't a JSON object.INVALID_ALGORITHM
- Thealg
in the JWT header isn't supported.INVALID_KEY_ID
- Thekid
in the JWT header exists, but wasn't a string.ALGORITHM_MISMATCH
- The key in the key store found by thekid
had a different algorithm than thealg
in the JWT header.INVALID_SIGNATURE
- The signature did not match.INVALID_CLAIM
- A claim was being checked, but was the wrong type.NOT_BEFORE
- The token was checked before thenbf
claim.TOKEN_EXPIRED
- The token was checked after theexp
claim.