node-jwtutils
Zero dependency JWT encoding and decoding for Node.
Features:
- Encode and decode any JWT tokens
- Supported asymmetric algorithmes (RS256, RS384, RS512, ES256, ES384 and ES512)
- Supported symmetric algorithmes (HS256, HS384 and HS512)
- Support for multiple issuers and keys per issuer.
- Import keys from JWK endpoints
- Easy service authentication for Google and Github
- Express middleware to validate JWT's
Background
Most other JWT implementations tend be complex and have a large array of
dependencies because they implement the seldom used JOSE standard.
This often makes the whole JWT encoding/decoding complicated and hard to
understand, something it really should not be.
So the focus of this module has been to make a simple, secure, fast and
flexible set of utility methods to work with JWT's. The code itself is also
easy to understand and less than 200 lines of code, making it much easier to
security audit the code.
Also note doing your own crypto is a bad idea so this module only deals with
the encoding/decoding of the JWT, the underlaying crypto operations are done
by Node's build-in crypto api that uses openssl.
While symmetric algorithms are supported for legacy integrations, they are
not advisable for production use.
Samples
Basic usage
const { JwtUtils, JwtVerifyError } = require('@connectedcars/jwtutils')
const unixNow = Math.floor(Date.now() / 1000)
let jwtHeader = {
typ: 'JWT',
alg: 'RS256',
kid: '1'
}
let jwtBody = {
aud: 'https://api.domain.tld',
iss: 'https://jwt.io/',
sub: 'subject@domain.tld',
iat: unixNow,
exp: unixNow + 600,
scope: ['http://stuff', 'http://stuff2']
}
const pemEncodedPrivateKey =
'-----BEGIN RSA PRIVATE KEY-----\n' +
'MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw\n' +
'33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW\n' +
'+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB\n' +
'AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS\n' +
'3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp\n' +
'uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE\n' +
'2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0\n' +
'GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K\n' +
'Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY\n' +
'6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5\n' +
'fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523\n' +
'Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP\n' +
'FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==\n' +
'-----END RSA PRIVATE KEY-----'
let jwt = JwtUtils.encode(pemEncodedPrivateKey, jwtHeader, jwtBody)
const publicKey =
'-----BEGIN PUBLIC KEY-----\n' +
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\n' +
'UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\n' +
'HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\n' +
'o2kQ+X5xK9cipRgEKwIDAQAB\n' +
'-----END PUBLIC KEY-----'
const allowedAudinces = ['https://api.domain.tld']
const pubKeys = {
'https://jwt.io/': {
'1@RS256': publicKey,
'default@RS256': publicKey
}
}
try {
let decodedJwtBody = JwtUtils.decode(jwt, pubKeys, allowedAudinces)
} catch (e) {
if (e instanceof JwtVerifyError) {
} else {
console.error(e)
}
}
Usage of express middleware
const express = require('express')
const { JwtAuthMiddleware, JwtVerifyError } = require('@connectedcars/jwtutils')
const audiences = ['https://api.domain.tld']
const pubKeys = {
'https://jwt.io/': {
'1@RS256': publicKey
},
'https://jwt.io/custom': {
'1@RS256': {
publicKey: publicKey,
expiresMax: 3600,
expiresSkew: 600,
nbfIatSkew: 300,
}
}
}
const app = express()
app.use(JwtAuthMiddleware(pubKeys, audiences, user => {
if (user.issuer === 'https://jwt.io/') {
if (!user.subject.match(/^[^@]+@domain\.tld$/)) {
throw new JwtVerifyError('Issuer https://jwt.io/ only allowed to have subject ending in @domain.tld')
}
}
}))
app.use((err, req, res, next) => {
if (err instanceof JwtVerifyError) {
if (err.innerError) {
console.error(`Failed with: ${err.innerError.message}`)
}
res.status(401).send(err.message)
} else {
res.status(500).send('Unknown error')
}
})
app.get('/', (req, res) => {
res.send(`Hello ${req.user.subject}`)
})
app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})
Integration with 3rd parties
Google Identity platform:
const { PubkeysHelper } = require('@connectedcars/jwtutils')
const applyGoogleKeys = (pubKeys) => {
PubkeysHelper.fetchJwkKeys('https://www.googleapis.com/oauth2/v3/certs')
.then(keys => {
pubKeys['https://accounts.google.com'] = keys
})
.catch(e => {
console.log(e)
})
}
applyGoogleKeys(pubKeys)
setInterval(() => {
applyGoogleKeys(pubKeys)
}, 60 * 60 * 1000)
Usage of service authentication (Google and Github)
const { JwtServiceAuth } = require('./index')
const fs = require('fs')
const r2 = require('r2')
let httpRequestHandler = async (method, url, headers, body) => {
let httpRespone = await r2[method.toLowerCase()](url, { headers, body }).response
let data = await httpResponse.arrayBuffer()
return {
statusCode: httpResponse.status,
data: data,
headers: httpResponse.headers
}
})
let jwtServiceAuth = new JwtServiceAuth(httpRequestHandler)
let gitHubAppPrivateKey = fs.readFileSync("user-appname.2017-01-01.private-key.pem", 'utf8')
let googleServiceAccountKeyfile = fs.readFileSync("user-serviceaccount-12345678.json", 'utf8')
async function getAccessTokens() {
let githubAppToken = await = jwtServiceAuth.getGithubAccessToken(gitHubAppPrivateKey, 1, 1)
let googleToken = await jwtServiceAuth.getGoogleAccessToken(googleServiceAccountKeyfile)
}
Documentation
Generate own keypair
Generate private RSA key:
openssl genrsa -aes256 -out private.pem 2048
Generate public key from private key:
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
Command line helper utils
NOTE: Does not support nested JSON
Load private key:
jwtencode private.pem
Copy/paste to stdin (Ctrl-D to end), the password line is only needed if the private key is encrypted:
password password-for-private-key
{
"alg": "RS256",
"typ": "JWT",
"kid": "1"
}
{
"iss": "jwt.io",
"aud": "https://api.domain.tld",
"sub": "subject@domain.tld",
"iat": 1504292127,
"nbf": 1504292127,
"exp": 1598986470
}
jwtdecode public.pem 1 RS256 https://jwt.io localhost
Copy/paste to stdin (Ctrl-D to end):
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwczovL2p3dC5pbyIsImF1ZCI6ImxvY2FsaG9zdCIsInN1YiI6InN1YmplY3RAZG9tYWluLnRsZCIsImlhdCI6MTUwNDI5MjEyNywibmJmIjoxNTA0MjkyMTI3LCJleHAiOjE1OTg5ODY0NzB9.0L5AWwUF3EleBqnQ6V0Lqa36jCccP4A7cAFHHIY1b-oE7pxCoFr8gnAOrlc16N0WUPI6O17JT79kQIPR-LjFm-BgBycBw4eEFYb8z7iXA-zqgQz4ajZXlIljJtJUBbTupbnzEiBKjEFnTxYqb-vUm-TDwTMPaYzBxqqfOrrvKlw