Signing JWT Securely From Your Browser Using WebCrypto

Hero Image

Traditionally we look at keying material as something that exists, primarily, serverside, or only in crafted scenarios with HTTPS client-side certificates. But, with the Web Crypto API, we have a large variety of untapped opportunities available to client-side SaaS Web applications!

In this post, we will explore the basic mechanics of Web Cryptography API RSA key generation, creating PEM certificates for use with SSH or other environments, and how to use those keys to sign JWTs for web authentication.

But first - why would we want to do any of this?


Traditional security measures in the browser started, in the dark ages, with Basic authentication using just a username and a password. Over the years, we’ve progressed from those humble beginnings to using cookies, secrets in the header, and eventually arrived at using JSON Web Tokens - for better or worse - as the defacto bearer token authentication mechanism.

These JWTs are issued via identity providers - Google, GitHub, Microsoft, Apple, and so forth - and are passed on each request back to the server. The JWT is cryptographically validated and authorized for the requested resource on the server. But the issuer of the JWT remains locked behind the identity provider’s infrastructure.

By and large, this is a good thing - the user's browser is, despite many years of effort, the least defended and most vulnerable element of the system. Plus, each authentication system would also need to be configured to trust the browser in a way that is moderately unnatural for modern architectures, which consider themselves as the sole source of authorization truth.

Delegated authorization models

Systems like AWS IAM, CI/CD, and the relationship between a user's browser and other local tooling like a CLI often handle authentication and authorization as independent channels. This isolation leads to user experiences that are less than ideal: having to manually copy secrets and client IDs from the browser into a hand-crafted configuration file for the AWS CLI, for example, or being unable to create lower-empowered accounts without substantial manual effort in the tooling.

Locally-driven authentication

What if we supported creating authentication tokens with embedded authorization information directly in the browser? This would allow the browser to operate as an effective peer to traditional OAuth2 experiences, empowering other consumers on the local system to perform restricted operations.

This would allow us to:

  • Create our own JWTs from the browser that are suitable for less-trusted environments.
  • Keep key pairs in local storage for fast user authentication without having to perform OAuth2 round trips through an identity provider.
  • Perform API calls without having to request a key directly from a third-party system.

A real-world example

Here at Fusebit, we use our ability to mint JWTs from the browser with local private keys to perform authorized API calls against our backend, without having to implement a wholly independent authorization mechanism. Additionally, these generated keys can live in OS protected spaces such as Apple Keychain adding an additional layer of security over keys stored elsewhere.

Having a single code path to authorize an API call allows us to simplify our authentication and authorization code, and leverage the same mechanisms throughout our various platforms and integrations: CLI, web, and automation all use the same mechanisms.


Let’s look at the underlying code that enables this functionality within the browser.

New key generation

First, let’s generate a new RSA 4kb key:

const generatedKeyPair: CryptoKeyPair = await crypto.subtle.generateKey(
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
['sign', 'verify']

This creates a RSA key, with a 4k modulus, that is suitable for use with for signing data and verifying existing signatures.

Convert the key to PEM

We can then take the returned generatedKeyPair and convert it to PEM:

function toPem(key: ArrayBuffer, type: 'private' | 'public'): string {
const pemContents = breakPemIntoMultipleLines(arrayBufferToBase64(key));
return `-----BEGIN ${type.toUpperCase()} KEY-----\n${pemContents}-----END ${type.toUpperCase()} KEY-----`;

// Let’s use the new toPem function to create PEM format strings for the privateKey and publicKey
const privateKeyBuffer: ArrayBuffer = await crypto.subtle.exportKey('pkcs8', generatedKeyPair.privateKey);
const privateKeyPem = toPem(privateKeyBuffer, 'private');

const exportedPublicKey: ArrayBuffer = await crypto.subtle.exportKey('spki', generatedKeyPair.publicKey);
const publicKeyPem = toPem(exportedPublicKey, 'public');

These PEM keys can be easily exported to the filesystem and used by a wide variety of tools such as ssh or openssl.

Creating and signing a JWT

Signing a JWT requires creating an object in a specific format, and then signing it using the private key we’ve created earlier. There’s lots of examples of JWT formats out there, so consider this code largely as an example, rather than prescriptive for all cases:

export async function signJwt(
tokenPayload: TokenPayload,
issuer: Issuer,
privateKey: CryptoKey,
algorithmOptions: Record<string, string> = {}
): Promise<string> {
const header = {
alg: algorithmOptions.algorithm || 'RS256',
typ: 'JWT',
kid: issuer.publicKeys[0].keyId,

const nowInSeconds = Math.floor( / 1000);
const neverEndingExpInSeconds = 9999999999;

const payload: TokenPayload = {
iat: nowInSeconds,
exp: neverEndingExpInSeconds,

const stringifiedHeader = JSON.stringify(header);
const stringifiedPayload = JSON.stringify(payload);

const headerBase64 = uint8ArrayToString(stringToUint8Array(stringifiedHeader));
const payloadBase64 = uint8ArrayToString(stringToUint8Array(stringifiedPayload));
const headerAndPayload = `${headerBase64}.${payloadBase64}`;

const messageAsUint8Array = stringToUint8Array(headerAndPayload);

const signature = await crypto.subtle.sign(
name: || 'RSASSA-PKCS1-v1_5',
hash: algorithmOptions.hash || 'SHA-256',

const base64Signature = uint8ArrayToString(new Uint8Array(signature));

return `${headerAndPayload}.${base64Signature}`;

To wrap up…

Hopefully, you’ll find the above code and implementation details helpful! Don’t hesitate to reach out if you have any questions, and we’ll be happy to help push through. You can find me on the Fusebit Discord, our community Slack, and at

Fusebit is a code-first integration platform that helps developers integrate their applications with external systems and APIs. To learn more, take Fusebit for a spin or look at our getting started guide!


Here’s a variety of utility functions used in the previous code examples:

function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {
const byteArray = new Uint8Array(arrayBuffer);
let byteString = '';
byteArray.forEach((byte) => {
byteString += String.fromCharCode(byte);
return btoa(byteString);

function breakPemIntoMultipleLines(pem: string): string {
const charsPerLine = 64;
let pemContents = '';
while (pem.length > 0) {
pemContents += `${pem.substring(0, charsPerLine)}\n`;
pem = pem.substring(64);
return pemContents;
function base64ToUint8Array(base64Contents: string): Uint8Array {
base64Contents = base64Contents.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
const content = atob(base64Contents);
return new Uint8Array(content.split('').map((c) => c.charCodeAt(0)));

function stringToUint8Array(contents: string): Uint8Array {
const encoded = btoa(unescape(encodeURIComponent(contents)));
return base64ToUint8Array(encoded);

function uint8ArrayToString(unsignedArray: Uint8Array): string {
const base64string = btoa(String.fromCharCode(...unsignedArray));
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');


Most Recent

15 September 2022
What Is Deno and Why You Should Try It

Learn the basics of what the Deno runtime environment is all about, and highlight the key features, benefits, and detractions.

1 September 2022
How to Implement GitHub OAuth in Your Node.js App

OAuth is a standard protocol used for authorization. This post will discuss implementing GitHub OAuth in your Node.js app.

31 August 2022
QuickBooks OAuth: How to Set up the Online Accounting API in Your Web App

Intuit uses OAuth2 to authorize applications for access to company data. Here's how to use their library to authorize your Node app.