1
0

Compare commits

..

19 Commits

Author SHA1 Message Date
Leo Developer
b05345279d allow using cryptokey directly 2024-01-18 20:47:05 +01:00
Richard Lee
55bc15bec4 Fix typing to make build work 2024-01-18 20:36:33 +01:00
kira924age
b0d4084a0f refactor: ๐Ÿ’ก reafactor decodePayload 2023-12-28 23:09:07 +01:00
kira924age
3fd594bbb5 feat: ๐ŸŽธ update decodePayload 2023-12-28 23:09:07 +01:00
kira924age
f8a216574a fix: ๐Ÿ› update decodePayload 2023-12-28 23:09:07 +01:00
kira924age
1f511549f5 feat: ๐ŸŽธ update decodePayload 2023-12-28 23:09:07 +01:00
Leo Developer
4be64469d3 fix header not being typed when using sign 2023-12-21 13:50:50 +01:00
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
5d4c51ad8a 2.2.9 2023-11-12 18:29:53 +01:00
78a0eeee9c clean up 2023-11-12 18:29:43 +01:00
4 changed files with 107 additions and 131 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import crypto from 'node:crypto'
Object.defineProperty(global, 'crypto', { value: { subtle: crypto.webcrypto.subtle }})
@@ -15,6 +14,11 @@ type Data = {
[key in JwtAlgorithm]: Dataset
}
type Payload = {
sub: string
name: string
}
const data: Data = {
'ES256': {
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",
name: "John Doe",
}
@@ -78,18 +82,18 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith
})
test('decode external', async () => {
const decoded = jwt.decode(data.token)
const decoded = jwt.decode<Payload>(data.token)
expect({
sub: payload.sub,
name: payload.name
}).toMatchObject({
sub: decoded.payload.sub,
sub: decoded.payload?.sub,
name: payload.name
})
})
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\-_]+$/)
})
@@ -99,7 +103,7 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith
sub: payload.sub,
name: payload.name
}).toMatchObject({
sub: decoded.payload.sub,
sub: decoded.payload?.sub,
name: payload.name
})
})

View File

@@ -3,14 +3,14 @@ if (typeof crypto === 'undefined' || !crypto.subtle)
/**
* @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
*/
export interface JwtAlgorithms {
export type JwtAlgorithms = {
[key: string]: SubtleCryptoImportKeyAlgorithm
}
@@ -18,16 +18,14 @@ export interface JwtAlgorithms {
* @typedef JwtHeader
* @prop {string} [typ] Type
*/
export interface JwtHeader {
export type JwtHeader<T = {}> = {
/**
* Type (default: `"JWT"`)
*
* @default "JWT"
*/
typ?: string
[key: string]: any
}
} & T
/**
* @typedef JwtPayload
@@ -39,7 +37,7 @@ export interface JwtHeader {
* @prop {string} [iat] Issued At
* @prop {string} [jti] JWT ID
*/
export interface JwtPayload {
export type JwtPayload<T = {}> = {
/** Issuer */
iss?: string
@@ -62,13 +60,13 @@ export interface JwtPayload {
jti?: string
[key: string]: any
}
} & T
/**
* @typedef JwtOptions
* @prop {JwtAlgorithm | string} algorithm
*/
export interface JwtOptions {
export type JwtOptions = {
algorithm?: JwtAlgorithm | string
}
@@ -77,37 +75,32 @@ export interface JwtOptions {
* @extends JwtOptions
* @prop {JwtHeader} [header]
*/
export interface JwtSignOptions extends JwtOptions {
header?: JwtHeader
}
export type JwtSignOptions<T> = {
header?: JwtHeader<T>
} & JwtOptions
/**
* @typedef JwtVerifyOptions
* @extends JwtOptions
* @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`)
*/
export interface JwtVerifyOptions extends JwtOptions {
/**
* If `true` all expiry checks will be skipped
*/
skipValidation?: boolean
export type JwtVerifyOptions = {
/**
* If `true` throw error if checks fail. (default: `false`)
*
* @default false
*/
throwError?: boolean
}
} & JwtOptions
/**
* @typedef JwtData
* @prop {JwtHeader} header
* @prop {JwtPayload} payload
*/
export interface JwtData {
header: JwtHeader
payload: JwtPayload
export type JwtData<Payload = {}, Header = {}> = {
header?: JwtHeader<Header>
payload?: JwtPayload<Payload>
}
const algorithms: JwtAlgorithms = {
@@ -139,28 +132,20 @@ function byteStringToBytes(byteStr: string): Uint8Array {
}
function arrayBufferToBase64String(arrayBuffer: ArrayBuffer): string {
const byteArray = new Uint8Array(arrayBuffer)
const byteStr = bytesToByteString(byteArray)
return btoa(byteStr)
return btoa(bytesToByteString(new Uint8Array(arrayBuffer)))
}
function base64StringToArrayBuffer(b64str: string): ArrayBuffer {
const byteStr = atob(b64str)
const bytes = byteStringToBytes(byteStr)
return bytes.buffer
return byteStringToBytes(atob(b64str)).buffer
}
function textToArrayBuffer(str: string): ArrayBuffer {
const buf = decodeURI(encodeURIComponent(str)) // 2 bytes for each char
const bytes = byteStringToBytes(buf)
return bytes
return byteStringToBytes(decodeURI(encodeURIComponent(str)))
}
// @ts-ignore
function arrayBufferToText(arrayBuffer: ArrayBuffer): string {
const byteArray = new Uint8Array(arrayBuffer)
const byteStr = bytesToByteString(byteArray)
return byteStr
return bytesToByteString(new Uint8Array(arrayBuffer))
}
function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string {
@@ -211,50 +196,77 @@ async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImport
return importTextSecret(key, algorithm)
}
function decodePayload(raw: string): JwtHeader | JwtPayload | null {
switch (raw.length % 4) {
case 0:
break
case 1:
raw += '==='
break
case 2:
raw += '=='
break
case 3:
raw += '='
break
default:
throw new Error('Invalid base64url string!')
}
function decodePayload<T = any>(raw: string): T | undefined {
try {
return JSON.parse(atob(raw))
const bytes = Array.from(atob(raw), char => char.charCodeAt(0));
const decodedString = new TextDecoder('utf-8').decode(new Uint8Array(bytes));
return JSON.parse(decodedString);
} catch {
return null
return
}
}
/**
* Signs a payload and returns the token
*
* @param {JwtPayload} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
* @param {string | JsonWebKey | CryptoKey} secret A string which is used to sign the payload.
* @param {JwtSignOptions | JwtAlgorithm | string} [options={ algorithm: 'HS256', header: { typ: 'JWT' } }] The options object or the algorithm.
* @throws {Error} If there's a validation issue.
* @returns {Promise<string>} Returns token as a `string`.
*/
export async function sign<Payload = {}, Header = {}>(payload: JwtPayload<Payload>, secret: string | JsonWebKey, options: JwtSignOptions<Header> | JwtAlgorithm = 'HS256'): Promise<string> {
if (typeof options === 'string')
options = { algorithm: options }
options = { algorithm: 'HS256', header: { typ: 'JWT' } as JwtHeader<Header>, ...options }
if (!payload || typeof payload !== 'object')
throw new Error('payload must be an object')
if (!secret || (typeof secret !== 'string' && typeof secret !== 'object'))
throw new Error('secret must be a string, a JWK object or a CryptoKey object')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm)
throw new Error('algorithm not found')
if (!payload.iat)
payload.iat = Math.floor(Date.now() / 1000)
const partialToken = `${textToBase64Url(JSON.stringify({ ...options.header, alg: options.algorithm }))}.${textToBase64Url(JSON.stringify(payload))}`
const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm)
const signature = await crypto.subtle.sign(algorithm, key, textToArrayBuffer(partialToken))
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 {string | JsonWebKey | CryptoKey} 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> {
export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise<boolean> {
if (typeof options === 'string')
options = { algorithm: options, throwError: false }
options = { algorithm: 'HS256', skipValidation: false, throwError: false, ...options }
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')
throw new Error('secret must be a string, a JWK object or a CryptoKey object')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
@@ -271,69 +283,25 @@ export async function verify(token: string, secret: string | JsonWebKey, options
const { payload } = decode(token)
if (!options.skipValidation && !payload) {
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 = secret instanceof CryptoKey ? secret : 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 'PARSE_ERROR'
throw err
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
*
* @param {JwtPayload} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
* @param {string | JsonWebKey} secret A string which is used to sign the payload.
* @param {JwtSignOptions | JwtAlgorithm | string} [options={ algorithm: 'HS256', header: { typ: 'JWT' } }] The options object or the algorithm.
* @throws {Error} If there's a validation issue.
* @returns {Promise<string>} Returns token as a `string`.
*/
export async function sign(payload: JwtPayload, secret: string | JsonWebKey, options: JwtSignOptions | JwtAlgorithm = 'HS256'): Promise<string> {
if (typeof options === 'string')
options = { algorithm: options }
options = { algorithm: 'HS256', header: { typ: 'JWT' }, ...options }
if (!payload || typeof payload !== 'object')
throw new Error('payload must be an object')
if (!secret || (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 algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm)
throw new Error('algorithm not found')
if (!payload.iat)
payload.iat = Math.floor(Date.now() / 1000)
const partialToken = `${textToBase64Url(JSON.stringify({ ...options.header, alg: options.algorithm }))}.${textToBase64Url(JSON.stringify(payload))}`
const key = await importKey(secret, algorithm)
const signature = await crypto.subtle.sign(algorithm, key, textToArrayBuffer(partialToken))
return `${partialToken}.${arrayBufferToBase64Url(signature)}`
}
/**
@@ -342,10 +310,10 @@ export async function sign(payload: JwtPayload, secret: string | JsonWebKey, opt
* @param {string} token The token string generated by `jwt.sign()`.
* @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 {
header: decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader,
payload: decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload
header: decodePayload<JwtHeader<Header>>(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')),
payload: decodePayload<JwtPayload<Payload>>(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
}
}