diff --git a/README.md b/README.md
index 89ea2e6..08dcc64 100644
--- a/README.md
+++ b/README.md
@@ -15,57 +15,66 @@ A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers.
## Install
-```
+```bash
npm i @tsndr/cloudflare-worker-jwt
```
## Examples
-
### Basic Example
```typescript
async () => {
- import jwt from '@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')
+ // Create a token
+ const token = await sign({
+ sub: "1234",
+ name: "John Doe",
+ email: "john.doe@gmail.com"
+ }, "secret")
- // Verifing token
- const isValid = await jwt.verify(token, 'secret')
+ // Verify token
+ const verifiedToken = await verify(token, "secret")
- // Check for validity
- if (!isValid)
+ // Abort if token isn't valid
+ if (!verifiedToken)
return
- // Decoding token
- const { payload } = jwt.decode(token)
+ // Access token payload
+ const { payload } = verifiedToken
+
+ // { sub: "1234", name: "John Doe", email: "john.doe@gmail.com" }
}
```
+
### Restrict Timeframe
```typescript
async () => {
- import jwt from '@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',
+ // Create a token
+ const token = await sign({
+ sub: "1234",
+ 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')
+ }, "secret")
- // Verifing token
- const isValid = await jwt.verify(token, 'secret') // false
+ // Verify token
+ const verifiedToken = await verify(token, "secret") // false
- // Check for validity
- if (!isValid)
+ // Abort if token isn't valid
+ if (!verifiedToken)
return
- // Decoding token
- const { payload } = jwt.decode(token) // { name: 'John Doe', email: 'john.doe@gmail.com', ... }
+ // Access token payload
+ const { payload } = verifiedToken
+
+ // { sub: "1234", name: "John Doe", email: "john.doe@gmail.com", ... }
}
```
@@ -78,79 +87,101 @@ async () => {
### Sign
-#### `jwt.sign(payload, secret, [options])`
+#### `sign(payload, secret, [options])`
Signs a payload and returns the token.
+
#### Arguments
-Argument | Type | Status | Default | Description
------------------------- | ------------------ | -------- | ----------- | -----------
+Argument | Type | Status | Default | Description
+------------------------ | ----------------------------------- | -------- | ----------- | -----------
`payload` | `object` | required | - | The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
`secret` | `string`, `JsonWebKey`, `CryptoKey` | required | - | A string which is used to sign the payload.
`options` | `string`, `object` | optional | `HS256` | Either the `algorithm` string or an object.
`options.algorithm` | `string` | optional | `HS256` | See [Available Algorithms](#available-algorithms)
`options.keyid` | `string` | optional | `undefined` | The `keyid` or `kid` to be set in the header of the resulting JWT.
+
#### `return`
+
Returns token as a `string`.
+
+
### Verify
-#### `jwt.verify(token, secret, [options])`
+#### `verify(token, secret, [options])`
-Verifies the integrity of the token and returns a boolean value.
+Verifies the integrity of the token.
-Argument | Type | Status | Default | Description
------------------------- | ------------------ | -------- | ------- | -----------
-`token` | `string` | required | - | The token string generated by `jwt.sign()`.
+Argument | Type | Status | Default | Description
+------------------------ | ----------------------------------- | -------- | ------- | -----------
+`token` | `string` | required | - | The token string generated by `sign()`.
`secret` | `string`, `JsonWebKey`, `CryptoKey` | required | - | The string which was used to sign the payload.
`options` | `string`, `object` | optional | `HS256` | Either the `algorithm` string or an object.
`options.algorithm` | `string` | optional | `HS256` | See [Available Algorithms](#available-algorithms)
`options.clockTolerance` | `number` | optional | `0` | Clock tolerance in seconds, to help with slighly out of sync systems.
-`options.throwError` | `boolean` | optional | `false` | By default this we will only throw implementation errors, only set this to `true` if you want verification errors to be thrown as well.
+`options.throwError` | `boolean` | optional | `false` | By default this we will only throw integration errors, only set this to `true` if you want verification errors to be thrown as well.
#### `throws`
-If `options.throwError` is `true` and the token is invalid, an error will be thrown.
+
+Throws integration errors and if `options.throwError` is set to `true` also throws `ALG_MISMATCH`, `NOT_YET_VALID`, `EXPIRED` or `INVALID_SIGNATURE`.
+
#### `return`
-Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
+
+Returns the decoded token or `undefined`.
+
+```typescript
+{
+ header: {
+ alg: "HS256",
+ typ: "JWT"
+ },
+ payload: {
+ name: "John Doe",
+ email: "john.doe@gmail.com"
+ }
+}
+```
+
+
### Decode
-#### `jwt.decode(token)`
+#### `decode(token)`
-Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure!
+Just returns the decoded token **without** verifying verifying it. Please use `verify()` if you intend to verify it as well.
Argument | Type | Status | Default | Description
----------- | -------- | -------- | ------- | -----------
-`token` | `string` | required | - | The token string generated by `jwt.sign()`.
+`token` | `string` | required | - | The token string generated by `sign()`.
+
#### `return`
+
Returns an `object` containing `header` and `payload`:
-```javascript
+
+```typescript
{
header: {
- alg: 'HS256',
- typ: 'JWT'
+ alg: "HS256",
+ typ: "JWT"
},
payload: {
- name: 'John Doe',
- email: 'john.doe@gmail.com'
+ name: "John Doe",
+ email: "john.doe@gmail.com"
}
}
```
+
### Available Algorithms
- - ES256
- - ES384
- - ES512
- - HS256
- - HS384
- - HS512
- - RS256
- - RS384
- - RS512
\ No newline at end of file
+
+ - `ES256`, `ES384`, `ES512`
+ - `HS256`, `HS384`, `HS512`
+ - `RS256`, `RS384`, `RS512`
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 4c41db9..9018d11 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -137,11 +137,11 @@ const algorithms: JwtAlgorithms = {
/**
* 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} Returns token as a `string`.
+ * @param payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
+ * @param secret A string which is used to sign the payload.
+ * @param [options={ algorithm: "HS256", header: { typ: "JWT" } }] The options object or the algorithm.
+ * @throws If there"s a validation issue.
+ * @returns Returns token as a `string`.
*/
export async function sign(payload: JwtPayload, secret: string | JsonWebKey | CryptoKey, options: JwtSignOptions | JwtAlgorithm = "HS256"): Promise {
if (typeof options === "string")
@@ -177,13 +177,13 @@ export async function sign(payload: JwtPayload} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
+ * @param token The token string generated by `sign()`.
+ * @param secret The string which was used to sign the payload.
+ * @param options The options object or the algorithm.
+ * @throws Throws integration errors and if `options.throwError` is set to `true` also throws `NOT_YET_VALID`, `EXPIRED` or `INVALID_SIGNATURE`.
+ * @returns Returns the decoded token or `undefined`.
*/
-export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = "HS256"): Promise {
+export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = "HS256"): Promise | undefined> {
if (typeof options === "string")
options = { algorithm: options }
options = { algorithm: "HS256", clockTolerance: 0, throwError: false, ...options }
@@ -207,41 +207,40 @@ export async function verify(token: string, secret: string | JsonWebKey | Crypto
if (!algorithm)
throw new Error("algorithm not found")
- const { header, payload } = decode(token)
-
- if (header?.alg !== options.algorithm) {
- if (options.throwError)
- throw new Error("ALG_MISMATCH")
- return false
- }
+ const decodedToken = decode(token)
try {
- if (!payload)
- throw new Error("PARSE_ERROR")
+ if (decodedToken.header?.alg !== options.algorithm)
+ throw new Error("INVALID_SIGNATURE")
- const now = Math.floor(Date.now() / 1000)
+ if (decodedToken.payload) {
+ const now = Math.floor(Date.now() / 1000)
- if (payload.nbf && payload.nbf > now && (payload.nbf - now) > (options.clockTolerance ?? 0))
- throw new Error("NOT_YET_VALID")
+ if (decodedToken.payload.nbf && decodedToken.payload.nbf > now && (decodedToken.payload.nbf - now) > (options.clockTolerance ?? 0))
+ throw new Error("NOT_YET_VALID")
- if (payload.exp && payload.exp <= now && (now - payload.exp) > (options.clockTolerance ?? 0))
- throw new Error("EXPIRED")
+ if (decodedToken.payload.exp && decodedToken.payload.exp <= now && (now - decodedToken.payload.exp) > (options.clockTolerance ?? 0))
+ 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]}`))
+ if (!await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`)))
+ throw new Error("INVALID_SIGNATURE")
+
+ return decodedToken
} catch(err) {
if (options.throwError)
throw err
- return false
+ return
}
}
/**
- * Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure!
+ * Returns the payload **without** verifying the integrity of the token. Please use `verify()` first to keep your application secure!
*
- * @param {string} token The token string generated by `jwt.sign()`.
- * @returns {JwtData} Returns an `object` containing `header` and `payload`.
+ * @param token The token string generated by `sign()`.
+ * @returns Returns an `object` containing `header` and `payload`.
*/
export function decode(token: string): JwtData {
return {
diff --git a/src/utils.ts b/src/utils.ts
index aaaa913..0b8dd90 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,3 +1,5 @@
+export type KeyUsages = "sign" | "verify"
+
export function bytesToByteString(bytes: Uint8Array): string {
let byteStr = ""
for (let i = 0; i < bytes.byteLength; i++) {
@@ -39,9 +41,10 @@ export function base64UrlToArrayBuffer(b64url: string): ArrayBuffer {
}
export function textToBase64Url(str: string): string {
- const encoder = new TextEncoder();
- const charCodes = encoder.encode(str);
- const binaryStr = String.fromCharCode(...charCodes);
+ const encoder = new TextEncoder()
+ const charCodes = encoder.encode(str)
+ const binaryStr = String.fromCharCode(...charCodes)
+
return btoa(binaryStr).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
}
@@ -49,7 +52,6 @@ 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 {
return await crypto.subtle.importKey("raw", textToArrayBuffer(key), algorithm, true, keyUsages)
}
diff --git a/tests/algorithms.spec.ts b/tests/algorithms.spec.ts
index f130e8f..bdb21f2 100644
--- a/tests/algorithms.spec.ts
+++ b/tests/algorithms.spec.ts
@@ -83,7 +83,7 @@ describe("Internal", () => {
expect(decoded.payload).toMatchObject(payload)
const verified = await jwt.verify(token, data.public, algorithm)
- expect(verified).toBe(true)
+ expect(verified).toBeTruthy()
})
})
@@ -99,6 +99,6 @@ describe("External", async () => {
})
const verified = await jwt.verify(data.token, data.public, algorithm)
- expect(verified).toBe(true)
+ expect(verified).toBeTruthy()
})
})
\ No newline at end of file
diff --git a/tests/index.spec.ts b/tests/index.spec.ts
index 2a9df05..7b49d6b 100644
--- a/tests/index.spec.ts
+++ b/tests/index.spec.ts
@@ -14,11 +14,11 @@ describe("Verify", async () => {
const expiredToken = await jwt.sign({ sub: "me", exp: now - offset }, secret)
test("Valid", () => {
- expect(jwt.verify(validToken, secret, { throwError: true })).resolves.toBe(true)
+ expect(jwt.verify(validToken, secret, { throwError: true })).resolves.toBeTruthy()
})
test("Not yet expired", () => {
- expect(jwt.verify(notYetExpired, secret, { throwError: true })).resolves.toBe(true)
+ expect(jwt.verify(notYetExpired, secret, { throwError: true })).resolves.toBeTruthy()
})
test("Not yet valid", () => {
@@ -30,8 +30,8 @@ describe("Verify", async () => {
})
test("Clock offset", () => {
- expect(jwt.verify(notYetValidToken, secret, { clockTolerance: offset, throwError: true })).resolves.toBe(true)
- expect(jwt.verify(expiredToken, secret, { clockTolerance: offset, throwError: true })).resolves.toBe(true)
+ expect(jwt.verify(notYetValidToken, secret, { clockTolerance: offset, throwError: true })).resolves.toBeTruthy()
+ expect(jwt.verify(expiredToken, secret, { clockTolerance: offset, throwError: true })).resolves.toBeTruthy()
expect(jwt.verify(notYetValidToken, secret, { clockTolerance: offset - 1, throwError: true })).rejects.toThrowError("NOT_YET_VALID")
expect(jwt.verify(expiredToken, secret, { clockTolerance: offset - 1, throwError: true })).rejects.toThrowError("EXPIRED")
diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts
index a4a88be..54979cf 100644
--- a/tests/utils.spec.ts
+++ b/tests/utils.spec.ts
@@ -77,5 +77,5 @@ describe("Imports", () => {
})
describe.todo("Payload", () => {
- test.todo("decodePayload")
+ test.todo("decodePayload")
})
\ No newline at end of file