1
0

Compare commits

...

136 Commits

Author SHA1 Message Date
32e00ac6b9 2.4.3 2024-01-26 15:03:51 +01:00
Nick DeGroot
247da9b396 🐛 Fix verification relying on a signing key 2024-01-26 15:02:52 +01:00
db0e5b51e0 add test workflow 2024-01-26 15:00:40 +01:00
6594895273 2.4.2 2024-01-21 00:08:03 +01:00
61a3a2ed50 update JwtPayload type, thanks @Le0Developer #61 2024-01-21 00:06:50 +01:00
d7a6847206 2.4.1 2024-01-20 23:50:02 +01:00
5ab19c4dc0 update npmignore 2024-01-20 23:49:51 +01:00
0308d20c38 restructure 2024-01-20 23:47:47 +01:00
703c0c4131 update .npmingnore 2024-01-18 22:42:36 +01:00
6b3e828126 working on more tests 2024-01-18 22:36:26 +01:00
35dc875f56 2.4.0 2024-01-18 20:53:34 +01:00
Richard Lee
11afa8eb87 Fix unicode payload signing 2024-01-18 20:52:36 +01:00
Leo Developer
b05345279d allow using cryptokey directly 2024-01-18 20:47:05 +01:00
Richard Lee
55bc15bec4 Fix typing to make build work 2024-01-18 20:36:33 +01:00
kira924age
b0d4084a0f refactor: 💡 reafactor decodePayload 2023-12-28 23:09:07 +01:00
kira924age
3fd594bbb5 feat: 🎸 update decodePayload 2023-12-28 23:09:07 +01:00
kira924age
f8a216574a fix: 🐛 update decodePayload 2023-12-28 23:09:07 +01:00
kira924age
1f511549f5 feat: 🎸 update decodePayload 2023-12-28 23:09:07 +01:00
Leo Developer
4be64469d3 fix header not being typed when using sign 2023-12-21 13:50:50 +01:00
72e64f1316 2.3.2 2023-11-16 20:34:51 +01:00
e235d835aa add generics to sign 2023-11-16 20:34:27 +01:00
03c66f4223 2.3.1 2023-11-16 20:23:58 +01:00
70488a6a48 update types and add generics to decode 2023-11-16 20:23:43 +01:00
760245d1c7 2.3.0 2023-11-16 15:53:01 +01:00
d40d924176 clean up 2023-11-16 15:52:03 +01:00
c2e4fccf56 fix decode issue #49 2023-11-16 15:46:27 +01:00
0c0919f78d 2.2.10 2023-11-14 12:10:04 +01:00
7299ea0614 update package.json 2023-11-14 12:09:51 +01:00
7004cd16ed clean up 2023-11-14 12:09:38 +01:00
5d4c51ad8a 2.2.9 2023-11-12 18:29:53 +01:00
78a0eeee9c clean up 2023-11-12 18:29:43 +01:00
565c623005 2.2.8 2023-11-12 18:04:05 +01:00
6b3192b4b9 refactor 2023-11-12 18:03:49 +01:00
a659abecda clean up 2023-11-11 19:53:36 +01:00
b16cc23654 2.2.7 2023-11-11 19:19:00 +01:00
0f347c77e1 update npmignore 2023-11-11 19:18:48 +01:00
39e2770ab3 2.2.6 2023-11-11 16:47:03 +01:00
78ef16e356 update tests 2023-11-11 16:44:42 +01:00
c4559460fb update action to latest node version for building 2023-10-11 21:07:51 +02:00
7027999975 2.2.5 2023-10-02 23:41:42 +02:00
166b074782 add .editorconfig 2023-10-02 23:41:30 +02:00
decbd238e9 2.2.4 2023-10-02 23:37:16 +02:00
894ac2c9f5 update dependencies 2023-10-02 23:37:01 +02:00
1394e4e33d 2.2.3 2023-10-02 23:19:51 +02:00
47028970b4 update readme 2023-10-02 23:18:05 +02:00
6ac43d493d add skipValidation option for verify 2023-10-02 23:10:45 +02:00
cc66bd9c47 add .editorconfig 2023-10-02 23:10:20 +02:00
a8f95f1c6d 2.2.2 2023-08-25 16:01:57 +02:00
Tom Lokhorst
b2886c0778 Allow array in Audience 2023-08-25 16:00:30 +02:00
76b7fcef27 2.2.1 2023-02-02 16:13:50 +01:00
3b5d8bcde8 hotfix 2023-02-02 16:13:28 +01:00
e22c509852 2.2.0 2023-02-02 15:48:08 +01:00
7ed375e876 audit fix 2023-02-02 15:46:10 +01:00
de658122b9 clean up 2023-02-02 15:46:10 +01:00
Michael Cereda
10dcf2f01b Support JWK format 2023-02-02 15:46:10 +01:00
1de6acbfff 2.1.4 2022-12-14 14:42:51 +01:00
b002db4547 option to set iat 2022-12-14 14:42:23 +01:00
1bdbd0a5c5 2.1.3 2022-11-25 00:34:50 +01:00
883ec55b73 ocd 2022-11-25 00:32:26 +01:00
Can Rau
c515fb76e6 docs: don't install as dev dependency (I think?)
Think the installation command shouldn't use `-D` as it's used in the production code right, so installing as dev dep would be confusing, right?

Please correct me if I'm wrong 🙏

Also thanks a lot for this package, was just using for a way to sign and verify JWT in Pages ❤️
2022-11-25 00:07:00 +01:00
ec41207832 2.1.2 2022-10-09 20:56:50 +02:00
e64e89f325 fix typo 2022-10-09 20:56:35 +02:00
fb573d928a 2.1.1 2022-10-09 20:48:01 +02:00
382ad66ea9 Revert "support for base64 secrets"
This reverts commit 67f1a8e5ed.
2022-10-09 20:47:29 +02:00
ffc1e87b32 2.1.0 2022-10-03 01:58:51 +02:00
decfa65391 clean up 2022-10-03 01:57:59 +02:00
badoge
67f1a8e5ed support for base64 secrets 2022-10-03 01:54:28 +02:00
fe1baf46b4 2.0.1 2022-07-08 15:07:00 +02:00
81f9eafd86 export types 2022-07-08 15:06:17 +02:00
747931ba69 2.0.0 2022-06-28 22:13:42 +02:00
261069d5cb fix workflow 2022-06-28 22:12:09 +02:00
7466a881a0 2.0.0-pre.0 2022-06-26 17:42:13 +02:00
81dde823d3 optimizations 2022-06-26 17:03:02 +02:00
210b733591 clean up 2022-06-26 15:54:10 +02:00
bf11705d11 exclude test file from npm package 2022-06-26 13:22:53 +02:00
99d172179e update workflow 2022-06-26 13:20:15 +02:00
63de8056a5 adapt old test file 2022-06-26 13:17:41 +02:00
b953f4165a typescript 2022-06-26 12:57:49 +02:00
c9da88c4bb Options fix 2022-06-22 19:11:53 +02:00
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
e7be1ed840 Bumped version 2021-02-04 12:20:45 +01:00
f2adb27218 Updated readme 2021-02-04 12:19:52 +01:00
17 changed files with 4874 additions and 160 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = false
[src/**/*.ts]
charset = utf-8
indent_style = space
indent_size = 4

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: [published]
jobs:
publish:
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: Build
run: npm run build
- name: Publish to npmjs
run: npm publish --tag latest --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- uses: actions/setup-node@v3
with:
registry-url: https://npm.pkg.github.com/
- name: Publish to GPR
run: npm publish --tag latest --access public
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

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

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

148
.gitignore vendored Normal file
View File

@@ -0,0 +1,148 @@
# 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.*
# Custom
/index.js
/index.d.ts

9
.npmignore Normal file
View File

@@ -0,0 +1,9 @@
.editorconfig
.github/
.gitignore
.nvmrc
coverage/
jest.config.ts
src/
tests/
tsconfig.json

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.

189
README.md
View File

@@ -2,62 +2,15 @@
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.
#### Parameters
`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
@@ -65,3 +18,133 @@ The token string generated by `jwt.sign()`.
``` ```
npm i @tsndr/cloudflare-worker-jwt npm i @tsndr/cloudflare-worker-jwt
``` ```
## Examples
### Basic Example
```typescript
async () => {
import jwt from '@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
```typescript
async () => {
import jwt from '@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 | 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.
`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 | Status | 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', skipValidation: false, throwError: false }` | The options object supporting `algorithm`, `skipValidation` 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 | Status | 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

102
index.js
View File

@@ -1,102 +0,0 @@
class Base64URL {
static parse(s) {
return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0)))
}
static stringify(a) {
return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
}
class JWT {
constructor() {
if (!crypto || !crypto.subtle)
throw new Error('Crypto not supported!')
this.algorithms = {
HS256: {
name: 'HMAC',
hash: {
name: 'SHA-256'
}
},
HS512: {
name: 'HMAC',
hash: {
name: 'SHA-512'
}
}
}
}
utf8ToUint8Array(str) {
const chars = []
str = btoa(unescape(encodeURIComponent(str)))
return Base64URL.parse(str)
}
async sign(payload, secret, alg = 'HS256') {
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 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:
break
case 2:
output += '=='
break
case 3:
output += '='
break
default:
throw new Error('Illegal base64url string!')
}
try {
return JSON.parse(decodeURIComponent(escape(atob(output))))
} catch {
return null
}
}
}
module.exports = new JWT

9
jest.config.ts Normal file
View File

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

3902
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,16 @@
{ {
"name": "@tsndr/cloudflare-worker-jwt", "name": "@tsndr/cloudflare-worker-jwt",
"version": "1.0.0", "version": "2.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", "type": "module",
"exports": "./index.js",
"types": "index.d.ts",
"engine": {
"node": ">=18"
},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "build": "tsc",
"test": "jest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -18,9 +24,19 @@
"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": {
"@cloudflare/workers-types": "^4.20231025.0",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
} }

239
src/index.ts Normal file
View File

@@ -0,0 +1,239 @@
import {
textToArrayBuffer,
arrayBufferToBase64Url,
base64UrlToArrayBuffer,
textToBase64Url,
importKey,
decodePayload
} from "./utils"
if (typeof crypto === 'undefined' || !crypto.subtle)
throw new Error('SubtleCrypto not supported!')
/**
* @typedef JwtAlgorithm
* @type {'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'}
*/
export type JwtAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'
/**
* @typedef JwtAlgorithms
*/
export type JwtAlgorithms = {
[key: string]: SubtleCryptoImportKeyAlgorithm
}
/**
* @typedef JwtHeader
* @prop {string} [typ] Type
*/
export type JwtHeader<T = {}> = {
/**
* Type (default: `"JWT"`)
*
* @default "JWT"
*/
typ?: string
} & T
/**
* @typedef JwtPayload
* @prop {string} [iss] Issuer
* @prop {string} [sub] Subject
* @prop {string | string[]} [aud] Audience
* @prop {string} [exp] Expiration Time
* @prop {string} [nbf] Not Before
* @prop {string} [iat] Issued At
* @prop {string} [jti] JWT ID
*/
export type JwtPayload<T = { [key: string]: any }> = {
/** Issuer */
iss?: string
/** Subject */
sub?: string
/** Audience */
aud?: string | string[]
/** Expiration Time */
exp?: number
/** Not Before */
nbf?: number
/** Issued At */
iat?: number
/** JWT ID */
jti?: string
} & T
/**
* @typedef JwtOptions
* @prop {JwtAlgorithm | string} algorithm
*/
export type JwtOptions = {
algorithm?: JwtAlgorithm | string
}
/**
* @typedef JwtSignOptions
* @extends JwtOptions
* @prop {JwtHeader} [header]
*/
export type JwtSignOptions<T> = {
header?: JwtHeader<T>
} & JwtOptions
/**
* @typedef JwtVerifyOptions
* @extends JwtOptions
* @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`)
*/
export type JwtVerifyOptions = {
/**
* If `true` throw error if checks fail. (default: `false`)
*
* @default false
*/
throwError?: boolean
} & JwtOptions
/**
* @typedef JwtData
* @prop {JwtHeader} header
* @prop {JwtPayload} payload
*/
export type JwtData<Payload = {}, Header = {}> = {
header?: JwtHeader<Header>
payload?: JwtPayload<Payload>
}
const algorithms: JwtAlgorithms = {
ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } },
ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } },
ES512: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } },
HS256: { name: 'HMAC', hash: { name: 'SHA-256' } },
HS384: { name: 'HMAC', hash: { name: 'SHA-384' } },
HS512: { name: 'HMAC', hash: { name: 'SHA-512' } },
RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
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
*
* @param {JwtPayload} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
* @param {string | JsonWebKey | CryptoKey} secret A string which is used to sign the payload.
* @param {JwtSignOptions | JwtAlgorithm | string} [options={ algorithm: 'HS256', header: { typ: 'JWT' } }] The options object or the algorithm.
* @throws {Error} If there's a validation issue.
* @returns {Promise<string>} Returns token as a `string`.
*/
export async function sign<Payload = {}, Header = {}>(payload: JwtPayload<Payload>, secret: string | JsonWebKey, options: JwtSignOptions<Header> | JwtAlgorithm = 'HS256'): Promise<string> {
if (typeof options === 'string')
options = { algorithm: options }
options = { algorithm: 'HS256', header: { typ: 'JWT' } as JwtHeader<Header>, ...options }
if (!payload || typeof payload !== 'object')
throw new Error('payload must be an object')
if (!secret || (typeof secret !== 'string' && typeof secret !== 'object'))
throw new Error('secret must be a string, a JWK object or a CryptoKey object')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm)
throw new Error('algorithm not found')
if (!payload.iat)
payload.iat = Math.floor(Date.now() / 1000)
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'])
const signature = await crypto.subtle.sign(algorithm, key, textToArrayBuffer(partialToken))
return `${partialToken}.${arrayBufferToBase64Url(signature)}`
}
/**
* Verifies the integrity of the token and returns a boolean value.
*
* @param {string} token The token string generated by `jwt.sign()`.
* @param {string | JsonWebKey | CryptoKey} 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`.
*/
export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise<boolean> {
if (typeof options === 'string')
options = { algorithm: options, throwError: false }
options = { algorithm: 'HS256', throwError: false, ...options }
if (typeof token !== 'string')
throw new Error('token must be a string')
if (typeof secret !== 'string' && typeof secret !== 'object')
throw new Error('secret must be a string, a JWK object or a CryptoKey object')
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 algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm)
throw new Error('algorithm not found')
const { payload } = decode(token)
try {
if (!payload)
throw new Error('PARSE_ERROR')
if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000))
throw new Error('NOT_YET_VALID')
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000))
throw new Error('EXPIRED')
const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ['verify'])
return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`))
} catch(err) {
if (options.throwError)
throw err
return false
}
}
/**
* 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 {JwtData} Returns an `object` containing `header` and `payload`.
*/
export function decode<Payload = {}, Header = {}>(token: string): JwtData<Payload, Header> {
return {
header: decodePayload<JwtHeader<Header>>(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')),
payload: decodePayload<JwtPayload<Payload>>(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
}
}
export default {
sign,
verify,
decode
}

94
src/utils.ts Normal file
View File

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

125
tests/index.spec.ts Normal file
View File

@@ -0,0 +1,125 @@
import crypto from 'node:crypto'
Object.defineProperty(global, 'crypto', { value: { subtle: crypto.webcrypto.subtle }})
import { describe, expect, test } from '@jest/globals'
import jwt, { JwtAlgorithm } from '../src/index'
type Dataset = {
public: string
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()
})
})

81
tests/utils.spec.ts Normal file
View File

@@ -0,0 +1,81 @@
import { describe, expect, test } from '@jest/globals'
import {
bytesToByteString,
byteStringToBytes,
arrayBufferToBase64String,
base64StringToArrayBuffer,
textToArrayBuffer,
arrayBufferToText,
arrayBufferToBase64Url,
base64UrlToArrayBuffer,
textToBase64Url,
pemToBinary,
importTextSecret
} from '../src/utils'
describe('Converters', () => {
const testString = 'cloudflare-worker-jwt'
const testByteArray = [ 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 45, 119, 111, 114, 107, 101, 114, 45, 106, 119, 116 ]
const testUint8Array = new Uint8Array(testByteArray)
const testBase64String = 'Y2xvdWRmbGFyZS13b3JrZXItand0'
const testArrayBuffer = testUint8Array.buffer
test('bytesToByteString', () => {
expect(bytesToByteString(testUint8Array)).toStrictEqual(testString)
})
test('byteStringToBytes', () => {
expect(byteStringToBytes(testString)).toStrictEqual(testUint8Array)
})
test('arrayBufferToBase64String', () => {
expect(arrayBufferToBase64String(testArrayBuffer)).toStrictEqual(testBase64String)
})
test('base64StringToArrayBuffer', () => {
expect(base64StringToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer)
})
test('textToArrayBuffer', () => {
expect(textToArrayBuffer(testString)).toStrictEqual(testUint8Array)
})
test('arrayBufferToText', () => {
expect(arrayBufferToText(testArrayBuffer)).toStrictEqual(testString)
})
test('arrayBufferToBase64Url', () => {
expect(arrayBufferToBase64Url(testArrayBuffer)).toStrictEqual(testBase64String)
})
test('base64UrlToArrayBuffer', () => {
expect(base64UrlToArrayBuffer(testBase64String)).toStrictEqual(testArrayBuffer)
})
test('textToBase64Url', () => {
expect(textToBase64Url(testString)).toStrictEqual(testBase64String)
})
test('pemToBinary', () => {
expect(pemToBinary(`-----BEGIN PUBLIC KEY-----\n${testBase64String}\n-----END PUBLIC KEY-----`)).toStrictEqual(testArrayBuffer)
})
})
describe('Imports', () => {
test('importTextSecret', async () => {
const testKey = 'cloudflare-worker-jwt'
const testAlgorithm = { name: 'HMAC', hash: { name: 'SHA-256' } }
const testCryptoKey = { type: 'secret', extractable: true, algorithm: { ...testAlgorithm, length: 168 }, usages: ['verify', 'sign'] }
expect(await importTextSecret(testKey, testAlgorithm, ['verify', 'sign'])).toMatchObject(testCryptoKey)
})
//test('importJwk', async () => {})
//test('importPublicKey', async () => {})
//test('importPrivateKey', async () => {})
//test('importKey', async () => {})
})
//describe('Payload', () => {
// test('decodePayload', () => {})
//})

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"outDir": ".",
"module": "esnext",
"target": "esnext",
"lib": ["esnext"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"preserveConstEnums": true,
"moduleResolution": "node",
"types": ["@cloudflare/workers-types"]
},
"include": ["src"],
"exclude": ["node_modules", "src/**/*.spec.ts"]
}