Skip to content

Instantly share code, notes, and snippets.

@sdesalas
Last active May 22, 2024 11:28
Show Gist options
  • Save sdesalas/4bc58e1bd6d79daf5236de4ed91fbd5a to your computer and use it in GitHub Desktop.
Save sdesalas/4bc58e1bd6d79daf5236de4ed91fbd5a to your computer and use it in GitHub Desktop.
JWT ES256 (ECDSA) Encoding and decoding in plain JavaScript (Web Crypto API)
<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