1
0

Compare commits

..

11 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
4 changed files with 44 additions and 38 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "2.3.0", "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.3.0", "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,6 +1,6 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "2.3.0", "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",
"type": "module", "type": "module",
"exports": "./index.js", "exports": "./index.js",

View File

@@ -14,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-----',
@@ -63,7 +68,7 @@ const data: Data = {
} }
const payload = { const payload: Payload = {
sub: "1234567890", sub: "1234567890",
name: "John Doe", name: "John Doe",
} }
@@ -77,7 +82,7 @@ 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
@@ -88,7 +93,7 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith
}) })
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\-_]+$/)
}) })

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,32 +75,32 @@ export interface JwtOptions {
* @extends JwtOptions * @extends JwtOptions
* @prop {JwtHeader} [header] * @prop {JwtHeader} [header]
*/ */
export interface JwtSignOptions extends JwtOptions { export type JwtSignOptions<T> = {
header?: JwtHeader header?: JwtHeader<T>
} } & 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` 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 = {
@@ -200,7 +198,10 @@ async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImport
function decodePayload<T = any>(raw: string): T | undefined { function decodePayload<T = any>(raw: string): T | undefined {
try { 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 { } catch {
return return
} }
@@ -210,22 +211,22 @@ function decodePayload<T = any>(raw: string): T | undefined {
* Signs a payload and returns the token * 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 {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 {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. * @param {JwtSignOptions | JwtAlgorithm | string} [options={ algorithm: 'HS256', header: { typ: 'JWT' } }] The options object or the algorithm.
* @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 = {}, Header = {}>(payload: JwtPayload<Payload>, secret: string | JsonWebKey, options: JwtSignOptions<Header> | 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' } as JwtHeader<Header>, ...options }
if (!payload || typeof payload !== 'object') if (!payload || typeof payload !== 'object')
throw new Error('payload must be an object') throw new Error('payload must be an object')
if (!secret || (typeof secret !== 'string' && typeof secret !== 'object')) if (!secret || (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') if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string') throw new Error('options.algorithm must be a string')
@@ -240,7 +241,7 @@ export async function sign(payload: JwtPayload, secret: string | JsonWebKey, opt
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 = await importKey(secret, algorithm) const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm)
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)}`
@@ -250,12 +251,12 @@ export async function sign(payload: JwtPayload, secret: string | JsonWebKey, opt
* Verifies the integrity of the token and returns a boolean value. * Verifies the integrity of the token and returns a boolean value.
* *
* @param {string} token The token string generated by `jwt.sign()`. * @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. * @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. * @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`. * @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> { export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise<boolean> {
if (typeof options === 'string') if (typeof options === 'string')
options = { algorithm: options, throwError: false } options = { algorithm: options, throwError: false }
@@ -265,7 +266,7 @@ export async function verify(token: string, secret: string | JsonWebKey, options
throw new Error('token must be a string') throw new Error('token must be a string')
if (typeof secret !== 'string' && typeof secret !== 'object') 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') if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string') throw new Error('options.algorithm must be a string')
@@ -292,7 +293,7 @@ export async function verify(token: string, secret: string | JsonWebKey, options
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 = await importKey(secret, algorithm) 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]}`)) return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`))
} catch(err) { } catch(err) {
@@ -309,10 +310,10 @@ export async function verify(token: string, secret: string | JsonWebKey, options
* @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<JwtHeader>(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')), header: decodePayload<JwtHeader<Header>>(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')),
payload: decodePayload<JwtPayload>(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) payload: decodePayload<JwtPayload<Payload>>(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
} }
} }