mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-12 21:30:35 +01:00
114 lines
3.1 KiB
TypeScript
114 lines
3.1 KiB
TypeScript
// This file is licensed under the Fossorial Commercial License.
|
|
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
//
|
|
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
|
|
|
import * as crypto from "crypto";
|
|
|
|
/**
|
|
* Validates a JWT using a public key
|
|
* @param token - The JWT to validate
|
|
* @param publicKey - The public key used for verification (PEM format)
|
|
* @returns The decoded payload if validation succeeds, throws an error otherwise
|
|
*/
|
|
function validateJWT<Payload>(
|
|
token: string,
|
|
publicKey: string
|
|
): Payload {
|
|
// Split the JWT into its three parts
|
|
const parts = token.split(".");
|
|
if (parts.length !== 3) {
|
|
throw new Error("Invalid JWT format");
|
|
}
|
|
|
|
const [encodedHeader, encodedPayload, signature] = parts;
|
|
|
|
// Decode the header to get the algorithm
|
|
const header = JSON.parse(Buffer.from(encodedHeader, "base64").toString());
|
|
const algorithm = header.alg;
|
|
|
|
// Verify the signature
|
|
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
const isValid = verify(signatureInput, signature, publicKey, algorithm);
|
|
|
|
if (!isValid) {
|
|
throw new Error("Invalid signature");
|
|
}
|
|
|
|
// Decode the payload
|
|
const payload = JSON.parse(
|
|
Buffer.from(encodedPayload, "base64").toString()
|
|
);
|
|
|
|
// Check if the token has expired
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (payload.exp && payload.exp < now) {
|
|
throw new Error("Token has expired");
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* Verifies the signature of a JWT
|
|
*/
|
|
function verify(
|
|
input: string,
|
|
signature: string,
|
|
publicKey: string,
|
|
algorithm: string
|
|
): boolean {
|
|
let verifyAlgorithm: string;
|
|
|
|
// Map JWT algorithm name to Node.js crypto algorithm name
|
|
switch (algorithm) {
|
|
case "RS256":
|
|
verifyAlgorithm = "RSA-SHA256";
|
|
break;
|
|
case "RS384":
|
|
verifyAlgorithm = "RSA-SHA384";
|
|
break;
|
|
case "RS512":
|
|
verifyAlgorithm = "RSA-SHA512";
|
|
break;
|
|
case "ES256":
|
|
verifyAlgorithm = "SHA256";
|
|
break;
|
|
case "ES384":
|
|
verifyAlgorithm = "SHA384";
|
|
break;
|
|
case "ES512":
|
|
verifyAlgorithm = "SHA512";
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
}
|
|
|
|
// Convert base64url signature to standard base64
|
|
const base64Signature = base64URLToBase64(signature);
|
|
|
|
// Verify the signature
|
|
const verifier = crypto.createVerify(verifyAlgorithm);
|
|
verifier.update(input);
|
|
return verifier.verify(publicKey, base64Signature, "base64");
|
|
}
|
|
|
|
/**
|
|
* Converts base64url format to standard base64
|
|
*/
|
|
function base64URLToBase64(base64url: string): string {
|
|
// Add padding if needed
|
|
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
|
|
const pad = base64.length % 4;
|
|
if (pad) {
|
|
if (pad === 1) {
|
|
throw new Error("Invalid base64url string");
|
|
}
|
|
base64 += "=".repeat(4 - pad);
|
|
}
|
|
|
|
return base64;
|
|
}
|
|
|
|
export { validateJWT };
|