1
0

Compare commits

...

12 Commits

Author SHA1 Message Date
e6baf7ab75 add algorithm to header and check during verification 2024-02-21 21:04:03 +01:00
cf24b34f63 clean up 2024-02-21 21:03:44 +01:00
91f30929da 2.4.6 2024-02-21 20:07:05 +01:00
93082884fa add esbuild 2024-02-21 19:59:14 +01:00
a9e83968b9 switch to vitest 2024-02-21 19:54:08 +01:00
4c480d5ac7 2.4.5 2024-02-01 01:10:51 +01:00
adf522c3ab Revert "add esbuild for bundling, update dependencies"
This reverts commit 4ceac0270f.
2024-02-01 01:10:32 +01:00
7c82dff259 2.4.4 2024-02-01 00:56:22 +01:00
4ceac0270f add esbuild for bundling, update dependencies 2024-02-01 00:56:12 +01:00
32e00ac6b9 2.4.3 2024-01-26 15:03:51 +01:00
Nick DeGroot
247da9b396 🐛 Fix verification relying on a signing key 2024-01-26 15:02:52 +01:00
db0e5b51e0 add test workflow 2024-01-26 15:00:40 +01:00
13 changed files with 1168 additions and 3327 deletions

21
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: latest
registry-url: https://registry.npmjs.org/
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test

2
.gitignore vendored
View File

@@ -146,3 +146,5 @@ dist
# Custom # Custom
/index.js /index.js
/index.d.ts /index.d.ts
/utils.js
/utils.d.ts

View File

@@ -3,7 +3,9 @@
.gitignore .gitignore
.nvmrc .nvmrc
coverage/ coverage/
jest.config.ts vite.config.ts
src/ src/
tests/ tests/
tsconfig.json tsconfig.json
utils.*
*.tgz

View File

@@ -1,9 +0,0 @@
import type { Config } from 'jest'
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true
}
export default config

4354
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "2.4.2", "version": "2.4.6",
"description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker", "description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker",
"type": "module", "type": "module",
"exports": "./index.js", "exports": "./index.js",
@@ -9,8 +9,8 @@
"node": ">=18" "node": ">=18"
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc & esbuild --bundle --target=esnext --platform=neutral --outfile=index.js src/index.ts & wait",
"test": "jest" "test": "vitest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -30,13 +30,11 @@
}, },
"homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme", "homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme",
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20231025.0", "@cloudflare/workers-types": "^4.20240208.0",
"@jest/globals": "^29.7.0", "@edge-runtime/vm": "^3.2.0",
"@types/jest": "^29.5.8", "@types/node": "^20.11.19",
"@types/node": "^20.9.0", "ts-node": "^10.9.2",
"jest": "^29.7.0", "typescript": "^5.3.3",
"ts-jest": "^29.1.1", "vitest": "^1.3.1"
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
} }
} }

View File

@@ -1,10 +1,10 @@
import { import {
textToArrayBuffer, textToArrayBuffer,
arrayBufferToBase64Url, arrayBufferToBase64Url,
base64UrlToArrayBuffer, base64UrlToArrayBuffer,
textToBase64Url, textToBase64Url,
importKey, importKey,
decodePayload decodePayload
} from "./utils" } from "./utils"
if (typeof crypto === 'undefined' || !crypto.subtle) if (typeof crypto === 'undefined' || !crypto.subtle)
@@ -34,6 +34,13 @@ export type JwtHeader<T = {}> = {
* @default "JWT" * @default "JWT"
*/ */
typ?: string typ?: string
/**
* Algorithm (default: `"HS256"`)
*
* @default "HS256"
*/
alg?: JwtAlgorithm
} & T } & T
/** /**
@@ -156,7 +163,7 @@ export async function sign<Payload = {}, Header = {}>(payload: JwtPayload<Payloa
const partialToken = `${textToBase64Url(JSON.stringify({ ...options.header, alg: options.algorithm }))}.${textToBase64Url(JSON.stringify(payload))}` 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 key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ['sign'])
const signature = await crypto.subtle.sign(algorithm, key, textToArrayBuffer(partialToken)) const signature = await crypto.subtle.sign(algorithm, key, textToArrayBuffer(partialToken))
return `${partialToken}.${arrayBufferToBase64Url(signature)}` return `${partialToken}.${arrayBufferToBase64Url(signature)}`
@@ -196,7 +203,13 @@ export async function verify(token: string, secret: string | JsonWebKey | Crypto
if (!algorithm) if (!algorithm)
throw new Error('algorithm not found') throw new Error('algorithm not found')
const { payload } = decode(token) const { header, payload } = decode(token)
if (header?.alg !== options.algorithm) {
if (options.throwError)
throw new Error('ALG_MISMATCH')
return false
}
try { try {
if (!payload) if (!payload)
@@ -208,13 +221,12 @@ export async function verify(token: string, secret: string | JsonWebKey | Crypto
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000))
throw new Error('EXPIRED') throw new Error('EXPIRED')
const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm) const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ['verify'])
return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`)) return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`))
} catch(err) { } catch(err) {
if (options.throwError) if (options.throwError)
throw err throw err
return false return false
} }
} }

3
src/test.ts Normal file
View File

@@ -0,0 +1,3 @@
import { sign } from './index'
console.log(await sign())

View File

@@ -49,36 +49,37 @@ export function pemToBinary(pem: string): ArrayBuffer {
return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, '')) return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, ''))
} }
export async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> { type KeyUsages = 'sign' | 'verify';
return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, ["verify", "sign"]) export async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, keyUsages)
} }
export async function importJwk(key: JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> { export async function importJwk(key: JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
return await crypto.subtle.importKey("jwk", key, algorithm, true, ["verify", "sign"]) return await crypto.subtle.importKey("jwk", key, algorithm, true, keyUsages)
} }
export async function importPublicKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> { export async function importPublicKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, ["verify"]) return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, keyUsages)
} }
export async function importPrivateKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> { export async function importPrivateKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, ["sign"]) return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, keyUsages)
} }
export async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> { export async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
if (typeof key === 'object') if (typeof key === 'object')
return importJwk(key, algorithm) return importJwk(key, algorithm, keyUsages)
if (typeof key !== 'string') if (typeof key !== 'string')
throw new Error('Unsupported key type!') throw new Error('Unsupported key type!')
if (key.includes('PUBLIC')) if (key.includes('PUBLIC'))
return importPublicKey(key, algorithm) return importPublicKey(key, algorithm, keyUsages)
if (key.includes('PRIVATE')) if (key.includes('PRIVATE'))
return importPrivateKey(key, algorithm) return importPrivateKey(key, algorithm, keyUsages)
return importTextSecret(key, algorithm) return importTextSecret(key, algorithm, keyUsages)
} }
export function decodePayload<T = any>(raw: string): T | undefined { export function decodePayload<T = any>(raw: string): T | undefined {

View File

@@ -1,7 +1,4 @@
import crypto from 'node:crypto' import { describe, expect, test } from 'vitest'
Object.defineProperty(global, 'crypto', { value: { subtle: crypto.webcrypto.subtle }})
import { describe, expect, test } from '@jest/globals'
import jwt, { JwtAlgorithm } from '../src/index' import jwt, { JwtAlgorithm } from '../src/index'
type Dataset = { type Dataset = {

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from '@jest/globals' import { describe, expect, test } from 'vitest'
import { import {
bytesToByteString, bytesToByteString,
byteStringToBytes, byteStringToBytes,
@@ -67,7 +67,7 @@ describe('Imports', () => {
const testAlgorithm = { name: 'HMAC', hash: { name: 'SHA-256' } } const testAlgorithm = { name: 'HMAC', hash: { name: 'SHA-256' } }
const testCryptoKey = { type: 'secret', extractable: true, algorithm: { ...testAlgorithm, length: 168 }, usages: ['verify', 'sign'] } const testCryptoKey = { type: 'secret', extractable: true, algorithm: { ...testAlgorithm, length: 168 }, usages: ['verify', 'sign'] }
expect(await importTextSecret(testKey, testAlgorithm)).toMatchObject(testCryptoKey) expect(await importTextSecret(testKey, testAlgorithm, ['verify', 'sign'])).toMatchObject(testCryptoKey)
}) })
//test('importJwk', async () => {}) //test('importJwk', async () => {})

View File

@@ -5,6 +5,7 @@
"target": "esnext", "target": "esnext",
"lib": ["esnext"], "lib": ["esnext"],
"declaration": true, "declaration": true,
"emitDeclarationOnly": true,
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,

9
vite.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'edge-runtime',
watch: false,
reporters: ['verbose']
}
})