Skip to content

Instantly share code, notes, and snippets.

@PeterHindes
Last active January 28, 2024 20:45
Show Gist options
  • Save PeterHindes/97883a77294d5852949036a985ffaa10 to your computer and use it in GitHub Desktop.
Save PeterHindes/97883a77294d5852949036a985ffaa10 to your computer and use it in GitHub Desktop.
Cloudflare Workers JWT Functions

Avalible Functions

generateKeyPair

Generates a public and private key

keyToString

Converts a key object into a string

stringToPrivateKey

converts a private key back from a string

stringToPublicKey

same but for public keys

sign

signs a payload and returns a jwt

verify

validates a jwt and returns the payload and a validation value

Example use

import { generateKeyPair, keyToString, stringToPrivateKey, stringToPublicKey, sign, verify } from './jwtFuncs.js'

// generate token function returns a signed json web token that includes, user id, and expiration date, and creation date,
async function generateToken(userId, privateKey) {
	// constant for expiration time 6hours
	const expirationTime = 6 * 60 * 60 * 1000
	// get current time
	const currentDate = new Date()
	// get expiration date
	const expirationDate = new Date(currentDate.getTime() + expirationTime)
	// get creation date
	const creationDate = new Date()
	// create payload
	const payload = {
		userId: userId,
		expirationDate: expirationDate,
		creationDate: creationDate
	}
	const jsonPayload = JSON.stringify(payload)
	console.log(jsonPayload);
	// sign payload
	const token = await sign(jsonPayload, privateKey)
	// return token
	return token
}


export default {
	async fetch(request, env) {
		const url = new URL(request.url)
		const pathname = url.pathname

		var privateKey = env.PRK
		var publicKey = env.PUK
		privateKey = await stringToPrivateKey(privateKey)
		publicKey = await stringToPublicKey(publicKey)

		if (pathname === '/sign') {
			const token = await generateToken(911, privateKey)
			return new Response(token)
		}
		else if (pathname === '/verify') {
			const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.IntcInVzZXJJZFwiOjkxMSxcImV4cGlyYXRpb25EYXRlXCI6XCIyMDIzLTA4LTEzVDAzOjA4OjI3LjgyOFpcIixcImNyZWF0aW9uRGF0ZVwiOlwiMjAyMy0wOC0xMlQyMTowODoyNy44MjhaXCJ9Ig.6Es_PUscpIPV2Q1LI3Dya0E-qCcsA0Vemp6sL/haLc+dy0GfqCjM90tyMZctK3P8yPJrzNUaYRsUkox+vn61I51uacaYg5Am2W3WTspUwpBz8jiilUMPXP1Z5sbGlD0JOc3RCC9uHKJe8940vWWJBZNq4dwVJDJ6386V0jKyXkqPMJs6aZkyOsGQcm4F7tBYnUUusSqnC4jB1iAVmJ/HxY4i5cLDda1Ftp3odxS0PcCEYbeGyJndpbtWkNcFp+3hZIuSc1O6F3xqFJwyew2umvYML1mvFCYbbYtKZkXf2fs7Lrb/tEp+apPxco5kSV7GyeL3XqqcB0SyF/y8EQWFzQ'

			const { isValid, payload } = await verify(token, publicKey);
			if (isValid) {
				console.log("Valid token");
				console.log(payload);
			}
			return new Response(isValid)
		} else {
			return new Response('Not Found', { status: 404 })
		}
	}
}
async function generateKeyPair() {
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: 'SHA-256',
},
true,
['sign', 'verify']
)
const privateKey = keyPair.privateKey
const publicKey = keyPair.publicKey
return { privateKey, publicKey }
}
// Convert a CryptoKey object to a string
async function keyToString(key) {
const exported = await crypto.subtle.exportKey('jwk', key);
return JSON.stringify(exported);
}
// Convert a string to a private CryptoKey object
async function stringToPrivateKey(str) {
const imported = JSON.parse(str);
return await crypto.subtle.importKey('jwk', imported, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['sign']);
}
// Convert a string to a public CryptoKey object
async function stringToPublicKey(str) {
const imported = JSON.parse(str);
return await crypto.subtle.importKey('jwk', imported, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', publicExponent: new Uint8Array([1, 0, 1]), modulusLength: 2048 }, true, ['verify']);
}
async function sign(payload, privateKey) {
// Encode header
const header = { alg: 'RS256', typ: 'JWT' }
const encodedHeader = base64UrlEncode(JSON.stringify(header))
// Encode payload
const encodedPayload = base64UrlEncode(JSON.stringify(payload))
// Create signature
const data = encodedHeader + '.' + encodedPayload
const encoder = new TextEncoder()
const encodedData = encoder.encode(data)
const signatureBuffer = await crypto.subtle.sign(
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
privateKey,
encodedData
)
const signature = base64UrlEncode(ab2str(signatureBuffer))
// Create token
const token = data + '.' + signature
return token
}
async function verify(token, publicKey) {
// Split token into parts
const parts = token.split('.')
if (parts.length !== 3) {
throw new Error('Invalid token')
}
// Decode header and payload
var header = null
var payload = null
var isValid = false
try {
header = JSON.parse(base64UrlDecode(parts[0]))
payload = JSON.parse(JSON.parse(base64UrlDecode(parts[1])))
// console.log(payload);
} catch (error) {
console.log(error);
return { isValid, payload }
}
// Verify signature
const data = parts[0] + '.' + parts[1]
const encoder = new TextEncoder()
const encodedData = encoder.encode(data)
const signatureBuffer = str2ab(base64UrlDecode(parts[2]))
isValid = await crypto.subtle.verify(
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
publicKey,
signatureBuffer,
encodedData
)
return { isValid, payload }
}
function base64UrlEncode(str) {
const base64 = btoa(str)
return base64.replace('+', '-').replace('/', '_').replace(/=+$/, '')
}
function base64UrlDecode(base64Url) {
let base64 = base64Url.replace('-', '+').replace('_', '/')
while (base64.length % 4) {
base64 += '='
}
return atob(base64)
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf))
}
function str2ab(str) {
const bufView = new Uint8Array(str.length)
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i)
}
return bufView.buffer
}
export { generateKeyPair, keyToString, stringToPrivateKey, stringToPublicKey, sign, verify };
@PeterHindes
Copy link
Author

PeterHindes commented Aug 12, 2023

Disclaimer: A large part of this was written with the help of Github Copilot, it seems sound, and the crypto is provided by the webCrypto API so that is unlikely to have issues, but still please point out any security issues so I can fix them for anyone else who finds this on google.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment