Create JWK and JWT in JavaScript

This article shows an examples of how you can create and deploy a JSON Web Key (JWK) and generate a JSON Web Token (JWT) purely in JavaScript. You may want to use this method when embedding GoodData using iframes or React SDK.

Install Dependencies

The examples were written using Node.JS version 18.17.0.

Ensure you have Node.JS version 18.17 or newer installed and then proceed with the installation of other dependencies.

Steps:

  1. Create a new Node.JS application:

    npm init
    
  2. Install dependencies:

    npm install jose@^4.14.4 node-forge@^1.3.1 uuid@^9.0.0
    

Create and Upload JWK

The following example shows how to generate a new JWK and push it to GoodData JWK store all from a single JavaScript application.

Steps:

  1. Create a new file called generate-jwk.mjs:

    import fs from "fs";
    import * as jose from "jose";
    import forge from "node-forge";
    
    function generateJwkCertificate(privateKeyPem, publicKeyPem) {
    // generate a new certificate for the public key and sign it with private key,
    // alternatively import your own certificate for existing key pair
    const certificateAttrs = [
            { name: "countryName", value: "US" },
            { name: "stateOrProvinceName", value: "California" },
            { name: "localityName", value: "San Francisco" },
            { name: "organizationName", value: "GoodData" },
            { name: "commonName", value: "gooddata.com" },
        ];
    
        const certificate = forge.pki.createCertificate();
        certificate.publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
        certificate.serialNumber = Math.floor(Math.random() * 1000000000).toString();
        certificate.validity.notBefore = new Date();
        certificate.validity.notAfter = new Date();
        certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1);
        certificate.setSubject(certificateAttrs);
        certificate.setIssuer(certificateAttrs);
        certificate.setExtensions([{
            name: "basicConstraints",
            cA: true
        }, {
            name: "keyUsage",
            keyCertSign: true,
            digitalSignature: true,
            nonRepudiation: true,
            keyEncipherment: true,
            dataEncipherment: true
        }, {
            name: "extKeyUsage",
            serverAuth: true,
            clientAuth: true,
            codeSigning: true,
            emailProtection: true,
            timeStamping: true
        }, {
            name: "nsCertType",
            client: true,
            server: true,
            email: true,
            objsign: true,
            sslCA: true,
            emailCA: true,
            objCA: true
        }, {
            name: "subjectAltName",
            altNames: [{
                type: 6, // URI
                value: "http://localhost"
            }, {
                type: 7, // IP
                ip: "127.0.0.1"
            }]
        }, {
            name: "subjectKeyIdentifier"
        }]);
    
        // sign the certificate
        const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
        certificate.sign(privateKey, forge.md.sha256.create());
    
        // generate Base64 URL encoded SHA1 thumbprint of signed ASN1 DER certificate (matches the backend logic)
        const asn1DerCertificate = forge.asn1.toDer(forge.pki.certificateToAsn1(certificate)).getBytes();
        const sha1Thumbprint = forge.md.sha1.create().update(asn1DerCertificate).digest().getBytes();
        const x5t = forge.util.encode64(sha1Thumbprint).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
    
        // convert to PEM, remove the comment header and footer, join to a single line, wrap to array
        const pemCertificate = forge.pki.certificateToPem(certificate);
        const lines = pemCertificate.split("\r\n").filter((line) => line.length > 0);
        const x5c = [lines.slice(1, -1).join("")];
    
        return { x5t, x5c };
    }
    
    async function generateJwk(algorithm, modulusLength, id) {
        // generate new RSA key pair (or import your own)
        const keyPair = await jose.generateKeyPair(algorithm, { modulusLength, extractable: true });
    
        // generate JWK base
        const jwk = await jose.exportJWK(keyPair.publicKey);
    
        // convert jose key pair to universal PEM string certificates
        const privateKeyPem = await jose.exportPKCS8(keyPair.privateKey);
        const publicKeyPem = await jose.exportSPKI(keyPair.publicKey);
    
        // generate certificate
        const jwkCertificate = generateJwkCertificate(privateKeyPem, publicKeyPem);
    
        return {
            jwk: {
                ...jwk,
                ...jwkCertificate,
                kid: id,
                use: "sig",
                alg: algorithm,
            },
            meta: {
                privateKeyPem,
                publicKeyPem,
            }
        };
    }
    
    // ID of the key under which it will be registered on GoodData Cloud server
    const keyId = "myKeyId";
    
    // call JWK generator
    const { jwk, meta } = await generateJwk("RS256", 2048, keyId);
    
    // save output of generator for later to generate JWT
    fs.writeFileSync("./jwk.json", JSON.stringify(jwk, null, 4));
    fs.writeFileSync("./jwk-meta.json", JSON.stringify(meta, null, 4));
    
    // register JWK on GoodData Cloud server
    
    const myHeaders = new Headers();
    myHeaders.append("Content-Type", "application/vnd.gooddata.api+json");
    // use your personal API token with Organization manage permission
    myHeaders.append("Authorization", "Bearer <api_token>");
    
    const requestOptions = {
        method: "POST",
        headers: myHeaders,
        body: JSON.stringify({
            data: {
                attributes: {
                    content: jwk
                },
                id: keyId,
                type: "jwk"
            }
        }),
        redirect: "follow"
    };
    
    // change the URL to include your GoodData Cloud hostname (such as https://my-company.trial.cloud.gooddata.com)
    fetch("https://<hostname>/api/v1/entities/jwks", requestOptions)
        .then(response => response.text())
        .then(result => console.log(result))
        .catch(error => console.log("error", error));
    
  2. Subtitute your own <api_token> and <hostname> values.

  3. Run the script to create a new JWK, register it in your GoodData Cloud organization and save the results to files:

    node ./generate-jwk.mjs
    

    You should see a success response from the GoodData server that contains the JWK metadata entity registered on the server.

Create JWT

You can now write a script that will generate JWT. The following script takes the login of a hypothetical user from your organization named John Doe. The actual code that generates JWT should be called only for users that your application authenticated.

The script uses files created by the generate-jwk.mjs script from the previous example. The files contain valid JWK registered on server and PEM RSA key pair used to generate the JWK.

Steps:

  1. Create a new file called generate-jwt.mjs:

    import fs from "fs";
    import * as jose from "jose";
    import { v4 as uuidv4 } from "uuid";
    
    // read JWT and keys from the files where we stored them
    const jwk = JSON.parse(fs.readFileSync("./jwk.json").toString());
    const keys = JSON.parse(fs.readFileSync("./jwk-meta.json").toString());
    
    const generateJwt = async (subject, userName, secondsToExpire, algorithm = "RS256", keyId = jwk.kid, jwtId = uuidv4()) => {
        // import keys to jose objects
        const privateKey = await jose.importPKCS8(keys.privateKeyPem, algorithm);
        const publicKey = await jose.importSPKI(keys.publicKeyPem, algorithm);
    
        // get current time from epoch in seconds for "issued at" claim
        const now = Math.round(Date.now() / 1000);
    
        // setup protected claims of the token
        const claims = {
            sub: subject, // subject of the token (user login in GoodData Cloud)
            name: userName, // name of the user
            iat: now, // time when token was issued
            jti: jwtId, // unique JWT id
        };
    
        // set token expiration, protected header attributes, and sign it with our private key
        const token = await new jose.SignJWT(claims)
            .setProtectedHeader({ alg: algorithm, typ: "JWT", kid: keyId })
            .setExpirationTime(`${secondsToExpire}s`)
            .sign(privateKey);
    
        // verify that token was encoded and signed correctly, write out the decrypted token
        try {
            const decryptedToken = await jose.jwtVerify(token, publicKey);
            console.log("Token signature verified:");
            console.log(decryptedToken, "\n");
        } catch (error) {
            console.error("Token verification failed:", error);
        }
    
        return token;
    };
    
    // generate the token for the user our application authenticated and we want to allow to authenticate to GoodData Cloud
    const jwt = await generateJwt("john.doe", "John Doe", 360_000);
    
    console.log(jwt);
    
  2. Run the script to write the decrypted version of the JWT (token payload and protected headers) into the console and then the actual encoded signed token:

    node ./generate-jwt.mjs
    

    You can use it as a bearer token to authenticate your API calls made to the GoodData for as long as it is valid (the code from the example generates a token valid for one hour) and use it for the login of an existing user from your organization.