Last active
May 22, 2024 11:28
-
-
Save sdesalas/4bc58e1bd6d79daf5236de4ed91fbd5a to your computer and use it in GitHub Desktop.
JWT ES256 (ECDSA) Encoding and decoding in plain JavaScript (Web Crypto API)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<body> | |
<h1>JWT ES256 Encoding and decoding in plain JavaScript (Web Crypto API)</h1> | |
<textarea id="log" style="width: 100%; height: 400px;"></textarea> | |
</body> | |
<script> | |
/// | |
/// PLEASE BE AWARE | |
/// IT IS GENERALLY A BAD IDEA TO GENERATE JWT TOKENS IN THE BROWSER | |
/// THIS SHOULD ONLY BE USED FOR TESTING PURPOSES | |
/// | |
// Generate JWT ES256 public/private key pair for use in JWT (ES256 algorithm) | |
// $ openssl ecparam -name secp256r1 -genkey -out jwt.es256.priv | |
// $ openssl ec -in jwt.es256.priv -pubout -outform PEM -out jwt.es256.pub | |
// | |
const jwtPrivES256 = ` | |
-----BEGIN EC PARAMETERS----- | |
BggqhkjOPQMBBw== | |
-----END EC PARAMETERS----- | |
-----BEGIN EC PRIVATE KEY----- | |
MHcCAQEEIB5G491EqwG7V6Lz2g5445eCHpmCbAR8QZoiq/UqOYQxoAoGCCqGSM49 | |
AwEHoUQDQgAEoRd6GJcVumBBpHmLVqz4wD169mFa3QL2yDumnLTGowJCvhONhLYe | |
+jjHRmq/N8MK3NNGfurfvVMy9YSOvQ3Uzg== | |
-----END EC PRIVATE KEY-----`; | |
const jwtPubES256 = ` | |
-----BEGIN PUBLIC KEY----- | |
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoRd6GJcVumBBpHmLVqz4wD169mFa | |
3QL2yDumnLTGowJCvhONhLYe+jjHRmq/N8MK3NNGfurfvVMy9YSOvQ3Uzg== | |
-----END PUBLIC KEY----- | |
`; | |
// Create PKCS8 format and encode as base64 | |
// $ openssl pkcs8 -topk8 -inform PEM -outform DER -in jwt.es256.priv -out jwt.es256.pkcs8 -nocrypt | |
// $ base64 -i jwt.es256.pkcs8 > jwt.es256.pkcs8.base64 | |
// | |
const pkcs8 = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHkbj3USrAbtXovPaDnjjl4IemYJsBHxBmiKr9So5hDGhRANCAAShF3oYlxW6YEGkeYtWrPjAPXr2YVrdAvbIO6actMajAkK+E42Eth76OMdGar83wwrc00Z+6t+9UzL1hI69DdTO'; | |
// Note: SPKI is just the public PEM key but removing the `-----BEGIN PUBLIC KEY-----` and line endings | |
const spki = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoRd6GJcVumBBpHmLVqz4wD169mFa3QL2yDumnLTGowJCvhONhLYe+jjHRmq/N8MK3NNGfurfvVMy9YSOvQ3Uzg=='; | |
function base64urlencode(str) { | |
return window.btoa(str) | |
.replace(/\+/g, '-') | |
.replace(/\//g, '_') | |
.replace(/=+$/, ''); | |
} | |
function base64urldecode(b64) { | |
return atob(b64.replace(/_/g, '/').replace(/-/g, '+')); | |
} | |
function stripurlencoding(b64) { | |
return b64.replace(/_/g, '/').replace(/-/g, '+'); | |
} | |
function base64ToArrayBuffer(b64) { | |
var byteString = window.atob(b64); | |
var byteArray = new Uint8Array(byteString.length); | |
for (var i = 0; i < byteString.length; i++) { | |
byteArray[i] = byteString.charCodeAt(i); | |
} | |
return byteArray.buffer; | |
} | |
function textToArrayBuffer(str) { | |
var buf = unescape(encodeURIComponent(str)) // 2 bytes for each char | |
var bufView = new Uint8Array(buf.length) | |
for (var i=0; i < buf.length; i++) { | |
bufView[i] = buf.charCodeAt(i) | |
} | |
return bufView | |
} | |
function arrayBufferToBase64(buffer) { | |
var binary = ''; | |
var bytes = new Uint8Array( buffer ); | |
var len = bytes.byteLength; | |
for (var i = 0; i < len; i++) { | |
binary += String.fromCharCode( bytes[ i ] ); | |
} | |
return binary; | |
} | |
const algo = { | |
name: "ECDSA", | |
namedCurve: "P-256", // secp256r1 | |
}; | |
const hash = {name: "SHA-256"}; | |
const signAlgo = {...algo, hash}; | |
async function sign(str) { | |
console.log('sign()', str) | |
const priv = await window.crypto.subtle.importKey( | |
"pkcs8", | |
base64ToArrayBuffer(pkcs8), | |
algo, | |
true, // extractable | |
["sign"] | |
) | |
const encoded = new TextEncoder().encode(str); | |
const result = await window.crypto.subtle.sign(signAlgo, priv, encoded); | |
return base64urlencode(arrayBufferToBase64(result)); | |
}; | |
async function verify(signature, data) { // base64 | |
console.log('verify()', signature, data) | |
const pub = await window.crypto.subtle.importKey( | |
"spki", | |
base64ToArrayBuffer(spki), | |
algo, | |
true, // extractable | |
["verify"] | |
) | |
console.log({pub}); | |
const bufSignature = base64ToArrayBuffer(stripurlencoding(signature)); | |
const bufData = textToArrayBuffer(data); | |
const result = await window.crypto.subtle.verify(signAlgo, pub, bufSignature, bufData); | |
console.log({bufSignature, bufData, result}); | |
return result; | |
} | |
// @see https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-structure | |
async function generateJWT(p) { | |
const header = JSON.stringify({ alg: 'ES256', typ: 'JWT' }); | |
const payload = JSON.stringify(p); | |
const input = `${base64urlencode(header)}.${base64urlencode(payload)}`; | |
const signature = await sign(input); | |
return `${input}.${signature}`; | |
} | |
async function verifyJWT(jwt) { | |
const [header, payload, signature] = String(jwt).split('.'); | |
const base64signature = stripurlencoding(signature); | |
return await verify(base64signature, `${header}.${payload}`); | |
} | |
console.log = (...arr) => { | |
const log = document.getElementById('log'); | |
arr.forEach(item => { | |
log.value = `${log.value}${JSON.stringify(item)} `; | |
}); | |
log.value = `${log.value}\n`; | |
} | |
(async () => { | |
// 1. Create payload | |
const payload = {iss: 'test-issuer', hello: 'world', iat: 1691779304}; | |
console.log('PAYLOAD', payload); | |
// 2. Generate JWT | |
const jwt = await generateJWT(payload); | |
console.log('JWT', jwt); | |
// 3. Verify JWT | |
const verified = await verifyJWT(jwt); | |
console.log('VERIFIED', verified); | |
})(); | |
</script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment