Compare commits
17 Commits
Le0Develop
...
v2.4.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
4c480d5ac7
|
|||
|
adf522c3ab
|
|||
|
7c82dff259
|
|||
|
4ceac0270f
|
|||
|
32e00ac6b9
|
|||
|
|
247da9b396 | ||
|
db0e5b51e0
|
|||
|
6594895273
|
|||
|
61a3a2ed50
|
|||
|
d7a6847206
|
|||
|
5ab19c4dc0
|
|||
|
0308d20c38
|
|||
|
703c0c4131
|
|||
|
6b3e828126
|
|||
|
35dc875f56
|
|||
|
|
11afa8eb87 | ||
|
|
b05345279d
|
21
.github/workflows/test.yml
vendored
Normal file
21
.github/workflows/test.yml
vendored
Normal 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
|
||||
10
.npmignore
10
.npmignore
@@ -1,7 +1,9 @@
|
||||
.github/
|
||||
src/
|
||||
.editorconfig
|
||||
index.spec.js
|
||||
index.test.js
|
||||
.github/
|
||||
.gitignore
|
||||
.nvmrc
|
||||
coverage/
|
||||
jest.config.ts
|
||||
src/
|
||||
tests/
|
||||
tsconfig.json
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@tsndr/cloudflare-worker-jwt",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@tsndr/cloudflare-worker-jwt",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.5",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20231025.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tsndr/cloudflare-worker-jwt",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.5",
|
||||
"description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker",
|
||||
"type": "module",
|
||||
"exports": "./index.js",
|
||||
|
||||
109
src/index.ts
109
src/index.ts
@@ -1,3 +1,12 @@
|
||||
import {
|
||||
textToArrayBuffer,
|
||||
arrayBufferToBase64Url,
|
||||
base64UrlToArrayBuffer,
|
||||
textToBase64Url,
|
||||
importKey,
|
||||
decodePayload
|
||||
} from "./utils"
|
||||
|
||||
if (typeof crypto === 'undefined' || !crypto.subtle)
|
||||
throw new Error('SubtleCrypto not supported!')
|
||||
|
||||
@@ -37,7 +46,7 @@ export type JwtHeader<T = {}> = {
|
||||
* @prop {string} [iat] Issued At
|
||||
* @prop {string} [jti] JWT ID
|
||||
*/
|
||||
export type JwtPayload<T = {}> = {
|
||||
export type JwtPayload<T = { [key: string]: any }> = {
|
||||
/** Issuer */
|
||||
iss?: string
|
||||
|
||||
@@ -58,8 +67,6 @@ export type JwtPayload<T = {}> = {
|
||||
|
||||
/** JWT ID */
|
||||
jti?: string
|
||||
|
||||
[key: string]: any
|
||||
} & T
|
||||
|
||||
/**
|
||||
@@ -115,98 +122,6 @@ const algorithms: JwtAlgorithms = {
|
||||
RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } }
|
||||
}
|
||||
|
||||
function bytesToByteString(bytes: Uint8Array): string {
|
||||
let byteStr = ''
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
byteStr += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return byteStr
|
||||
}
|
||||
|
||||
function byteStringToBytes(byteStr: string): Uint8Array {
|
||||
let bytes = new Uint8Array(byteStr.length)
|
||||
for (let i = 0; i < byteStr.length; i++) {
|
||||
bytes[i] = byteStr.charCodeAt(i)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function arrayBufferToBase64String(arrayBuffer: ArrayBuffer): string {
|
||||
return btoa(bytesToByteString(new Uint8Array(arrayBuffer)))
|
||||
}
|
||||
|
||||
function base64StringToArrayBuffer(b64str: string): ArrayBuffer {
|
||||
return byteStringToBytes(atob(b64str)).buffer
|
||||
}
|
||||
|
||||
function textToArrayBuffer(str: string): ArrayBuffer {
|
||||
return byteStringToBytes(decodeURI(encodeURIComponent(str)))
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
function arrayBufferToText(arrayBuffer: ArrayBuffer): string {
|
||||
return bytesToByteString(new Uint8Array(arrayBuffer))
|
||||
}
|
||||
|
||||
function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string {
|
||||
return arrayBufferToBase64String(arrayBuffer).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
|
||||
}
|
||||
|
||||
function base64UrlToArrayBuffer(b64url: string): ArrayBuffer {
|
||||
return base64StringToArrayBuffer(b64url.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''))
|
||||
}
|
||||
|
||||
function textToBase64Url(str: string): string {
|
||||
return btoa(str).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
|
||||
}
|
||||
|
||||
function pemToBinary(pem: string): ArrayBuffer {
|
||||
return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, ''))
|
||||
}
|
||||
|
||||
async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, ["verify", "sign"])
|
||||
}
|
||||
|
||||
async function importJwk(key: JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("jwk", key, algorithm, true, ["verify", "sign"])
|
||||
}
|
||||
|
||||
async function importPublicKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, ["verify"])
|
||||
}
|
||||
|
||||
async function importPrivateKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, ["sign"])
|
||||
}
|
||||
|
||||
async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm): Promise<CryptoKey> {
|
||||
if (typeof key === 'object')
|
||||
return importJwk(key, algorithm)
|
||||
|
||||
if (typeof key !== 'string')
|
||||
throw new Error('Unsupported key type!')
|
||||
|
||||
if (key.includes('PUBLIC'))
|
||||
return importPublicKey(key, algorithm)
|
||||
|
||||
if (key.includes('PRIVATE'))
|
||||
return importPrivateKey(key, algorithm)
|
||||
|
||||
return importTextSecret(key, algorithm)
|
||||
}
|
||||
|
||||
function decodePayload<T = any>(raw: string): T | undefined {
|
||||
try {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a payload and returns the token
|
||||
*
|
||||
@@ -241,7 +156,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 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))
|
||||
|
||||
return `${partialToken}.${arrayBufferToBase64Url(signature)}`
|
||||
@@ -293,7 +208,7 @@ export async function verify(token: string, secret: string | JsonWebKey | Crypto
|
||||
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000))
|
||||
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]}`))
|
||||
} catch(err) {
|
||||
|
||||
94
src/utils.ts
Normal file
94
src/utils.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export function bytesToByteString(bytes: Uint8Array): string {
|
||||
let byteStr = ''
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
byteStr += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return byteStr
|
||||
}
|
||||
|
||||
export function byteStringToBytes(byteStr: string): Uint8Array {
|
||||
let bytes = new Uint8Array(byteStr.length)
|
||||
for (let i = 0; i < byteStr.length; i++) {
|
||||
bytes[i] = byteStr.charCodeAt(i)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
export function arrayBufferToBase64String(arrayBuffer: ArrayBuffer): string {
|
||||
return btoa(bytesToByteString(new Uint8Array(arrayBuffer)))
|
||||
}
|
||||
|
||||
export function base64StringToArrayBuffer(b64str: string): ArrayBuffer {
|
||||
return byteStringToBytes(atob(b64str)).buffer
|
||||
}
|
||||
|
||||
export function textToArrayBuffer(str: string): ArrayBuffer {
|
||||
return byteStringToBytes(decodeURI(encodeURIComponent(str)))
|
||||
}
|
||||
|
||||
export function arrayBufferToText(arrayBuffer: ArrayBuffer): string {
|
||||
return bytesToByteString(new Uint8Array(arrayBuffer))
|
||||
}
|
||||
|
||||
export function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string {
|
||||
return arrayBufferToBase64String(arrayBuffer).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
|
||||
}
|
||||
|
||||
export function base64UrlToArrayBuffer(b64url: string): ArrayBuffer {
|
||||
return base64StringToArrayBuffer(b64url.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''))
|
||||
}
|
||||
|
||||
export function textToBase64Url(str: string): string {
|
||||
const encoder = new TextEncoder();
|
||||
const charCodes = encoder.encode(str);
|
||||
const binaryStr = String.fromCharCode(...charCodes);
|
||||
return btoa(binaryStr).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
|
||||
}
|
||||
|
||||
export function pemToBinary(pem: string): ArrayBuffer {
|
||||
return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, ''))
|
||||
}
|
||||
|
||||
type KeyUsages = 'sign' | 'verify';
|
||||
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, keyUsages: KeyUsages[]): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("jwk", key, algorithm, true, keyUsages)
|
||||
}
|
||||
|
||||
export async function importPublicKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, keyUsages)
|
||||
}
|
||||
|
||||
export async function importPrivateKey(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, keyUsages)
|
||||
}
|
||||
|
||||
export async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
|
||||
if (typeof key === 'object')
|
||||
return importJwk(key, algorithm, keyUsages)
|
||||
|
||||
if (typeof key !== 'string')
|
||||
throw new Error('Unsupported key type!')
|
||||
|
||||
if (key.includes('PUBLIC'))
|
||||
return importPublicKey(key, algorithm, keyUsages)
|
||||
|
||||
if (key.includes('PRIVATE'))
|
||||
return importPrivateKey(key, algorithm, keyUsages)
|
||||
|
||||
return importTextSecret(key, algorithm, keyUsages)
|
||||
}
|
||||
|
||||
export function decodePayload<T = any>(raw: string): T | undefined {
|
||||
try {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto'
|
||||
Object.defineProperty(global, 'crypto', { value: { subtle: crypto.webcrypto.subtle }})
|
||||
|
||||
import { describe, expect, test } from '@jest/globals'
|
||||
import jwt, { JwtAlgorithm } from '.'
|
||||
import jwt, { JwtAlgorithm } from '../src/index'
|
||||
|
||||
type Dataset = {
|
||||
public: string
|
||||
@@ -73,6 +73,11 @@ const payload: Payload = {
|
||||
name: "John Doe",
|
||||
}
|
||||
|
||||
const unicodePayload: Payload = {
|
||||
sub: "1234567890",
|
||||
name: "John Doe 😎",
|
||||
}
|
||||
|
||||
describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorithm, data) => {
|
||||
let token = ''
|
||||
|
||||
@@ -97,6 +102,11 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith
|
||||
expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/)
|
||||
})
|
||||
|
||||
test('sign unciode', async () => {
|
||||
token = await jwt.sign<Payload>(unicodePayload, data.private, algorithm)
|
||||
expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/)
|
||||
})
|
||||
|
||||
test('decode internal', async () => {
|
||||
const decoded = jwt.decode(token)
|
||||
expect({
|
||||
81
tests/utils.spec.ts
Normal file
81
tests/utils.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from '@jest/globals'
|
||||
import {
|
||||
bytesToByteString,
|
||||
byteStringToBytes,
|
||||
arrayBufferToBase64String,
|
||||
base64StringToArrayBuffer,
|
||||
textToArrayBuffer,
|
||||
arrayBufferToText,
|
||||
arrayBufferToBase64Url,
|
||||
base64UrlToArrayBuffer,
|
||||
textToBase64Url,
|
||||
pemToBinary,
|
||||
importTextSecret
|
||||
} from '../src/utils'
|
||||
|
||||
describe('Converters', () => {
|
||||
const testString = 'cloudflare-worker-jwt'
|
||||
const testByteArray = [ 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 45, 119, 111, 114, 107, 101, 114, 45, 106, 119, 116 ]
|
||||
const testUint8Array = new Uint8Array(testByteArray)
|
||||
const testBase64String = 'Y2xvdWRmbGFyZS13b3JrZXItand0'
|
||||
const testArrayBuffer = testUint8Array.buffer
|
||||
|
||||
test('bytesToByteString', () => {
|
||||
expect(bytesToByteString(testUint8Array)).toStrictEqual(testString)
|
||||
})
|
||||
|
||||
test('byteStringToBytes', () => {
|
||||
expect(byteStringToBytes(testString)).toStrictEqual(testUint8Array)
|
||||
})
|
||||
|
||||
test('arrayBufferToBase64String', () => {
|
||||
expect(arrayBufferToBase64String(testArrayBuffer)).toStrictEqual(testBase64String)
|
||||
})
|
||||
|
||||
test('base64StringToArrayBuffer', () => {
|
||||
expect(base64StringToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer)
|
||||
})
|
||||
|
||||
test('textToArrayBuffer', () => {
|
||||
expect(textToArrayBuffer(testString)).toStrictEqual(testUint8Array)
|
||||
})
|
||||
|
||||
test('arrayBufferToText', () => {
|
||||
expect(arrayBufferToText(testArrayBuffer)).toStrictEqual(testString)
|
||||
})
|
||||
|
||||
test('arrayBufferToBase64Url', () => {
|
||||
expect(arrayBufferToBase64Url(testArrayBuffer)).toStrictEqual(testBase64String)
|
||||
})
|
||||
|
||||
test('base64UrlToArrayBuffer', () => {
|
||||
expect(base64UrlToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer)
|
||||
})
|
||||
|
||||
test('textToBase64Url', () => {
|
||||
expect(textToBase64Url(testString)).toStrictEqual(testBase64String)
|
||||
})
|
||||
|
||||
test('pemToBinary', () => {
|
||||
expect(pemToBinary(`-----BEGIN PUBLIC KEY-----\n${testBase64String}\n-----END PUBLIC KEY-----`)).toStrictEqual(testArrayBuffer)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Imports', () => {
|
||||
test('importTextSecret', async () => {
|
||||
const testKey = 'cloudflare-worker-jwt'
|
||||
const testAlgorithm = { name: 'HMAC', hash: { name: 'SHA-256' } }
|
||||
const testCryptoKey = { type: 'secret', extractable: true, algorithm: { ...testAlgorithm, length: 168 }, usages: ['verify', 'sign'] }
|
||||
|
||||
expect(await importTextSecret(testKey, testAlgorithm, ['verify', 'sign'])).toMatchObject(testCryptoKey)
|
||||
})
|
||||
|
||||
//test('importJwk', async () => {})
|
||||
//test('importPublicKey', async () => {})
|
||||
//test('importPrivateKey', async () => {})
|
||||
//test('importKey', async () => {})
|
||||
})
|
||||
|
||||
//describe('Payload', () => {
|
||||
// test('decodePayload', () => {})
|
||||
//})
|
||||
Reference in New Issue
Block a user