1
0

Compare commits

..

38 Commits

Author SHA1 Message Date
2420f57628 3.2.0 2025-05-29 01:28:50 +02:00
1b4c93adb2 add algorithm none 2025-05-29 01:26:59 +02:00
06c5605bf2 3.1.7 2025-05-26 01:04:33 +02:00
a4edaba6f0 clean up tests 2025-05-26 01:02:50 +02:00
b2a3b4c25f 3.1.6 2025-05-26 00:58:30 +02:00
c691324515 update jwk type and dev deps 2025-05-26 00:57:04 +02:00
66385f323c 3.1.5 2025-05-11 23:12:57 +02:00
1b6ac02f7c update readme 2025-05-11 23:11:14 +02:00
95e45e67f6 3.1.4 2025-03-14 14:34:33 +01:00
5e6af9cf25 update dependencies and address type errors 2025-03-14 14:31:50 +01:00
d3d7e10b60 3.1.3 2024-10-27 13:45:10 +01:00
fdaa618f65 update pipelines 2024-10-27 13:44:59 +01:00
cce5a61777 update readme 2024-10-27 13:43:25 +01:00
e57ccf7e1c 3.1.2 2024-10-07 19:18:46 +02:00
0d4eb1919b clean up readme 2024-10-07 19:18:26 +02:00
ea136cf1a5 3.1.1 2024-10-03 19:32:59 +02:00
457a989fe3 update readme 2024-10-03 19:32:05 +02:00
9f74c8b56c 3.1.0 2024-10-03 19:25:44 +02:00
c7ce8686d3 add options.header 2024-10-03 19:25:22 +02:00
9a68f6fdad 3.0.0 2024-10-03 19:16:37 +02:00
11a002052d make verify return decoded token 2024-10-03 19:15:14 +02:00
674ff1ddb5 2.5.4 2024-09-28 02:27:55 +02:00
9b5be8b554 update dev dependencies 2024-09-28 02:27:45 +02:00
fc72ce01d5 npm audit fix 2024-09-28 02:26:51 +02:00
Abiria
b46db60e45 fix: include esbuild as devDependency 2024-09-28 02:24:48 +02:00
2d7eed49da audit fix 2024-07-15 22:13:45 +02:00
Denbeigh Stevens
7468a3e102 fix sign/verify secret typing 2024-06-23 23:00:40 +02:00
8a75c24253 2.5.3 2024-03-08 21:58:04 +01:00
Kendell R
38b8c3e2d3 Don't break = 2024-03-08 21:57:22 +01:00
e49bada1a5 2.5.2 2024-03-01 20:32:48 +01:00
c75c26044f remove math abs 2024-03-01 20:31:24 +01:00
5942b3d1bd clean up tests 2024-02-24 16:46:17 +01:00
c1214f897a 2.5.1 2024-02-23 23:46:56 +01:00
f3dfdff3bb fix falsly invalid tokens 2024-02-23 23:42:38 +01:00
0cd10e17f7 remove console.log 2024-02-22 23:24:19 +01:00
884371fac6 remove node from dev deps 2024-02-22 23:21:31 +01:00
83678c92eb 2.5.0 2024-02-22 23:04:39 +01:00
37fdf4f602 update readme 2024-02-22 23:03:05 +01:00
11 changed files with 1179 additions and 1298 deletions

View File

@@ -1,4 +1,5 @@
name: Publish name: Publish
run-name: Publish ${{ github.ref_name }}
on: on:
release: release:
@@ -13,18 +14,27 @@ jobs:
with: with:
node-version: latest node-version: latest
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run tests
run: npm test
- name: Build - name: Build
run: npm run build run: npm run build
- name: Publish to npmjs - name: Publish to npmjs
run: npm publish --tag latest --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --tag ${{ contains(github.ref_name, '-') && 'pre' || 'latest' }} --access public
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: latest
registry-url: https://npm.pkg.github.com/ registry-url: https://npm.pkg.github.com/
- name: Publish to GPR - name: Publish to GPR
run: npm publish --tag latest --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm publish --tag ${{ contains(github.ref_name, '-') && 'pre' || 'latest' }} --access public

31
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Release
run-name: Release ${{ github.ref_name }}
on:
push:
tags:
- 'v*'
jobs:
release:
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
- name: Build
run: npm run build
- name: Create release
run: gh release create ${{ github.ref_name }} --draft ${{ contains(github.ref_name, '-') && '--prerelease --latest=false' || '--latest' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -15,7 +15,12 @@ jobs:
with: with:
node-version: latest node-version: latest
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run tests - name: Run tests
run: npm test run: npm test
- name: Build
run: npm run build

164
README.md
View File

@@ -15,57 +15,68 @@ A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers.
## Install ## Install
``` ```bash
npm i @tsndr/cloudflare-worker-jwt npm i @tsndr/cloudflare-worker-jwt
``` ```
## Examples ## Examples
### Basic Example ### Basic Example
```typescript ```typescript
import jwt from "@tsndr/cloudflare-worker-jwt"
async () => { async () => {
import jwt from '@tsndr/cloudflare-worker-jwt'
// Creating a token // Create a token
const token = await jwt.sign({ name: 'John Doe', email: 'john.doe@gmail.com' }, 'secret') const token = await jwt.sign({
sub: "1234",
name: "John Doe",
email: "john.doe@gmail.com"
}, "secret")
// Verifing token // Verify token
const isValid = await jwt.verify(token, 'secret') const verifiedToken = await jwt.verify(token, "secret")
// Check for validity // Abort if token isn't valid
if (!isValid) if (!verifiedToken)
return return
// Decoding token // Access token payload
const { payload } = jwt.decode(token) const { payload } = verifiedToken
// { sub: "1234", name: "John Doe", email: "john.doe@gmail.com" }
} }
``` ```
### Restrict Timeframe ### Restrict Timeframe
```typescript ```typescript
async () => { import jwt from "@tsndr/cloudflare-worker-jwt"
import jwt from '@tsndr/cloudflare-worker-jwt'
// Creating a token async () => {
// Create a token
const token = await jwt.sign({ const token = await jwt.sign({
name: 'John Doe', sub: "1234",
email: 'john.doe@gmail.com', name: "John Doe",
email: "john.doe@gmail.com",
nbf: Math.floor(Date.now() / 1000) + (60 * 60), // Not before: Now + 1h nbf: Math.floor(Date.now() / 1000) + (60 * 60), // Not before: Now + 1h
exp: Math.floor(Date.now() / 1000) + (2 * (60 * 60)) // Expires: Now + 2h exp: Math.floor(Date.now() / 1000) + (2 * (60 * 60)) // Expires: Now + 2h
}, 'secret') }, "secret")
// Verifing token // Verify token
const isValid = await jwt.verify(token, 'secret') // false const verifiedToken = await jwt.verify(token, "secret")
// Check for validity // Abort if token isn't valid
if (!isValid) if (!verifiedToken)
return return
// Decoding token // Access token payload
const { payload } = jwt.decode(token) // { name: 'John Doe', email: 'john.doe@gmail.com', ... } const { payload } = verifiedToken
// { sub: "1234", name: "John Doe", email: "john.doe@gmail.com", ... }
} }
``` ```
@@ -78,73 +89,104 @@ async () => {
<hr> <hr>
### Sign ### Sign
#### `jwt.sign(payload, secret, [options])` #### `sign(payload, secret, [options])`
Signs a payload and returns the token. Signs a payload and returns the token.
#### Arguments #### Arguments
Argument | Type | Status | Default | Description Argument | Type | Status | Default | Description
----------- | -------- | -------- | ------- | ----------- ------------------------ | ----------------------------------- | -------- | ----------- | -----------
`payload` | `object` | required | - | The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload. `payload` | `object` | required | - | The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
`secret` | `string` | required | - | A string which is used to sign the payload. `secret` | `string`, `JsonWebKey`, `CryptoKey` | optional | - | A secret which is used to sign the payload.
`options` | `object` | optional | `{ algorithm: 'HS256' }` | The options object supporting `algorithm` and `keyid`. (See [Available Algorithms](#available-algorithms)) `options` | `string`, `object` | optional | `HS256` | Either the `algorithm` string or an object.
`options.algorithm` | `string` | optional | `HS256` | See [Available Algorithms](#available-algorithms)
`options.header` | `object` | optional | `undefined` | Extend the header of the resulting JWT.
#### `return` #### `return`
Returns token as a `string`. Returns token as a `string`.
<hr> <hr>
### Verify ### Verify
#### `jwt.verify(token, secret, [options])` #### `verify(token, secret, [options])`
Verifies the integrity of the token and returns a boolean value. Verifies the integrity of the token.
Argument | Type | Status | Default | Description Argument | Type | Status | Default | Description
----------- | -------- | -------- | ------- | ----------- ------------------------ | ----------------------------------- | -------- | ------- | -----------
`token` | `string` | required | - | The token string generated by `jwt.sign()`. `token` | `string` | required | - | The token string generated by `sign()`.
`secret` | `string` | required | - | The string which was used to sign the payload. `secret` | `string`, `JsonWebKey`, `CryptoKey` | optional | - | The secret which was used to sign the payload.
`options` | `object` | optional | `{ algorithm: 'HS256', skipValidation: false, throwError: false }` | The options object supporting `algorithm`, `skipValidation` and `throwError`. (See [Available Algorithms](#available-algorithms)) `options` | `string`, `object` | optional | `HS256` | Either the `algorithm` string or an object.
`options.algorithm` | `string` | optional | `HS256` | See [Available Algorithms](#available-algorithms)
`options.clockTolerance` | `number` | optional | `0` | Clock tolerance in seconds, to help with slighly out of sync systems.
`options.throwError` | `boolean` | optional | `false` | By default this we will only throw integration errors, only set this to `true` if you want verification errors to be thrown as well.
#### `throws` #### `throws`
If `options.throwError` is `true` and the token is invalid, an error will be thrown.
Throws integration errors and if `options.throwError` is set to `true` also throws `ALG_MISMATCH`, `NOT_YET_VALID`, `EXPIRED` or `INVALID_SIGNATURE`.
#### `return` #### `return`
Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
<hr> Returns the decoded token or `undefined`.
### Decode ```typescript
#### `jwt.decode(token)`
Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure!
Argument | Type | Status | Default | Description
----------- | -------- | -------- | ------- | -----------
`token` | `string` | required | - | The token string generated by `jwt.sign()`.
#### `return`
Returns an `object` containing `header` and `payload`:
```javascript
{ {
header: { header: {
alg: 'HS256', alg: "HS256",
typ: 'JWT' typ: "JWT"
}, },
payload: { payload: {
name: 'John Doe', sub: "1234",
email: 'john.doe@gmail.com' name: "John Doe",
email: "john.doe@gmail.com"
} }
} }
``` ```
<hr>
### Decode
#### `decode(token)`
Just returns the decoded token **without** verifying verifying it. Please use `verify()` if you intend to verify it as well.
Argument | Type | Status | Default | Description
----------- | -------- | -------- | ------- | -----------
`token` | `string` | required | - | The token string generated by `sign()`.
#### `return`
Returns an `object` containing `header` and `payload`:
```typescript
{
header: {
alg: "HS256",
typ: "JWT"
},
payload: {
sub: "1234",
name: "John Doe",
email: "john.doe@gmail.com"
}
}
```
### Available Algorithms ### Available Algorithms
- ES256
- ES384 - `ES256`, `ES384`, `ES512`
- ES512 - `HS256`, `HS384`, `HS512`
- HS256 - `RS256`, `RS384`, `RS512`
- HS384 - `none`
- HS512
- RS256
- RS384
- RS512

1622
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.7", "version": "3.2.0",
"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",
@@ -30,11 +30,10 @@
}, },
"homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme", "homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme",
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0", "@cloudflare/workers-types": "^4.20250525.0",
"@edge-runtime/vm": "^3.2.0", "@edge-runtime/vm": "^5.0.0",
"@types/node": "^20.11.19", "esbuild": "^0.25.4",
"ts-node": "^10.9.2", "typescript": "^5.8.3",
"typescript": "^5.3.3", "vitest": "^3.1.4"
"vitest": "^1.3.1"
} }
} }

View File

@@ -1,20 +1,20 @@
import { import {
textToArrayBuffer, textToUint8Array,
arrayBufferToBase64Url, arrayBufferToBase64Url,
base64UrlToArrayBuffer, base64UrlToUint8Array,
textToBase64Url, textToBase64Url,
importKey, importKey,
decodePayload decodePayload
} from "./utils" } from "./utils"
if (typeof crypto === 'undefined' || !crypto.subtle) if (typeof crypto === "undefined" || !crypto.subtle)
throw new Error('SubtleCrypto not supported!') throw new Error("SubtleCrypto not supported!")
/** /**
* @typedef JwtAlgorithm * @typedef JwtAlgorithm
* @type {'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'} * @type {"none" | "ES256" | "ES384" | "ES512" | "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512"}
*/ */
export type JwtAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' export type JwtAlgorithm = "none" | "ES256" | "ES384" | "ES512" | "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512"
/** /**
* @typedef JwtAlgorithms * @typedef JwtAlgorithms
@@ -118,58 +118,62 @@ export type JwtVerifyOptions = {
* @prop {JwtPayload} payload * @prop {JwtPayload} payload
*/ */
export type JwtData<Payload = {}, Header = {}> = { export type JwtData<Payload = {}, Header = {}> = {
header?: JwtHeader<Header> header: JwtHeader<Header>
payload?: JwtPayload<Payload> payload: JwtPayload<Payload>
} }
const algorithms: JwtAlgorithms = { const algorithms: JwtAlgorithms = {
ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } }, none: { name: "none" },
ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } }, ES256: { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
ES512: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } }, ES384: { name: "ECDSA", namedCurve: "P-384", hash: { name: "SHA-384" } },
HS256: { name: 'HMAC', hash: { name: 'SHA-256' } }, ES512: { name: "ECDSA", namedCurve: "P-521", hash: { name: "SHA-512" } },
HS384: { name: 'HMAC', hash: { name: 'SHA-384' } }, HS256: { name: "HMAC", hash: { name: "SHA-256" } },
HS512: { name: 'HMAC', hash: { name: 'SHA-512' } }, HS384: { name: "HMAC", hash: { name: "SHA-384" } },
RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } }, HS512: { name: "HMAC", hash: { name: "SHA-512" } },
RS384: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } }, RS256: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } } RS384: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-384" } },
RS512: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-512" } }
} }
/** /**
* 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 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 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 [options={ algorithm: "HS256", header: { typ: "JWT" } }] The options object or the algorithm.
* @throws {Error} If there's a validation issue. * @throws If there"s a validation issue.
* @returns {Promise<string>} Returns token as a `string`. * @returns Returns token as a `string`.
*/ */
export async function sign<Payload = {}, Header = {}>(payload: JwtPayload<Payload>, secret: string | JsonWebKey, options: JwtSignOptions<Header> | JwtAlgorithm = 'HS256'): Promise<string> { export async function sign<Payload = {}, Header = {}>(payload: JwtPayload<Payload>, secret: string | JsonWebKeyWithKid | CryptoKey | undefined, 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' } as JwtHeader<Header>, ...options } options = { algorithm: "HS256", header: { typ: "JWT", ...(options.header ?? {}) } 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 (options.algorithm !== "none" && (!secret || (typeof secret !== "string" && typeof secret !== "object")))
throw new Error('secret must be a string, a JWK object or a CryptoKey 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")
const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm] const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm) if (!algorithm)
throw new Error('algorithm not found') throw new Error("algorithm not found")
if (!payload.iat) if (!payload.iat)
payload.iat = Math.floor(Date.now() / 1000) payload.iat = Math.floor(Date.now() / 1000)
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, ['sign']) if (options.algorithm === "none")
const signature = await crypto.subtle.sign(algorithm, key, textToArrayBuffer(partialToken)) return partialToken
const key = secret instanceof CryptoKey ? secret : await importKey(secret!, algorithm, ["sign"])
const signature = await crypto.subtle.sign(algorithm, key, textToUint8Array(partialToken))
return `${partialToken}.${arrayBufferToBase64Url(signature)}` return `${partialToken}.${arrayBufferToBase64Url(signature)}`
} }
@@ -177,76 +181,80 @@ export async function sign<Payload = {}, Header = {}>(payload: JwtPayload<Payloa
/** /**
* 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 token The token string generated by `sign()`.
* @param {string | JsonWebKey | CryptoKey} secret The string which was used to sign the payload. * @param secret The string which was used to sign the payload.
* @param {JWTVerifyOptions | JWTAlgorithm} options The options object or the algorithm. * @param 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 Throws integration errors and if `options.throwError` is set to `true` also throws `NOT_YET_VALID`, `EXPIRED` or `INVALID_SIGNATURE`.
* @returns {Promise<boolean>} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`. * @returns Returns the decoded token or `undefined`.
*/ */
export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = 'HS256'): Promise<boolean> { export async function verify<Payload = {}, Header = {}>(token: string, secret: string | JsonWebKeyWithKid | CryptoKey | undefined, options: JwtVerifyOptions | JwtAlgorithm = "HS256"): Promise<JwtData<Payload, Header> | undefined> {
if (typeof options === 'string') if (typeof options === "string")
options = { algorithm: options } options = { algorithm: options }
options = { algorithm: 'HS256', clockTolerance: 0, throwError: false, ...options } options = { algorithm: "HS256", clockTolerance: 0, throwError: false, ...options }
if (typeof token !== 'string') if (typeof token !== "string")
throw new Error('token must be a string') throw new Error("token must be a string")
if (typeof secret !== 'string' && typeof secret !== 'object') if (options.algorithm !== "none" && typeof secret !== "string" && typeof secret !== "object")
throw new Error('secret must be a string, a JWK object or a CryptoKey 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")
const tokenParts = token.split('.') const tokenParts = token.split(".", 3)
if (tokenParts.length !== 3) if (tokenParts.length < 2)
throw new Error('token must consist of 3 parts') throw new Error("token must consist of 2 or more parts")
const [ tokenHeader, tokenPayload, tokenSignature ] = tokenParts
const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm] const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm) if (!algorithm)
throw new Error('algorithm not found') throw new Error("algorithm not found")
const { header, payload } = decode(token) const decodedToken = decode<Payload, Header>(token)
if (header?.alg !== options.algorithm) {
if (options.throwError)
throw new Error('ALG_MISMATCH')
return false
}
try { try {
if (!payload) if (decodedToken.header?.alg !== options.algorithm)
throw new Error('PARSE_ERROR') throw new Error("INVALID_SIGNATURE")
if (decodedToken.payload) {
const now = Math.floor(Date.now() / 1000) const now = Math.floor(Date.now() / 1000)
if (payload.nbf && Math.abs(payload.nbf - now) > (options.clockTolerance ?? 0)) if (decodedToken.payload.nbf && decodedToken.payload.nbf > now && (decodedToken.payload.nbf - now) > (options.clockTolerance ?? 0))
throw new Error('NOT_YET_VALID') throw new Error("NOT_YET_VALID")
if (payload.exp && Math.abs(payload.exp - now) > (options.clockTolerance ?? 0)) if (decodedToken.payload.exp && decodedToken.payload.exp <= now && (now - decodedToken.payload.exp) > (options.clockTolerance ?? 0))
throw new Error('EXPIRED') throw new Error("EXPIRED")
}
const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ['verify']) if (algorithm.name === "none")
return decodedToken
return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`)) const key = secret instanceof CryptoKey ? secret : await importKey(secret!, algorithm, ["verify"])
if (!await crypto.subtle.verify(algorithm, key, base64UrlToUint8Array(tokenSignature), textToUint8Array(`${tokenHeader}.${tokenPayload}`)))
throw new Error("INVALID_SIGNATURE")
return decodedToken
} catch(err) { } catch(err) {
if (options.throwError) if (options.throwError)
throw err throw err
return false return
} }
} }
/** /**
* 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 `verify()` first to keep your application secure!
* *
* @param {string} token The token string generated by `jwt.sign()`. * @param token The token string generated by `sign()`.
* @returns {JwtData} Returns an `object` containing `header` and `payload`. * @returns Returns an `object` containing `header` and `payload`.
*/ */
export function decode<Payload = {}, Header = {}>(token: string): JwtData<Payload, Header> { export function decode<Payload = {}, Header = {}>(token: string): JwtData<Payload, Header> {
return { return {
header: decodePayload<JwtHeader<Header>>(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')), header: decodePayload<JwtHeader<Header>>(token.split(".")[0].replace(/-/g, "+").replace(/_/g, "/")),
payload: decodePayload<JwtPayload<Payload>>(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) payload: decodePayload<JwtPayload<Payload>>(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
} }
} }

View File

@@ -1,5 +1,7 @@
export type KeyUsages = "sign" | "verify"
export function bytesToByteString(bytes: Uint8Array): string { export function bytesToByteString(bytes: Uint8Array): string {
let byteStr = '' let byteStr = ""
for (let i = 0; i < bytes.byteLength; i++) { for (let i = 0; i < bytes.byteLength; i++) {
byteStr += String.fromCharCode(bytes[i]) byteStr += String.fromCharCode(bytes[i])
} }
@@ -18,12 +20,12 @@ export function arrayBufferToBase64String(arrayBuffer: ArrayBuffer): string {
return btoa(bytesToByteString(new Uint8Array(arrayBuffer))) return btoa(bytesToByteString(new Uint8Array(arrayBuffer)))
} }
export function base64StringToArrayBuffer(b64str: string): ArrayBuffer { export function base64StringToUint8Array(b64str: string): Uint8Array {
return byteStringToBytes(atob(b64str)).buffer return byteStringToBytes(atob(b64str))
} }
export function textToArrayBuffer(str: string): ArrayBuffer { export function textToUint8Array(str: string): Uint8Array {
return byteStringToBytes(decodeURI(encodeURIComponent(str))) return byteStringToBytes(str)
} }
export function arrayBufferToText(arrayBuffer: ArrayBuffer): string { export function arrayBufferToText(arrayBuffer: ArrayBuffer): string {
@@ -31,30 +33,30 @@ export function arrayBufferToText(arrayBuffer: ArrayBuffer): string {
} }
export function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string { export function arrayBufferToBase64Url(arrayBuffer: ArrayBuffer): string {
return arrayBufferToBase64String(arrayBuffer).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') return arrayBufferToBase64String(arrayBuffer).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
} }
export function base64UrlToArrayBuffer(b64url: string): ArrayBuffer { export function base64UrlToUint8Array(b64url: string): Uint8Array {
return base64StringToArrayBuffer(b64url.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')) return base64StringToUint8Array(b64url.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""))
} }
export function textToBase64Url(str: string): string { export function textToBase64Url(str: string): string {
const encoder = new TextEncoder(); const encoder = new TextEncoder()
const charCodes = encoder.encode(str); const charCodes = encoder.encode(str)
const binaryStr = String.fromCharCode(...charCodes); const binaryStr = String.fromCharCode(...charCodes)
return btoa(binaryStr).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
return btoa(binaryStr).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
} }
export function pemToBinary(pem: string): ArrayBuffer { export function pemToBinary(pem: string): Uint8Array {
return base64StringToArrayBuffer(pem.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, '')) return base64StringToUint8Array(pem.replace(/-+(BEGIN|END).*/g, "").replace(/\s/g, ""))
} }
type KeyUsages = 'sign' | 'verify';
export async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> { export async function importTextSecret(key: string, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, keyUsages) return await crypto.subtle.importKey("raw", textToUint8Array(key), algorithm, true, keyUsages)
} }
export async function importJwk(key: JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> { export async function importJwk(key: JsonWebKeyWithKid, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
return await crypto.subtle.importKey("jwk", key, algorithm, true, keyUsages) return await crypto.subtle.importKey("jwk", key, algorithm, true, keyUsages)
} }
@@ -66,29 +68,25 @@ export async function importPrivateKey(key: string, algorithm: SubtleCryptoImpor
return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, keyUsages) return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, keyUsages)
} }
export async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> { export async function importKey(key: string | JsonWebKeyWithKid, algorithm: SubtleCryptoImportKeyAlgorithm, keyUsages: KeyUsages[]): Promise<CryptoKey> {
if (typeof key === 'object') if (typeof key === "object")
return importJwk(key, algorithm, keyUsages) 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, keyUsages) return importPublicKey(key, algorithm, keyUsages)
if (key.includes('PRIVATE')) if (key.includes("PRIVATE"))
return importPrivateKey(key, algorithm, keyUsages) return importPrivateKey(key, algorithm, keyUsages)
return importTextSecret(key, algorithm, keyUsages) return importTextSecret(key, algorithm, keyUsages)
} }
export function decodePayload<T = any>(raw: string): T | undefined { export function decodePayload<T = any>(raw: string): T {
try {
const bytes = Array.from(atob(raw), char => char.charCodeAt(0)); const bytes = Array.from(atob(raw), char => char.charCodeAt(0));
const decodedString = new TextDecoder('utf-8').decode(new Uint8Array(bytes)); const decodedString = new TextDecoder("utf-8").decode(new Uint8Array(bytes));
return JSON.parse(decodedString); return JSON.parse(decodedString);
} catch {
return
}
} }

107
tests/algorithms.spec.ts Normal file
View File

@@ -0,0 +1,107 @@
import { describe, expect, test } from 'vitest'
import jwt, { JwtAlgorithm } from '../src/index'
type Dataset = {
public?: string
private?: string
token: string
}
type Data = {
[key in JwtAlgorithm]: Dataset
}
type Payload = {
sub: string
name: string
emoji: string
}
const data: Data = {
'none': {
token: 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ'
},
'ES256': {
public: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2\nOF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r\n1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ri9u3ZJpyoHf1_i3KpVE5gMggyU3VMYPeEVktAsG1kGLOxFNJBXydQls3WFBaXXH2-sN74IMe-nDcM7NoJ6GMQ'
},
'ES384': {
public: '-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+\nPk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii\n1D3jaW6pmGVJFhodzC31cy5sfOYotrzF\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/p\nE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZz\nMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw\n8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.6nmlvcCpsfENb6ssgX3rJ2XSjpFSXD1RPS1CK0iDZFdi6I6Gnmpi456RSW6-0XSSgq2E2XBcWCSYE6TeI63jOGZJTQ6-65g4sndbzBPqYPWbLny00NQ4MQgQXVu6tRzg'
},
'ES512': {
public: '-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ\nPDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47\n6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM\nAl8G7CqwoJOsW7Kddns=\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga\n9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf\nZ6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN\nv3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear\njMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12\new==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.AbRwaCPJM2X3XjE2kInClHVGJNIcpL5C4a1ZrgwEM05RTryyNbazWRFbXAEDtHm8crvXpqw3a8JQwYDwvMyoOr4jAJ4RbhJitLgCGEzhKjvNy4xGrg3dV8gGEShFowgDfVz0KqHOX_Bc_DbyL-gtZPdfGTwT2upLkJE-lj47RStPDh0b'
},
'HS256': {
public: 'secret',
private: 'secret',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'
},
'HS384': {
public: 'secret',
private: 'secret',
token: 'eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hO2sthNQUSfvI9ylUdMKDxcrm8jB3KL6Rtkd3FOskL-jVqYh2CK1es8FKCQO8_tW'
},
'HS512': {
public: 'secret',
private: 'secret',
token: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wUVS6tazE2N98_J4SH_djkEe1igXPu0qILAvVXCiO6O20gdf5vZ2sYFWX3c-Hy6L4TD47b3DSAAO9XjSqpJfag'
},
'RS256': {
public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\nMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\nNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\nqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\np2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\nZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\nVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\nlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\nsJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\nmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\ndgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\nta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\nDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\nN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\nt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\nAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\nDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\nxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\nmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\net6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\nVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\nTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\ndn/RsYEONbwQSjIfMPkvxF+8HQ==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Eci61G6w4zh_u9oOCk_v1M_sKcgk0svOmW4ZsL-rt4ojGUH2QY110bQTYNwbEVlowW7phCg7vluX_MCKVwJkxJT6tMk2Ij3Plad96Jf2G2mMsKbxkC-prvjvQkBFYWrYnKWClPBRCyIcG0dVfBvqZ8Mro3t5bX59IKwQ3WZ7AtGBYz5BSiBlrKkp6J1UmP_bFV3eEzIHEFgzRa3pbr4ol4TK6SnAoF88rLr2NhEz9vpdHglUMlOBQiqcZwqrI-Z4XDyDzvnrpujIToiepq9bCimPgVkP54VoZzy-mMSGbthYpLqsL_4MQXaI1Uf_wKFAUuAtzVn4-ebgsKOpvKNzVA'
},
'RS384': {
public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\nMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\nNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\nqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\np2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\nZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\nVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\nlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\nsJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\nmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\ndgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\nta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\nDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\nN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\nt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\nAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\nDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\nxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\nmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\net6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\nVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\nTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\ndn/RsYEONbwQSjIfMPkvxF+8HQ==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.oPvWzaCp8xUpt5mHhPSn0qLsZfCFj0NVmb4mz4dFQPCCMj-F5zVn9e3zZoj0lIXWM8rxB69QHC3Er47mtDt3BKgysTL3BvvV89kD6UjLoUcAI3lwj0mi7acLoE27i1_TnIBqWNRPAsdvTDawNE0_4lvI5bxEWQCqisJwxCoMDIeJsmDzfyApgU_SAFSVULxXwU2VewaxdQB-41OZdWwUEAxh81iB6DFWrqd2CaJkUYoWjgYpeWsyeC2m_-ECGrHGEz1nKTm9c7BaPxurz7fHD7RJd9Wpx-mKDVsfspO9quWb_OLeGGbxTtAomMvjQjut56kx2fqTleDnNDh_0GE88w'
},
'RS512': {
public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\nMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\nNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\nqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\np2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\nZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\nVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\nlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\nsJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\nmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\ndgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\nta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\nDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\nN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\nt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\nAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\nDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\nxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\nmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\net6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\nVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\nTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\ndn/RsYEONbwQSjIfMPkvxF+8HQ==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.kEZmDAnHHU_0bcGXMd5LA7vF87yQgXNaioPHP4lU4O3JJYuZ54fJdv3HT58xk-MFEDuWro_5fvNIp2VM-PlkZvYWrhQkJ-c-seoSa3ANq_PciC3bGfzYHEdjAE71GrAMI4FlcAGsq3ChkOnCTFqjWDmVwaRYCgMsFQ-U5cjvFhndFMizrkRljTF4v5oFdWytV_J-UafPtNdQXcGND1M74DqObnTHhZHg8aDfNzZcvnIeKcDVGUlUEL5ia1kPMrVhCtOAOJmEU8ivCdWWzt-jMQBf7cZeoCzDKHG72ysTTCfRoBVc1_SrQTHcHDiiBeW9nCazMLkltyP5NeawR_RNlg'
}
}
const payload: Payload = {
sub: "1234567890",
name: "John Doe",
emoji: "😎"
}
const jwtRegex = /^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)?$/
describe("Internal", () => {
test.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', async (algorithm, data) => {
const token = await jwt.sign<Payload>(payload, data.private, algorithm)
expect(token).toMatch(jwtRegex)
const decoded = jwt.decode<Payload>(token)
expect(decoded.payload).toMatchObject(payload)
const verified = await jwt.verify(token, data.public, algorithm)
expect(verified).toBeTruthy()
})
})
describe("External", async () => {
test.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', async (algorithm, data) => {
const decoded = jwt.decode<Payload>(data.token)
expect({
sub: decoded.payload?.sub,
name: decoded.payload?.name,
}).toMatchObject({
sub: payload.sub,
name: payload.name
})
const verified = await jwt.verify(data.token, data.public, algorithm)
expect(verified).toBeTruthy()
})
})

View File

@@ -1,148 +1,39 @@
import { describe, expect, test } from 'vitest' import { describe, expect, test } from "vitest"
import jwt, { JwtAlgorithm } from '../src/index' import jwt from "../src/index"
type Dataset = { describe("Verify", async () => {
public: string const secret = "super-secret"
private: string
token: string
}
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-----',
private: '-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2\nOF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r\n1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ri9u3ZJpyoHf1_i3KpVE5gMggyU3VMYPeEVktAsG1kGLOxFNJBXydQls3WFBaXXH2-sN74IMe-nDcM7NoJ6GMQ'
},
'ES384': {
public: '-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+\nPk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii\n1D3jaW6pmGVJFhodzC31cy5sfOYotrzF\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/p\nE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZz\nMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw\n8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.6nmlvcCpsfENb6ssgX3rJ2XSjpFSXD1RPS1CK0iDZFdi6I6Gnmpi456RSW6-0XSSgq2E2XBcWCSYE6TeI63jOGZJTQ6-65g4sndbzBPqYPWbLny00NQ4MQgQXVu6tRzg'
},
'ES512': {
public: '-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ\nPDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47\n6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM\nAl8G7CqwoJOsW7Kddns=\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga\n9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf\nZ6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN\nv3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear\njMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12\new==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.AbRwaCPJM2X3XjE2kInClHVGJNIcpL5C4a1ZrgwEM05RTryyNbazWRFbXAEDtHm8crvXpqw3a8JQwYDwvMyoOr4jAJ4RbhJitLgCGEzhKjvNy4xGrg3dV8gGEShFowgDfVz0KqHOX_Bc_DbyL-gtZPdfGTwT2upLkJE-lj47RStPDh0b'
},
'HS256': {
public: 'secret',
private: 'secret',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'
},
'HS384': {
public: 'secret',
private: 'secret',
token: 'eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hO2sthNQUSfvI9ylUdMKDxcrm8jB3KL6Rtkd3FOskL-jVqYh2CK1es8FKCQO8_tW'
},
'HS512': {
public: 'secret',
private: 'secret',
token: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wUVS6tazE2N98_J4SH_djkEe1igXPu0qILAvVXCiO6O20gdf5vZ2sYFWX3c-Hy6L4TD47b3DSAAO9XjSqpJfag'
},
'RS256': {
public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\nMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\nNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\nqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\np2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\nZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\nVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\nlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\nsJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\nmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\ndgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\nta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\nDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\nN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\nt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\nAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\nDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\nxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\nmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\net6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\nVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\nTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\ndn/RsYEONbwQSjIfMPkvxF+8HQ==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Eci61G6w4zh_u9oOCk_v1M_sKcgk0svOmW4ZsL-rt4ojGUH2QY110bQTYNwbEVlowW7phCg7vluX_MCKVwJkxJT6tMk2Ij3Plad96Jf2G2mMsKbxkC-prvjvQkBFYWrYnKWClPBRCyIcG0dVfBvqZ8Mro3t5bX59IKwQ3WZ7AtGBYz5BSiBlrKkp6J1UmP_bFV3eEzIHEFgzRa3pbr4ol4TK6SnAoF88rLr2NhEz9vpdHglUMlOBQiqcZwqrI-Z4XDyDzvnrpujIToiepq9bCimPgVkP54VoZzy-mMSGbthYpLqsL_4MQXaI1Uf_wKFAUuAtzVn4-ebgsKOpvKNzVA'
},
'RS384': {
public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\nMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\nNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\nqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\np2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\nZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\nVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\nlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\nsJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\nmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\ndgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\nta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\nDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\nN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\nt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\nAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\nDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\nxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\nmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\net6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\nVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\nTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\ndn/RsYEONbwQSjIfMPkvxF+8HQ==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.oPvWzaCp8xUpt5mHhPSn0qLsZfCFj0NVmb4mz4dFQPCCMj-F5zVn9e3zZoj0lIXWM8rxB69QHC3Er47mtDt3BKgysTL3BvvV89kD6UjLoUcAI3lwj0mi7acLoE27i1_TnIBqWNRPAsdvTDawNE0_4lvI5bxEWQCqisJwxCoMDIeJsmDzfyApgU_SAFSVULxXwU2VewaxdQB-41OZdWwUEAxh81iB6DFWrqd2CaJkUYoWjgYpeWsyeC2m_-ECGrHGEz1nKTm9c7BaPxurz7fHD7RJd9Wpx-mKDVsfspO9quWb_OLeGGbxTtAomMvjQjut56kx2fqTleDnNDh_0GE88w'
},
'RS512': {
public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----',
private: '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\nMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\nNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\nqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\np2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\nZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\nVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\nlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\nsJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\nmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\ndgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\nta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\nDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\nN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\nt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\nAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\nDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\nxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\nmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\net6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\nVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\nTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\ndn/RsYEONbwQSjIfMPkvxF+8HQ==\n-----END PRIVATE KEY-----',
token: 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.kEZmDAnHHU_0bcGXMd5LA7vF87yQgXNaioPHP4lU4O3JJYuZ54fJdv3HT58xk-MFEDuWro_5fvNIp2VM-PlkZvYWrhQkJ-c-seoSa3ANq_PciC3bGfzYHEdjAE71GrAMI4FlcAGsq3ChkOnCTFqjWDmVwaRYCgMsFQ-U5cjvFhndFMizrkRljTF4v5oFdWytV_J-UafPtNdQXcGND1M74DqObnTHhZHg8aDfNzZcvnIeKcDVGUlUEL5ia1kPMrVhCtOAOJmEU8ivCdWWzt-jMQBf7cZeoCzDKHG72ysTTCfRoBVc1_SrQTHcHDiiBeW9nCazMLkltyP5NeawR_RNlg'
}
}
const payload: Payload = {
sub: "1234567890",
name: "John Doe",
}
const unicodePayload: Payload = {
sub: "1234567890",
name: "John Doe 😎",
}
describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorithm, data) => {
let token = ''
test('verify external', async () => {
const verified = await jwt.verify(data.token, data.public, algorithm)
expect(verified).toBeTruthy()
})
test('decode external', async () => {
const decoded = jwt.decode<Payload>(data.token)
expect({
sub: payload.sub,
name: payload.name
}).toMatchObject({
sub: decoded.payload?.sub,
name: payload.name
})
})
test('sign internal', async () => {
token = await jwt.sign<Payload>(payload, data.private, algorithm)
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({
sub: payload.sub,
name: payload.name
}).toMatchObject({
sub: decoded.payload?.sub,
name: payload.name
})
})
test('verify internal', async () => {
const verified = await jwt.verify(token, data.public, algorithm)
expect(verified).toBeTruthy()
})
})
describe('Verify', async () => {
const secret = 'super-secret'
const now = Math.floor(Date.now() / 1000) const now = Math.floor(Date.now() / 1000)
const off = 30 // 30 seconds const offset = 30 // 30 seconds
const nbf = now + off // Not valid before 30 seconds from now
const exp = now - off // Expired 30 seconds ago
const notYetValidToken = await jwt.sign({ sub: 'me', nbf }, secret) const validToken = await jwt.sign({ sub: "me", nbf: now - offset }, secret)
const expiredToken = await jwt.sign({ sub: 'me', exp }, secret) const notYetExpired = await jwt.sign({ sub: "me", exp: now + offset }, secret)
test('Not yet valid', () => { const notYetValidToken = await jwt.sign({ sub: "me", nbf: now + offset }, secret)
expect(jwt.verify(notYetValidToken, secret, { throwError: true })).rejects.toThrowError('NOT_YET_VALID') const expiredToken = await jwt.sign({ sub: "me", exp: now - offset }, secret)
test("Valid", async () => {
await expect(jwt.verify(validToken, secret, { throwError: true })).resolves.toBeTruthy()
}) })
test('Expired', () => { test("Not yet expired", async () => {
console.log({ exp, now: Math.floor(Date.now() / 1000) }) await expect(jwt.verify(notYetExpired, secret, { throwError: true })).resolves.toBeTruthy()
expect(jwt.verify(expiredToken, secret, { throwError: true })).rejects.toThrowError('EXPIRED')
}) })
test('Clock offset', () => { test("Not yet valid", async () => {
expect(jwt.verify(notYetValidToken, secret, { clockTolerance: off, throwError: true })).resolves.toBe(true) await expect(jwt.verify(notYetValidToken, secret, { throwError: true })).rejects.toThrowError("NOT_YET_VALID")
expect(jwt.verify(expiredToken, secret, { clockTolerance: off, throwError: true })).resolves.toBe(true) })
test("Expired", async () => {
await expect(jwt.verify(expiredToken, secret, { throwError: true })).rejects.toThrowError("EXPIRED")
})
test("Clock offset", async () => {
await expect(jwt.verify(notYetValidToken, secret, { clockTolerance: offset, throwError: true })).resolves.toBeTruthy()
await expect(jwt.verify(expiredToken, secret, { clockTolerance: offset, throwError: true })).resolves.toBeTruthy()
await expect(jwt.verify(notYetValidToken, secret, { clockTolerance: offset - 1, throwError: true })).rejects.toThrowError("NOT_YET_VALID")
await expect(jwt.verify(expiredToken, secret, { clockTolerance: offset - 1, throwError: true })).rejects.toThrowError("EXPIRED")
}) })
}) })

View File

@@ -1,81 +1,81 @@
import { describe, expect, test } from 'vitest' import { describe, expect, test } from "vitest"
import { import {
bytesToByteString, bytesToByteString,
byteStringToBytes, byteStringToBytes,
arrayBufferToBase64String, arrayBufferToBase64String,
base64StringToArrayBuffer, base64StringToUint8Array,
textToArrayBuffer, textToUint8Array,
arrayBufferToText, arrayBufferToText,
arrayBufferToBase64Url, arrayBufferToBase64Url,
base64UrlToArrayBuffer, base64UrlToUint8Array,
textToBase64Url, textToBase64Url,
pemToBinary, pemToBinary,
importTextSecret importTextSecret
} from '../src/utils' } from "../src/utils"
describe('Converters', () => { describe("Converters", () => {
const testString = 'cloudflare-worker-jwt' 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 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 testUint8Array = new Uint8Array(testByteArray)
const testBase64String = 'Y2xvdWRmbGFyZS13b3JrZXItand0' const testBase64String = "Y2xvdWRmbGFyZS13b3JrZXItand0"
const testArrayBuffer = testUint8Array.buffer const testArrayBuffer = testUint8Array
test('bytesToByteString', () => { test("bytesToByteString", () => {
expect(bytesToByteString(testUint8Array)).toStrictEqual(testString) expect(bytesToByteString(testUint8Array)).toStrictEqual(testString)
}) })
test('byteStringToBytes', () => { test("byteStringToBytes", () => {
expect(byteStringToBytes(testString)).toStrictEqual(testUint8Array) expect(byteStringToBytes(testString)).toStrictEqual(testUint8Array)
}) })
test('arrayBufferToBase64String', () => { test("arrayBufferToBase64String", () => {
expect(arrayBufferToBase64String(testArrayBuffer)).toStrictEqual(testBase64String) expect(arrayBufferToBase64String(testArrayBuffer)).toStrictEqual(testBase64String)
}) })
test('base64StringToArrayBuffer', () => { test("base64StringToArrayBuffer", () => {
expect(base64StringToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer) expect(base64StringToUint8Array(testBase64String)).toStrictEqual(testArrayBuffer)
}) })
test('textToArrayBuffer', () => { test("textToArrayBuffer", () => {
expect(textToArrayBuffer(testString)).toStrictEqual(testUint8Array) expect(textToUint8Array(testString)).toStrictEqual(testUint8Array)
}) })
test('arrayBufferToText', () => { test("arrayBufferToText", () => {
expect(arrayBufferToText(testArrayBuffer)).toStrictEqual(testString) expect(arrayBufferToText(testArrayBuffer)).toStrictEqual(testString)
}) })
test('arrayBufferToBase64Url', () => { test("arrayBufferToBase64Url", () => {
expect(arrayBufferToBase64Url(testArrayBuffer)).toStrictEqual(testBase64String) expect(arrayBufferToBase64Url(testArrayBuffer)).toStrictEqual(testBase64String)
}) })
test('base64UrlToArrayBuffer', () => { test("base64UrlToArrayBuffer", () => {
expect(base64UrlToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer) expect(base64UrlToUint8Array(testBase64String)).toStrictEqual(testArrayBuffer)
}) })
test('textToBase64Url', () => { test("textToBase64Url", () => {
expect(textToBase64Url(testString)).toStrictEqual(testBase64String) expect(textToBase64Url(testString)).toStrictEqual(testBase64String)
}) })
test('pemToBinary', () => { test("pemToBinary", () => {
expect(pemToBinary(`-----BEGIN PUBLIC KEY-----\n${testBase64String}\n-----END PUBLIC KEY-----`)).toStrictEqual(testArrayBuffer) expect(pemToBinary(`-----BEGIN PUBLIC KEY-----\n${testBase64String}\n-----END PUBLIC KEY-----`)).toStrictEqual(testArrayBuffer)
}) })
}) })
describe('Imports', () => { describe("Imports", () => {
test('importTextSecret', async () => { test("importTextSecret", async () => {
const testKey = 'cloudflare-worker-jwt' const testKey = "cloudflare-worker-jwt"
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, ['verify', 'sign'])).toMatchObject(testCryptoKey) await expect(importTextSecret(testKey, testAlgorithm, ["verify", "sign"])).resolves.toMatchObject(testCryptoKey)
}) })
//test('importJwk', async () => {}) test.todo("importJwk")
//test('importPublicKey', async () => {}) test.todo("importPublicKey")
//test('importPrivateKey', async () => {}) test.todo("importPrivateKey")
//test('importKey', async () => {}) test.todo("importKey")
}) })
//describe('Payload', () => { describe.todo("Payload", () => {
// test('decodePayload', () => {}) test.todo("decodePayload")
//}) })