1
0

Compare commits

..

55 Commits

Author SHA1 Message Date
21ec1b6f2a Update to v1.4.3 2022-06-22 12:37:08 +02:00
Toby Schneider
97df6e7f81 Merge pull request #17 from IMZihad21/main
Destructure payload from decode function properly
2022-06-22 12:11:09 +02:00
ZèD
7198501a40 Destructure payload from decode function properly
The decode function returns an object containing a header and payload properties. Assigning the whole object to payload fails nbf and exp checks on verify JWT as those properties not found in decode return object directly. Instead, destructure payload property from decode return data that contains those values and check them correctly.

Signed-off-by: ZèD <imzihad@gmail.com>
2022-06-22 12:06:50 +06:00
cb6209b1b6 Docs fix 2022-06-04 22:29:46 +02:00
8f4e5e3199 Docs fix 2022-06-04 22:29:30 +02:00
594cdd6c05 Docs fix 2022-06-04 17:43:48 +02:00
f85abf30a8 Update docs 2022-06-04 17:43:11 +02:00
095b1d43f3 Update docs 2022-06-04 17:42:33 +02:00
0bc128fec1 Update docs 2022-06-04 17:40:47 +02:00
f846695242 Update docs 2022-06-04 17:29:50 +02:00
695e1c0dfe .decode() syntax change to support headers 2022-06-04 17:23:53 +02:00
64da4c625f .verify() bugfix 2022-06-04 14:53:59 +02:00
e4038ae0a7 Just to make sure 2022-06-04 14:40:06 +02:00
44329617de Fix .verify() and add testing 2022-06-04 14:18:06 +02:00
Toby Schneider
e7964b63c2 Merge pull request #9 from alaister/main
Add support for JWT header
2022-06-01 20:10:51 +02:00
b733a0650d Update readme 2022-06-01 15:11:57 +02:00
43879de15e Implement throwError option for .verify(). 2022-06-01 15:09:25 +02:00
0c8f476751 Update to v1.1.7 2022-04-11 02:20:54 +02:00
3c5d178fec Fix timestamp check 2022-04-11 02:20:14 +02:00
Alaister Young
f12cafd9d0 Add support for JWT header 2022-03-01 10:21:32 +11:00
e0219ff21f Update to v1.1.6 2022-02-27 16:15:58 +01:00
Toby Schneider
bc7fa845ed Merge pull request #7 from plesiv/add-rsa-algorithm
Add support for RSA algorithm
2022-02-27 15:56:13 +01:00
Toby Schneider
5ee043e597 Merge pull request #8 from workeffortwaste/fix-constructor-error
Fix constructor error
2022-02-27 15:55:38 +01:00
Chris Johnson
9c52217ca2 Fix constructor error 2022-02-24 09:17:40 +00:00
Zoran Plesivcak
5160cfa416 Add support for RSA algorithm 2022-02-13 23:58:16 +00:00
430fa0eb84 Update to 1.1.5 2021-11-02 13:58:16 +01:00
4d4cf316d0 Change error message 2021-11-02 13:56:12 +01:00
e244ff0618 Added more algorithms, keyid and did some cleanup 2021-10-27 01:22:01 +02:00
Toby Schneider
13943f64d7 Create FUNDING.yml 2021-10-26 23:54:37 +02:00
8952a12e4f Updated example code 2021-09-23 22:51:14 +02:00
af3ea4cf8d Rearranged readme 2021-05-26 19:16:25 +02:00
d99e361d65 Bump version 2021-05-26 02:19:23 +02:00
79f030a35a Added JSDoc 2021-05-26 02:19:04 +02:00
5fd472c33d Renamed workflow 2021-05-25 22:44:42 +02:00
54ad90f6fd Updated tslint 2021-05-25 22:44:10 +02:00
8d9f70ca36 Added nbf and exp support 2021-05-23 22:41:35 +02:00
e34e1aedab Cleaned up 2021-05-23 18:33:04 +02:00
d31254de78 Bumped version to 1.0.8 2021-05-23 18:18:04 +02:00
8fadfab966 Rearranged readme 2021-05-23 18:17:12 +02:00
187bd60b57 Updated readme 2021-05-23 18:16:04 +02:00
f905176573 Updated readme 2021-05-23 18:03:28 +02:00
39c3fb790d Updated workflow 2021-05-23 17:48:49 +02:00
5d8345996a Added linting 2021-05-23 17:47:22 +02:00
eebc0e988a Bumped version 2021-05-01 16:24:26 +02:00
bf90a7c855 Added typescript support 2021-05-01 16:21:47 +02:00
6e0ce1cf82 Renamed alg to algorithm 2021-05-01 16:21:27 +02:00
4ed24b18c5 Removed unused variable 2021-05-01 15:54:49 +02:00
d12809e15d Fixed typo 2021-02-08 17:01:30 +01:00
7a093c2c85 Fixed typo 2021-02-08 17:00:57 +01:00
14a9631b8b Added LICENSE 2021-02-08 16:37:41 +01:00
c247675832 Updated license 2021-02-05 13:52:41 +01:00
0d3c7d621a Bump version to test github action 2021-02-04 17:49:09 +01:00
62479197f5 Added workflow to publish package 2021-02-04 17:48:13 +01:00
0a3a9b58d1 Updated readme 2021-02-04 12:42:47 +01:00
959a7c9490 Updated readme 2021-02-04 12:42:33 +01:00
10 changed files with 9164 additions and 124 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: tsndr

30
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Publish
on:
release:
types: [created]
jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
publish-gpr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://npm.pkg.github.com/
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

144
.gitignore vendored Normal file
View File

@@ -0,0 +1,144 @@
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# node.js
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Tobias Schneider
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

190
README.md
View File

@@ -2,65 +2,149 @@
A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers. A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers.
## Contents ## Contents
- [Usage](#usage)
- [Install](#install) - [Install](#install)
- [Examples](#examples)
## Usage - [Usage](#usage)
- [Sign](#sign)
### Simple Example - [Verify](#verify)
- [Decode](#decode)
```javascript
const jwt = require('@tsndr/cloudflare-worker-jwt')
// Creating a token
const token = jwt.sign({ name: 'John Doe', email: 'john.doe@gmail.com' }, 'secret')
// Verifing token
const isValid = jwt.verify(token, secret)
// Decoding token
const payload = jwt.decode(token)
```
### `jwt.sign(payload, secret, [algorithm])`
Signs a payload and returns the token.
`payload`
Can be an object, buffer or a string.
`secret`
A string which is used to sign the payload.
`algorithm` (optional, default: `HS256`)
The algorithm used to sign the payload, possible values: `HS256`(default) or `HS512`
### `jwt.verify(token, secret, [algorithm])`
Verifies the integrity of the token and returns a boolean value.
`token`
The token string generated by `jwt.sign()`.
`secret`
A string which is used to sign the payload.
`algorithm` (optional, default: `HS256`)
The algorithm used to sign the payload, possible values: `HS256`(default) or `HS512`
### `jwt.decode(token)`
Returns the payload without verifying the integrity of the token.
`token`
The token string generated by `jwt.sign()`.
## Install ## Install
``` ```
npm i @tsndr/cloudflare-worker-jwt npm i -D @tsndr/cloudflare-worker-jwt
``` ```
## Examples
### Basic Example
```javascript
async () => {
const jwt = require('@tsndr/cloudflare-worker-jwt')
// Creating a token
const token = await jwt.sign({ name: 'John Doe', email: 'john.doe@gmail.com' }, 'secret')
// Verifing token
const isValid = await jwt.verify(token, 'secret')
// Check for validity
if (!isValid)
return
// Decoding token
const { payload } = jwt.decode(token)
}
```
### Restrict Timeframe
```javascript
async () => {
const jwt = require('@tsndr/cloudflare-worker-jwt')
// Creating a token
const token = await jwt.sign({
name: 'John Doe',
email: 'john.doe@gmail.com',
nbf: Math.floor(Date.now() / 1000) + (60 * 60), // Not before: Now + 1h
exp: Math.floor(Date.now() / 1000) + (2 * (60 * 60)) // Expires: Now + 2h
}, 'secret')
// Verifing token
const isValid = await jwt.verify(token, 'secret') // false
// Check for validity
if (!isValid)
return
// Decoding token
const { payload } = jwt.decode(token) // { name: 'John Doe', email: 'john.doe@gmail.com', ... }
}
```
## Usage
- [Sign](#sign)
- [Verify](#verify)
- [Decode](#decode)
<hr>
### Sign
#### `jwt.sign(payload, secret, [options])`
Signs a payload and returns the token.
#### Arguments
Argument | Type | Satus | 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.
`secret` | `string` | required | - | A string which is used to sign the payload.
`options` | `object` | optional | `{ algorithm: 'HS256' }` | The options object supporting `algorithm` and `keyid`. (See [Available Algorithms](#available-algorithms))
#### `return`
Returns token as a `string`.
<hr>
### Verify
#### `jwt.verify(token, secret, [options])`
Verifies the integrity of the token and returns a boolean value.
Argument | Type | Satus | Default | Description
----------- | -------- | -------- | ------- | -----------
`token` | `string` | required | - | The token string generated by `jwt.sign()`.
`secret` | `string` | required | - | The string which was used to sign the payload.
`options` | `object` | optional | `{ algorithm: 'HS256', throwError: false }` | The options object supporting `algorithm` and `throwError`. (See [Available Algorithms](#available-algorithms))
#### `throws`
If `options.throwError` is `true` and the token is invalid, an error will be thrown.
#### `return`
Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
<hr>
### Decode
#### `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 | Satus | Default | Description
----------- | -------- | -------- | ------- | -----------
`token` | `string` | required | - | The token string generated by `jwt.sign()`.
#### `return`
Returns an `object` containing `header` and `payload`:
```javascript
{
header: {
alg: 'HS256',
typ: 'JWT'
},
payload: {
name: 'John Doe',
email: 'john.doe@gmail.com'
}
}
```
### Available Algorithms
- ES256
- ES384
- ES512
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512

60
index.d.ts vendored Normal file
View File

@@ -0,0 +1,60 @@
/**
* JWT
*
* @class
* @constructor
* @public
*/
declare class JWT {
/**
* Signs a payload and returns the token
*
* @param {object} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
* @param {string} secret A string which is used to sign the payload.
* @param {JWTSignOptions | JWTAlgorithm} options The options object or the algorithm.
* @throws {Error} If there's a validation issue.
* @returns {Promise<string>} Returns token as a `string`.
*/
sign(payload: object, secret: string, options?: JWTSignOptions | JWTAlgorithm): Promise<string>
/**
* Verifies the integrity of the token and returns a boolean value.
*
* @param {string} token The token string generated by `jwt.sign()`.
* @param {string} secret The string which was used to sign the payload.
* @param {JWTVerifyOptions | JWTAlgorithm} options The options object or the algorithm.
* @throws {Error | string} Throws an error `string` if the token is invalid or an `Error-Object` if there's a validation issue.
* @returns {Promise<boolean>} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
*/
verify(token: string, secret: string, options?: JWTVerifyOptions | JWTAlgorithm): Promise<boolean>
/**
* Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure!
*
* @param {string} token The token string generated by `jwt.sign()`.
* @returns {JWTDecodeReturn} Returns an `object` containing `header` and `payload`.
*/
decode(token: string): JWTDecodeReturn
}
declare const _exports: JWT
type JWTAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'
type JWTSignOptions = {
algorithm?: JWTAlgorithm,
keyid?: string
header?: object
}
type JWTVerifyOptions = {
algorithm?: JWTAlgorithm
throwError?: boolean
}
type JWTDecodeReturn = {
header: object,
payload: object
}
export = _exports

158
index.js
View File

@@ -9,94 +9,118 @@ class Base64URL {
class JWT { class JWT {
constructor() { constructor() {
if (!crypto || !crypto.subtle) if (typeof crypto === 'undefined' || !crypto.subtle)
throw new Error('Crypto not supported!') throw new Error('Crypto not supported!')
this.algorithms = { this.algorithms = {
HS256: { ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } },
name: 'HMAC', ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } },
hash: { ES512: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } },
name: 'SHA-256' HS256: { name: 'HMAC', hash: { name: 'SHA-256' } },
} HS384: { name: 'HMAC', hash: { name: 'SHA-384' } },
}, HS512: { name: 'HMAC', hash: { name: 'SHA-512' } },
HS512: { RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
name: 'HMAC', RS384: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } },
hash: { RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } },
name: 'SHA-512'
} }
} }
_utf8ToUint8Array(str) {
return Base64URL.parse(btoa(unescape(encodeURIComponent(str))))
} }
_str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
} }
utf8ToUint8Array(str) { return buf;
const chars = []
str = btoa(unescape(encodeURIComponent(str)))
return Base64URL.parse(str)
} }
async sign(payload, secret, alg = 'HS256') { _decodePayload(raw) {
if (payload === null || typeof payload !== 'object') switch (raw.length % 4) {
throw new Error('payload must be an object')
if (typeof secret !== 'string')
throw new Error('secret must be a string')
if (typeof alg !== 'string')
throw new Error('alg must be a string')
const importAlgorithm = this.algorithms[alg]
if (!importAlgorithm)
throw new Error('algorithm not found')
const payloadAsJSON = JSON.stringify(payload)
const partialToken = `${Base64URL.stringify(this.utf8ToUint8Array(JSON.stringify({ alg, typ: 'JWT' })))}.${Base64URL.stringify(this.utf8ToUint8Array(payloadAsJSON))}`
const key = await crypto.subtle.importKey('raw', this.utf8ToUint8Array(secret), importAlgorithm, false, ['sign'])
const characters = payloadAsJSON.split('')
const it = this.utf8ToUint8Array(payloadAsJSON).entries()
let i = 0
const result = []
let current
while (!(current = it.next()).done) {
result.push([current.value[1], characters[i]])
i++
}
const signature = await crypto.subtle.sign(importAlgorithm.name, key, this.utf8ToUint8Array(partialToken))
return `${partialToken}.${Base64URL.stringify(new Uint8Array(signature))}`
}
async verify(token, secret, alg = 'HS256') {
if (typeof token !== 'string')
throw new Error('token must be a string')
if (typeof secret !== 'string')
throw new Error('secret must be a string')
if (typeof alg !== 'string')
throw new Error('alg must be a string')
const tokenParts = token.split('.')
if (tokenParts.length !== 3)
throw new Error('token must have 3 parts')
const importAlgorithm = this.algorithms[alg]
if (!importAlgorithm)
throw new Error('algorithm not found')
const keyData = this.utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey('raw', keyData, importAlgorithm, false, ['sign'])
const partialToken = tokenParts.slice(0, 2).join('.')
const signaturePart = tokenParts[2]
const messageAsUint8Array = this.utf8ToUint8Array(partialToken)
const res = await crypto.subtle.sign(importAlgorithm.name, key, messageAsUint8Array)
return Base64URL.stringify(new Uint8Array(res)) === signaturePart
}
decode(token) {
let output = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
switch (output.length % 4) {
case 0: case 0:
break break
case 2: case 2:
output += '==' raw += '=='
break break
case 3: case 3:
output += '=' raw += '='
break break
default: default:
throw new Error('Illegal base64url string!') throw new Error('Illegal base64url string!')
} }
try { try {
return JSON.parse(decodeURIComponent(escape(atob(output)))) return JSON.parse(decodeURIComponent(escape(atob(raw))))
} catch { } catch {
return null return null
} }
} }
async sign(payload, secret, options = { algorithm: 'HS256', header: { typ: 'JWT' } }) {
if (typeof options === 'string')
options = { algorithm: options, header: { typ: 'JWT' } }
if (payload === null || typeof payload !== 'object')
throw new Error('payload must be an object')
if (typeof secret !== 'string')
throw new Error('secret must be a string')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const importAlgorithm = this.algorithms[options.algorithm]
if (!importAlgorithm)
throw new Error('algorithm not found')
payload.iat = Math.floor(Date.now() / 1000)
const payloadAsJSON = JSON.stringify(payload)
const partialToken = `${Base64URL.stringify(this._utf8ToUint8Array(JSON.stringify({ ...options.header, alg: options.algorithm, kid: options.keyid })))}.${Base64URL.stringify(this._utf8ToUint8Array(payloadAsJSON))}`
let keyFormat = 'raw'
let keyData
if (secret.startsWith('-----BEGIN')) {
keyFormat = 'pkcs8'
keyData = this._str2ab(atob(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, '')))
} else
keyData = this._utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, importAlgorithm, false, ['sign'])
const signature = await crypto.subtle.sign(importAlgorithm, key, this._utf8ToUint8Array(partialToken))
return `${partialToken}.${Base64URL.stringify(new Uint8Array(signature))}`
}
async verify(token, secret, options = { algorithm: 'HS256', throwError: false }) {
if (typeof options === 'string')
options = { algorithm: options }
if (typeof token !== 'string')
throw new Error('token must be a string')
if (typeof secret !== 'string')
throw new Error('secret must be a string')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const tokenParts = token.split('.')
if (tokenParts.length !== 3)
throw new Error('token must consist of 3 parts')
const importAlgorithm = this.algorithms[options.algorithm]
if (!importAlgorithm)
throw new Error('algorithm not found')
const { payload } = this.decode(token)
if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) {
if (options.throwError)
throw 'NOT_YET_VALID'
return false
}
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) {
if (options.throwError)
throw 'EXPIRED'
return false
}
let keyFormat = 'raw'
let keyData
if (secret.startsWith('-----BEGIN')) {
keyFormat = 'spki'
keyData = this._str2ab(atob(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, '')))
} else
keyData = this._utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, importAlgorithm, false, ['verify'])
return await crypto.subtle.verify(importAlgorithm, key, Base64URL.parse(tokenParts[2]), this._utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`))
}
decode(token) {
return {
header: this._decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')),
payload: this._decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
}
}
} }
module.exports = new JWT module.exports = new JWT

168
index.test.js Normal file
View File

@@ -0,0 +1,168 @@
const { subtle } = require('node:crypto').webcrypto
Object.defineProperty(global, 'crypto', {
value: { subtle }
})
const JWT = require('./index')
const oneDay = (60 * 60 * 24)
const now = Date.now() / 1000
const secrets = {}
// Keypairs
for (const algorithm of Object.keys(JWT.algorithms)) {
if (algorithm.startsWith('HS'))
secrets[algorithm] = 'secret'
else if (algorithm.startsWith('RS')) {
secrets[algorithm] = {
public: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----`,
private: `-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----`
}
} else if (algorithm.startsWith('ES')) {
if (algorithm === 'ES256') {
secrets[algorithm] = {
public: `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----`,
private: `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----`
}
} else if (algorithm === 'ES384') {
secrets[algorithm] = {
public: `-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+
Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii
1D3jaW6pmGVJFhodzC31cy5sfOYotrzF
-----END PUBLIC KEY-----`,
private: `-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/p
E9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZz
MIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw
8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=
-----END PRIVATE KEY-----`
}
} else if (algorithm === 'ES512') {
secrets[algorithm] = {
public: `-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
Al8G7CqwoJOsW7Kddns=
-----END PUBLIC KEY-----`,
private: `-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga
9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf
Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN
v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear
jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12
ew==
-----END PRIVATE KEY-----`
}
}
}
}
// Payload
const testPayload = {
sub: "1234567890",
name: "John Doe"
}
// Self test
test.each(Object.entries(secrets))(`Self test: %s`, async (algorithm, key) => {
let privateKey = key
let publicKey = key
if (typeof key === 'object') {
privateKey = key.private
publicKey = key.public
}
const token = await JWT.sign(testPayload, privateKey, { algorithm })
expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/)
const verified = await JWT.verify(token, publicKey, { algorithm })
expect(verified).toBeTruthy()
const { payload } = JWT.decode(token)
expect({
sub: payload.sub,
name: payload.name
}).toMatchObject({
sub: testPayload.sub,
name: testPayload.name
})
})
// External token test
const externalTokens = {
ES256: 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ri9u3ZJpyoHf1_i3KpVE5gMggyU3VMYPeEVktAsG1kGLOxFNJBXydQls3WFBaXXH2-sN74IMe-nDcM7NoJ6GMQ',
ES384: 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.6nmlvcCpsfENb6ssgX3rJ2XSjpFSXD1RPS1CK0iDZFdi6I6Gnmpi456RSW6-0XSSgq2E2XBcWCSYE6TeI63jOGZJTQ6-65g4sndbzBPqYPWbLny00NQ4MQgQXVu6tRzg',
ES512: 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.AbRwaCPJM2X3XjE2kInClHVGJNIcpL5C4a1ZrgwEM05RTryyNbazWRFbXAEDtHm8crvXpqw3a8JQwYDwvMyoOr4jAJ4RbhJitLgCGEzhKjvNy4xGrg3dV8gGEShFowgDfVz0KqHOX_Bc_DbyL-gtZPdfGTwT2upLkJE-lj47RStPDh0b',
HS256: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o',
HS384: 'eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hO2sthNQUSfvI9ylUdMKDxcrm8jB3KL6Rtkd3FOskL-jVqYh2CK1es8FKCQO8_tW',
HS512: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wUVS6tazE2N98_J4SH_djkEe1igXPu0qILAvVXCiO6O20gdf5vZ2sYFWX3c-Hy6L4TD47b3DSAAO9XjSqpJfag',
RS256: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Eci61G6w4zh_u9oOCk_v1M_sKcgk0svOmW4ZsL-rt4ojGUH2QY110bQTYNwbEVlowW7phCg7vluX_MCKVwJkxJT6tMk2Ij3Plad96Jf2G2mMsKbxkC-prvjvQkBFYWrYnKWClPBRCyIcG0dVfBvqZ8Mro3t5bX59IKwQ3WZ7AtGBYz5BSiBlrKkp6J1UmP_bFV3eEzIHEFgzRa3pbr4ol4TK6SnAoF88rLr2NhEz9vpdHglUMlOBQiqcZwqrI-Z4XDyDzvnrpujIToiepq9bCimPgVkP54VoZzy-mMSGbthYpLqsL_4MQXaI1Uf_wKFAUuAtzVn4-ebgsKOpvKNzVA',
RS384: 'eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.oPvWzaCp8xUpt5mHhPSn0qLsZfCFj0NVmb4mz4dFQPCCMj-F5zVn9e3zZoj0lIXWM8rxB69QHC3Er47mtDt3BKgysTL3BvvV89kD6UjLoUcAI3lwj0mi7acLoE27i1_TnIBqWNRPAsdvTDawNE0_4lvI5bxEWQCqisJwxCoMDIeJsmDzfyApgU_SAFSVULxXwU2VewaxdQB-41OZdWwUEAxh81iB6DFWrqd2CaJkUYoWjgYpeWsyeC2m_-ECGrHGEz1nKTm9c7BaPxurz7fHD7RJd9Wpx-mKDVsfspO9quWb_OLeGGbxTtAomMvjQjut56kx2fqTleDnNDh_0GE88w',
RS512: 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.kEZmDAnHHU_0bcGXMd5LA7vF87yQgXNaioPHP4lU4O3JJYuZ54fJdv3HT58xk-MFEDuWro_5fvNIp2VM-PlkZvYWrhQkJ-c-seoSa3ANq_PciC3bGfzYHEdjAE71GrAMI4FlcAGsq3ChkOnCTFqjWDmVwaRYCgMsFQ-U5cjvFhndFMizrkRljTF4v5oFdWytV_J-UafPtNdQXcGND1M74DqObnTHhZHg8aDfNzZcvnIeKcDVGUlUEL5ia1kPMrVhCtOAOJmEU8ivCdWWzt-jMQBf7cZeoCzDKHG72ysTTCfRoBVc1_SrQTHcHDiiBeW9nCazMLkltyP5NeawR_RNlg'
}
test.each(Object.entries(externalTokens))('Verify external tokens: %s', async (algorithm, token) => {
const key = secrets[algorithm]
let privateKey = key
let publicKey = key
if (typeof key === 'object') {
privateKey = key.private
publicKey = key.public
}
const verified = await JWT.verify(token, publicKey, { algorithm })
expect(verified).toBeTruthy()
const { payload } = JWT.decode(token)
expect({
sub: payload.sub,
name: payload.name
}).toMatchObject({
sub: testPayload.sub,
name: testPayload.name
})
})

8505
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "1.0.1", "version": "1.4.3",
"description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker", "description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "jest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -18,9 +18,12 @@
"cloudflare-worker" "cloudflare-worker"
], ],
"author": "Tobias Schneider", "author": "Tobias Schneider",
"license": "ISC", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/tsndr/cloudflare-worker-jwt/issues" "url": "https://github.com/tsndr/cloudflare-worker-jwt/issues"
}, },
"homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme" "homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme",
"devDependencies": {
"jest": "^28.1.0"
}
} }