commit 6a17fb6be2bf334079cee30f099291d2e98d27c0 Author: Tobias Schneider Date: Thu Feb 4 12:02:42 2021 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..2dfe0f9 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Cloudflare Worker JWT + +A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers. + +## Contents + +- [Usage](#usage) +- [Install](#install) + +## Usage + +### Simple Example + +```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 + +``` +npm i @tsndr/cloudflare-worker-jwt +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..a300159 --- /dev/null +++ b/index.js @@ -0,0 +1,102 @@ +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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2313ef --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tsndr/cloudflare-worker-jwt", + "version": "1.0.0", + "description": "A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tsndr/cloudflare-worker-jwt.git" + }, + "keywords": [ + "jwt", + "token", + "cloudflare", + "worker", + "cloudflare-worker" + ], + "author": "Tobias Schneider", + "license": "ISC", + "bugs": { + "url": "https://github.com/tsndr/cloudflare-worker-jwt/issues" + }, + "homepage": "https://github.com/tsndr/cloudflare-worker-jwt#readme" +}