1
0

Compare commits

..

43 Commits

Author SHA1 Message Date
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
12 changed files with 1554 additions and 3842 deletions

View File

@@ -2,29 +2,29 @@ name: Publish
on:
release:
types: [created]
types: [published]
jobs:
publish-npm:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 12
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm publish --access public
- 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}}
publish-gpr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v3
with:
node-version: 12
registry-url: https://npm.pkg.github.com/
- run: npm publish --access public
- name: Publish to GPR
run: npm publish --tag latest --access public
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

4
.gitignore vendored
View File

@@ -142,3 +142,7 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Custom
/index.js
/index.d.ts

6
.npmignore Normal file
View File

@@ -0,0 +1,6 @@
.github/
src/
test/
.nvmrc
index.test.js
tsconfig.json

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16

View File

@@ -8,12 +8,15 @@ A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers.
- [Install](#install)
- [Examples](#examples)
- [Usage](#usage)
- [Sign](#sign)
- [Verify](#verify)
- [Decode](#decode)
## Install
```
npm i -D @tsndr/cloudflare-worker-jwt
npm i @tsndr/cloudflare-worker-jwt
```
@@ -21,9 +24,9 @@ npm i -D @tsndr/cloudflare-worker-jwt
### Basic Example
```javascript
```typescript
async () => {
const jwt = require('@tsndr/cloudflare-worker-jwt')
import jwt from '@tsndr/cloudflare-worker-jwt'
// Creating a token
const token = await jwt.sign({ name: 'John Doe', email: 'john.doe@gmail.com' }, 'secret')
@@ -36,15 +39,15 @@ async () => {
return
// Decoding token
const payload = jwt.decode(token)
const { payload } = jwt.decode(token)
}
```
### Restrict Timeframe
```javascript
```typescript
async () => {
const jwt = require('@tsndr/cloudflare-worker-jwt')
import jwt from '@tsndr/cloudflare-worker-jwt'
// Creating a token
const token = await jwt.sign({
@@ -62,56 +65,78 @@ async () => {
return
// Decoding token
const payload = jwt.decode(token) // { name: 'John Doe', email: 'john.doe@gmail.com', ... }
const { payload } = jwt.decode(token) // { name: 'John Doe', email: 'john.doe@gmail.com', ... }
}
```
## Usage
- [Sign](#sign)
- [Verify](#verify)
- [Decode](#decode)
<hr>
### `jwt.sign(payload, secret, [options])`
### Sign
#### `jwt.sign(payload, secret, [options])`
Signs a payload and returns the token.
#### Arguments
Argument | Type | Satus | 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.
`secret` | `string` | required | - | A string which is used to sign the payload.
`options` | `object`, `string` | optional | `{ algorithm: 'HS256' }` | The options object supporting `algorithm` and `keyid` or just the algorithm string. (See [Available Algorithms](#available-algorithms))
`options` | `object` | optional | `{ algorithm: 'HS256' }` | The options object supporting `algorithm` and `keyid`. (See [Available Algorithms](#available-algorithms))
#### `return`
Returns token as a `string`.
<hr>
### `jwt.verify(token, secret, [options])`
### Verify
#### `jwt.verify(token, secret, [options])`
Verifies the integrity of the token and returns a boolean value.
Argument | Type | Satus | Default | Description
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.
`algorithm` | `object`, `string` | optional | `{ algorithm: 'HS256', throwError: false }` | The options object supporting `algorithm` or just the algorithm string. (See [Available Algorithms](#available-algorithms))
`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>
### `jwt.decode(token)`
### 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
Argument | Type | Status | Default | Description
----------- | -------- | -------- | ------- | -----------
`token` | `string` | required | - | The token string generated by `jwt.sign()`.
#### `return`
Returns payload `object`.
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

55
index.d.ts vendored
View File

@@ -1,55 +0,0 @@
/**
* 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 {object | null} Returns payload `object`.
*/
decode(token: string): object | null
}
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
}
export = _exports

123
index.js
View File

@@ -1,123 +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 (typeof crypto === 'undefined' || !crypto.subtle)
throw new Error('Crypto not supported!')
this.algorithms = {
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' } },
}
}
_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);
}
return buf;
}
_decodePayload(raw) {
switch (raw.length % 4) {
case 0:
break
case 2:
raw += '=='
break
case 3:
raw += '='
break
default:
throw new Error('Illegal base64url string!')
}
try {
return JSON.parse(decodeURIComponent(escape(atob(raw))))
} catch {
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]), `${tokenParts[0]}.${tokenParts[1]}`)
}
decode(token) {
return this._decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
}
}
module.exports = new JWT

View File

@@ -1,19 +1,31 @@
const { subtle } = require('crypto').webcrypto
const { subtle } = require('node:crypto').webcrypto
Object.defineProperty(global, 'crypto', {
value: { subtle }
})
const JWT = require('./index')
const jwt = require('./index.js')
const oneDay = (60 * 60 * 24)
const now = Date.now() / 1000
const algorithms = [
'ES256',
'ES384',
'ES512',
'HS256',
'HS384',
'HS512',
'RS256',
'RS384',
'RS512'
]
const secrets = {}
// Keypairs
for (const algorithm of Object.keys(JWT.algorithms)) {
if (algorithm.startsWith('HS'))
for (const algorithm of algorithms) {
if (algorithm.startsWith('HS')) {
//secrets[algorithm] = 'c2VjcmV0'
secrets[algorithm] = 'secret'
else if (algorithm.startsWith('RS')) {
} else if (algorithm.startsWith('RS')) {
secrets[algorithm] = {
public: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
@@ -111,16 +123,19 @@ const testPayload = {
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 })
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 })
const verified = await jwt.verify(token, publicKey, { algorithm })
expect(verified).toBeTruthy()
const payload = JWT.decode(token)
expect(payload).toBeTruthy()
const { payload } = jwt.decode(token)
expect({
sub: payload.sub,
name: payload.name
@@ -145,15 +160,16 @@ const externalTokens = {
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
if (typeof key === 'object')
publicKey = key.public
}
const verified = await JWT.verify(token, publicKey, { algorithm })
const verified = await jwt.verify(token, publicKey, { algorithm })
expect(verified).toBeTruthy()
const payload = JWT.decode(token)
const { payload } = jwt.decode(token)
expect({
sub: payload.sub,
name: payload.name

4828
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,11 @@
{
"name": "@tsndr/cloudflare-worker-jwt",
"version": "1.3.0",
"version": "2.2.1",
"description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest"
},
"repository": {
@@ -24,6 +26,8 @@
},
"homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme",
"devDependencies": {
"jest": "^28.1.0"
"@cloudflare/workers-types": "^3.13.0",
"jest": "^28.1.1",
"typescript": "^4.7.4"
}
}

313
src/index.ts Normal file
View File

@@ -0,0 +1,313 @@
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 interface JwtAlgorithms {
[key: string]: SubtleCryptoImportKeyAlgorithm
}
/**
* @typedef JwtHeader
* @prop {string} [typ] Type
*/
export interface JwtHeader {
/**
* Type (default: `"JWT"`)
*
* @default "JWT"
*/
typ?: string
[key: string]: any
}
/**
* @typedef JwtPayload
* @prop {string} [iss] Issuer
* @prop {string} [sub] Subject
* @prop {string} [aud] Audience
* @prop {string} [exp] Expiration Time
* @prop {string} [nbf] Not Before
* @prop {string} [iat] Issued At
* @prop {string} [jti] JWT ID
*/
export interface JwtPayload {
/** Issuer */
iss?: string
/** Subject */
sub?: string
/** Audience */
aud?: string
/** Expiration Time */
exp?: number
/** Not Before */
nbf?: number
/** Issued At */
iat?: number
/** JWT ID */
jti?: string
[key: string]: any
}
/**
* @typedef JwtOptions
* @prop {JwtAlgorithm | string} algorithm
*/
export interface JwtOptions {
algorithm?: JwtAlgorithm | string
}
/**
* @typedef JwtSignOptions
* @extends JwtOptions
* @prop {JwtHeader} [header]
*/
export interface JwtSignOptions extends JwtOptions {
header?: JwtHeader
}
/**
* @typedef JwtVerifyOptions
* @extends JwtOptions
* @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`)
*/
export interface JwtVerifyOptions extends JwtOptions {
/**
* If `true` throw error if checks fail. (default: `false`)
*
* @default false
*/
throwError?: boolean
}
/**
* @typedef JwtData
* @prop {JwtHeader} header
* @prop {JwtPayload} payload
*/
export interface JwtData {
header: JwtHeader
payload: JwtPayload
}
function base64UrlParse(s: string): Uint8Array {
// @ts-ignore
return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0)))
// return new Uint8Array(Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''))).map(c => c.charCodeAt(0)))
}
function base64UrlStringify(a: Uint8Array): string {
// @ts-ignore
return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
// return btoa(String.fromCharCode.apply(0, Array.from(a))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
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' } }
}
function _utf8ToUint8Array(str: string): Uint8Array {
return base64UrlParse(btoa(unescape(encodeURIComponent(str))))
}
function _str2ab(str: string): ArrayBuffer {
str = atob(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);
}
return buf;
}
function _decodePayload(raw: string): JwtHeader | JwtPayload | null {
switch (raw.length % 4) {
case 0:
break
case 2:
raw += '=='
break
case 3:
raw += '='
break
default:
throw new Error('Illegal base64url string!')
}
try {
return JSON.parse(decodeURIComponent(escape(atob(raw))))
} catch {
return null
}
}
/**
* 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} 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: JwtPayload, secret: string | JsonWebKey, options: JwtSignOptions | JwtAlgorithm = { algorithm: 'HS256', header: { typ: 'JWT' } }): Promise<string> {
if (typeof options === 'string')
options = { algorithm: options, header: { typ: 'JWT' } }
options = { algorithm: 'HS256', header: { typ: 'JWT' }, ...options }
if (payload === null || typeof payload !== 'object')
throw new Error('payload must be an object')
if (typeof secret !== 'string' && typeof secret !== 'object')
throw new Error('secret must be a string or a JWK 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 payloadAsJSON = JSON.stringify(payload)
const partialToken = `${base64UrlStringify(_utf8ToUint8Array(JSON.stringify({ ...options.header, alg: options.algorithm })))}.${base64UrlStringify(_utf8ToUint8Array(payloadAsJSON))}`
let keyFormat = 'raw'
let keyData
if (typeof secret === 'object') {
keyFormat = 'jwk'
keyData = secret
} else if (typeof secret === 'string' && secret.startsWith('-----BEGIN')) {
keyFormat = 'pkcs8'
keyData = _str2ab(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, ''))
} else
keyData = _utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['sign'])
const signature = await crypto.subtle.sign(algorithm, key, _utf8ToUint8Array(partialToken))
return `${partialToken}.${base64UrlStringify(new Uint8Array(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} 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, 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 or a JWK 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)
if (!payload) {
if (options.throwError)
throw 'PARSE_ERROR'
return false
}
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 (typeof secret === 'object') {
keyFormat = 'jwk';
keyData = secret;
} else if (typeof secret === 'string' && secret.startsWith('-----BEGIN')) {
keyFormat = 'spki'
keyData = _str2ab(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, ''))
} else
keyData = _utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['verify'])
return await crypto.subtle.verify(algorithm, key, base64UrlParse(tokenParts[2]), _utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`))
}
/**
* 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(token: string): JwtData {
return {
header: _decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader,
payload: _decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload
}
}
export default {
sign,
verify,
decode
}

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"outDir": ".",
"module": "commonjs",
"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"]
}