Files
astro-oidc-rp/src/lib/sign.ts

69 lines
2.1 KiB
TypeScript

const te = new TextEncoder();
function b64uToBytes(input: string): Uint8Array {
// pad and replace
input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4;
if (pad) input += "=".repeat(4 - pad);
return new Uint8Array(Buffer.from(input, "base64"));
}
function bytesToB64u(bytes: Uint8Array | ArrayBuffer): string {
const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
return Buffer.from(buf)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
async function importKey(secret: string): Promise<CryptoKey> {
return await crypto.subtle.importKey(
"raw",
te.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
}
export async function signPayload(
payload: unknown,
secret: string,
): Promise<string> {
const payloadJson =
typeof payload === "string" ? payload : JSON.stringify(payload);
const payloadB64u = bytesToB64u(te.encode(payloadJson));
const key = await importKey(secret);
const sig = await crypto.subtle.sign("HMAC", key, te.encode(payloadB64u));
const sigB64u = bytesToB64u(new Uint8Array(sig));
return `${payloadB64u}.${sigB64u}`;
}
export async function verifyAndDecode<T = unknown>(
token: string,
secret: string,
): Promise<{ valid: boolean; payload?: T }> {
const [payloadB64u, sigB64u] = token.split(".");
if (!payloadB64u || !sigB64u) return { valid: false };
const key = await importKey(secret);
const expected = await crypto.subtle.sign(
"HMAC",
key,
te.encode(payloadB64u),
);
const given = b64uToBytes(sigB64u);
const expectedBytes = new Uint8Array(expected);
if (given.length !== expectedBytes.length) return { valid: false };
// Constant-time compare
let diff = 0;
for (let i = 0; i < given.length; i++) diff |= given[i] ^ expectedBytes[i];
if (diff !== 0) return { valid: false };
try {
const json = Buffer.from(b64uToBytes(payloadB64u)).toString("utf8");
return { valid: true, payload: JSON.parse(json) as T };
} catch {
return { valid: false };
}
}