1
0

Compare commits

..

10 Commits

Author SHA1 Message Date
72e64f1316 2.3.2 2023-11-16 20:34:51 +01:00
e235d835aa add generics to sign 2023-11-16 20:34:27 +01:00
03c66f4223 2.3.1 2023-11-16 20:23:58 +01:00
70488a6a48 update types and add generics to decode 2023-11-16 20:23:43 +01:00
760245d1c7 2.3.0 2023-11-16 15:53:01 +01:00
d40d924176 clean up 2023-11-16 15:52:03 +01:00
c2e4fccf56 fix decode issue #49 2023-11-16 15:46:27 +01:00
0c0919f78d 2.2.10 2023-11-14 12:10:04 +01:00
7299ea0614 update package.json 2023-11-14 12:09:51 +01:00
7004cd16ed clean up 2023-11-14 12:09:38 +01:00
4 changed files with 97 additions and 101 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "2.2.9", "version": "2.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "2.2.9", "version": "2.3.2",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20231025.0", "@cloudflare/workers-types": "^4.20231025.0",

View File

@@ -1,9 +1,13 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "2.2.9", "version": "2.3.2",
"description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker", "description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker",
"main": "index.js", "type": "module",
"exports": "./index.js",
"types": "index.d.ts", "types": "index.d.ts",
"engine": {
"node": ">=18"
},
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"test": "jest" "test": "jest"

View File

@@ -1,4 +1,3 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
Object.defineProperty(global, 'crypto', { value: { subtle: crypto.webcrypto.subtle }}) Object.defineProperty(global, 'crypto', { value: { subtle: crypto.webcrypto.subtle }})
@@ -15,6 +14,11 @@ type Data = {
[key in JwtAlgorithm]: Dataset [key in JwtAlgorithm]: Dataset
} }
type Payload = {
sub: string
name: string
}
const data: Data = { const data: Data = {
'ES256': { 'ES256': {
public: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n-----END PUBLIC KEY-----', public: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n-----END PUBLIC KEY-----',
@@ -64,7 +68,7 @@ const data: Data = {
} }
const payload = { const payload: Payload = {
sub: "1234567890", sub: "1234567890",
name: "John Doe", name: "John Doe",
} }
@@ -78,18 +82,18 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith
}) })
test('decode external', async () => { test('decode external', async () => {
const decoded = jwt.decode(data.token) const decoded = jwt.decode<Payload>(data.token)
expect({ expect({
sub: payload.sub, sub: payload.sub,
name: payload.name name: payload.name
}).toMatchObject({ }).toMatchObject({
sub: decoded.payload.sub, sub: decoded.payload?.sub,
name: payload.name name: payload.name
}) })
}) })
test('sign internal', async () => { test('sign internal', async () => {
token = await jwt.sign(payload, data.private, algorithm) token = await jwt.sign<Payload>(payload, data.private, algorithm)
expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/) expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/)
}) })
@@ -99,7 +103,7 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith
sub: payload.sub, sub: payload.sub,
name: payload.name name: payload.name
}).toMatchObject({ }).toMatchObject({
sub: decoded.payload.sub, sub: decoded.payload?.sub,
name: payload.name name: payload.name
}) })
}) })

View File

@@ -3,14 +3,14 @@ if (typeof crypto === 'undefined' || !crypto.subtle)
/** /**
* @typedef JwtAlgorithm * @typedef JwtAlgorithm
* @type {'ES256'|'ES384'|'ES512'|'HS256'|'HS384'|'HS512'|'RS256'|'RS384'|'RS512'} * @type {'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'}
*/ */
export type JwtAlgorithm = 'ES256'|'ES384'|'ES512'|'HS256'|'HS384'|'HS512'|'RS256'|'RS384'|'RS512' export type JwtAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'
/** /**
* @typedef JwtAlgorithms * @typedef JwtAlgorithms
*/ */
export interface JwtAlgorithms { export type JwtAlgorithms = {
[key: string]: SubtleCryptoImportKeyAlgorithm [key: string]: SubtleCryptoImportKeyAlgorithm
} }
@@ -18,16 +18,14 @@ export interface JwtAlgorithms {
* @typedef JwtHeader * @typedef JwtHeader
* @prop {string} [typ] Type * @prop {string} [typ] Type
*/ */
export interface JwtHeader { export type JwtHeader<T = {}> = {
/** /**
* Type (default: `"JWT"`) * Type (default: `"JWT"`)
* *
* @default "JWT" * @default "JWT"
*/ */
typ?: string typ?: string
} & T
[key: string]: any
}
/** /**
* @typedef JwtPayload * @typedef JwtPayload
@@ -39,7 +37,7 @@ export interface JwtHeader {
* @prop {string} [iat] Issued At * @prop {string} [iat] Issued At
* @prop {string} [jti] JWT ID * @prop {string} [jti] JWT ID
*/ */
export interface JwtPayload { export type JwtPayload<T = {}> = {
/** Issuer */ /** Issuer */
iss?: string iss?: string
@@ -62,13 +60,13 @@ export interface JwtPayload {
jti?: string jti?: string
[key: string]: any [key: string]: any
} } & T
/** /**
* @typedef JwtOptions * @typedef JwtOptions
* @prop {JwtAlgorithm | string} algorithm * @prop {JwtAlgorithm | string} algorithm
*/ */
export interface JwtOptions { export type JwtOptions = {
algorithm?: JwtAlgorithm | string algorithm?: JwtAlgorithm | string
} }
@@ -77,37 +75,32 @@ export interface JwtOptions {
* @extends JwtOptions * @extends JwtOptions
* @prop {JwtHeader} [header] * @prop {JwtHeader} [header]
*/ */
export interface JwtSignOptions extends JwtOptions { export type JwtSignOptions = {
header?: JwtHeader header?: JwtHeader
} } & JwtOptions
/** /**
* @typedef JwtVerifyOptions * @typedef JwtVerifyOptions
* @extends JwtOptions * @extends JwtOptions
* @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`) * @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`)
*/ */
export interface JwtVerifyOptions extends JwtOptions { export type JwtVerifyOptions = {
/**
* If `true` all expiry checks will be skipped
*/
skipValidation?: boolean
/** /**
* If `true` throw error if checks fail. (default: `false`) * If `true` throw error if checks fail. (default: `false`)
* *
* @default false * @default false
*/ */
throwError?: boolean throwError?: boolean
} } & JwtOptions
/** /**
* @typedef JwtData * @typedef JwtData
* @prop {JwtHeader} header * @prop {JwtHeader} header
* @prop {JwtPayload} payload * @prop {JwtPayload} payload
*/ */
export interface JwtData { export type JwtData<Payload = {}, Header = {}> = {
header: JwtHeader header?: JwtHeader<Header>
payload: JwtPayload payload?: JwtPayload<Payload>
} }
const algorithms: JwtAlgorithms = { const algorithms: JwtAlgorithms = {
@@ -203,77 +196,14 @@ async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImport
return importTextSecret(key, algorithm) return importTextSecret(key, algorithm)
} }
function decodePayload(raw: string): JwtHeader | JwtPayload | null { function decodePayload<T = any>(raw: string): T | undefined {
try { try {
raw += '='.repeat(4-(raw.length % 4))
return JSON.parse(atob(raw)) return JSON.parse(atob(raw))
} catch { } catch {
return null return
} }
} }
/**
* Verifies the integrity of the token and returns a boolean value.
*
* @param {string} token The token string generated by `jwt.sign()`.
* @param {string | JsonWebKey} secret The string which was used to sign the payload.
* @param {JWTVerifyOptions | JWTAlgorithm} options The options object or the algorithm.
* @throws {Error | string} Throws an error `string` if the token is invalid or an `Error-Object` if there's a validation issue.
* @returns {Promise<boolean>} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
*/
export async function verify(token: string, secret: string | JsonWebKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', skipValidation: false, throwError: false }): Promise<boolean> {
if (typeof options === 'string')
options = { algorithm: options, throwError: false }
options = { algorithm: 'HS256', skipValidation: false, throwError: false, ...options }
if (typeof token !== 'string')
throw new Error('token must be a string')
if (typeof secret !== 'string' && typeof secret !== 'object')
throw new Error('secret must be a string or a JWK object')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const tokenParts = token.split('.')
if (tokenParts.length !== 3)
throw new Error('token must consist of 3 parts')
const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm)
throw new Error('algorithm not found')
const { payload } = decode(token)
if (!options.skipValidation && !payload) {
if (options.throwError)
throw 'PARSE_ERROR'
return false
}
if (!options.skipValidation && payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) {
if (options.throwError)
throw 'NOT_YET_VALID'
return false
}
if (!options.skipValidation && payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) {
if (options.throwError)
throw 'EXPIRED'
return false
}
const key = await importKey(secret, algorithm)
return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`))
}
/** /**
* Signs a payload and returns the token * Signs a payload and returns the token
* *
@@ -283,9 +213,10 @@ export async function verify(token: string, secret: string | JsonWebKey, options
* @throws {Error} If there's a validation issue. * @throws {Error} If there's a validation issue.
* @returns {Promise<string>} Returns token as a `string`. * @returns {Promise<string>} Returns token as a `string`.
*/ */
export async function sign(payload: JwtPayload, secret: string | JsonWebKey, options: JwtSignOptions | JwtAlgorithm = 'HS256'): Promise<string> { export async function sign<Payload = {}>(payload: JwtPayload<Payload>, secret: string | JsonWebKey, options: JwtSignOptions | JwtAlgorithm = 'HS256'): Promise<string> {
if (typeof options === 'string') if (typeof options === 'string')
options = { algorithm: options } options = { algorithm: options }
options = { algorithm: 'HS256', header: { typ: 'JWT' }, ...options } options = { algorithm: 'HS256', header: { typ: 'JWT' }, ...options }
if (!payload || typeof payload !== 'object') if (!payload || typeof payload !== 'object')
@@ -313,16 +244,73 @@ export async function sign(payload: JwtPayload, secret: string | JsonWebKey, opt
return `${partialToken}.${arrayBufferToBase64Url(signature)}` return `${partialToken}.${arrayBufferToBase64Url(signature)}`
} }
/**
* Verifies the integrity of the token and returns a boolean value.
*
* @param {string} token The token string generated by `jwt.sign()`.
* @param {string | JsonWebKey} secret The string which was used to sign the payload.
* @param {JWTVerifyOptions | JWTAlgorithm} options The options object or the algorithm.
* @throws {Error | string} Throws an error `string` if the token is invalid or an `Error-Object` if there's a validation issue.
* @returns {Promise<boolean>} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
*/
export async function verify(token: string, secret: string | JsonWebKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise<boolean> {
if (typeof options === 'string')
options = { algorithm: options, throwError: false }
options = { algorithm: 'HS256', throwError: false, ...options }
if (typeof token !== 'string')
throw new Error('token must be a string')
if (typeof secret !== 'string' && typeof secret !== 'object')
throw new Error('secret must be a string or a JWK object')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const tokenParts = token.split('.')
if (tokenParts.length !== 3)
throw new Error('token must consist of 3 parts')
const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm)
throw new Error('algorithm not found')
const { payload } = decode(token)
try {
if (!payload)
throw new Error('PARSE_ERROR')
if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000))
throw new Error('NOT_YET_VALID')
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000))
throw new Error('EXPIRED')
const key = await importKey(secret, algorithm)
return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`))
} catch(err) {
if (options.throwError)
throw err
return false
}
}
/** /**
* Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure! * Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure!
* *
* @param {string} token The token string generated by `jwt.sign()`. * @param {string} token The token string generated by `jwt.sign()`.
* @returns {JwtData} Returns an `object` containing `header` and `payload`. * @returns {JwtData} Returns an `object` containing `header` and `payload`.
*/ */
export function decode(token: string): JwtData { export function decode<Payload = {}, Header = {}>(token: string): JwtData<Payload, Header> {
return { return {
header: decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader, header: decodePayload<JwtHeader<Header>>(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')),
payload: decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload payload: decodePayload<JwtPayload<Payload>>(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
} }
} }