@digitalbazaar/minimal-jwt
Version:
Minimal signature/verification JWT library
108 lines (92 loc) • 3.68 kB
JavaScript
/*!
* Copyright (c) 2020-2023 Digital Bazaar, Inc. All rights reserved.
*/
import * as base64url from 'base64url-universal';
import {b64UrlEncodedStringToObject, createJwsSigningInput} from './util.js';
const BASE64URL_REGEX = /^[A-Za-z0-9_-]+$/;
/**
* Serializes and signs the payload as a JWT.
*
* @param {object} options - The options to use.
* @param {object} options.payload - The set of claims to be signed.
* @param {object} options.header - A set of JOSE Header Parameters.
* @param {string} header.alg -The cryptographic algorithm to secure the JWT.
* @param {string} header.kid -The ID of the key used to secure the JWT.
* @param {Function} options.signFn - Performs the signature on the JWS Signing
* Input.
*
* @returns {Promise<{string}>} Resolves with the signed JWT.
*/
export async function sign({payload, header = {}, signFn}) {
if(!(header.alg && typeof header.alg === 'string')) {
throw new Error('An algorithm "header.alg" must be a string.');
}
if(!(header.kid && typeof header.kid === 'string')) {
throw new Error('A Key ID "header.kid" must be a string.');
}
if(!(signFn && typeof signFn === 'function')) {
throw new Error('A sign function "signFn" must be specified.');
}
// JWS Protected Header
const jwsHeader = {
...header,
typ: header.typ || 'JWT'
};
// Encode the JWS Protected Header as BASE64URL(UTF8(JWS Protected Header))
const encodedHeader = base64url.encode(JSON.stringify(jwsHeader));
// Encode the JWS Payload as BASE64URL(UTF8(JWS Payload))
const encodedPayload = base64url.encode(JSON.stringify(payload));
// Create the JWS Signing Input
// BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload))
const data = createJwsSigningInput({encodedHeader, encodedPayload});
// Compute the JWS Signature
const signature = await signFn({data});
// Encode the JWS Signature as BASE64URL(JWS Signature)
let encodedSignature;
if(typeof signature === 'string' && BASE64URL_REGEX.test(signature)) {
encodedSignature = signature;
} else if(signature instanceof Uint8Array) {
encodedSignature = base64url.encode(signature);
} else {
throw new Error(
'"signFn" must return a base64url-encoded string or a Uint8Array.');
}
// Concatenate these values in the order Header.Payload.Signature
return encodedHeader + '.' + encodedPayload + '.' + encodedSignature;
}
/**
* Verifies the claims of a JWT.
*
* @param {object} options - The options to use.
* @param {string} options.jwt - The JSON Web Token to verify.
* @param {Function} options.verifyFn - Verifies the signature and header of
* the JWT.
*
* @returns {Promise<{object}>} Resolves with parsed header and claims of JWT.
*/
export async function verify({jwt, verifyFn}) {
if(!(jwt && typeof jwt === 'string' && jwt.includes('.'))) {
throw new TypeError('The "jwt" is invalid.');
}
if(!(verifyFn && typeof verifyFn === 'function')) {
throw new Error('A verify function "verifyFn" must be specified.');
}
const [encodedHeader, encodedPayload, encodedSignature] = jwt.split('.');
const header = b64UrlEncodedStringToObject({
str: encodedHeader,
name: 'JWT Header'
});
const payload = b64UrlEncodedStringToObject({
str: encodedPayload,
name: 'JWT Payload'
});
// perform signature verification
const signature = base64url.decode(encodedSignature);
const data = createJwsSigningInput({encodedHeader, encodedPayload});
const {alg, kid} = header;
const verified = await verifyFn({alg, kid, data, signature});
if(!verified) {
throw new Error('Failed to verify signature.');
}
return {header, payload};
}