@shlomiatar/nano-jwt
Version:
A tiny, minimalistic HS256/HS512 jwt verifier using WebCryptoAPIs (for browser/bun/cloudflare)
32 lines (27 loc) • 1.77 kB
text/typescript
const [dec, enc] = [new TextDecoder(), new TextEncoder()];
const b64uToU8 = (s: string) => new Uint8Array(atob(s.replace(/-/g, '+').replace(/_/g, '/')).split('').map(c => c.charCodeAt(0)));
const b64uToJson = (s: string) => JSON.parse(dec.decode(b64uToU8(s)));
type Matcher<T> = Partial<T> | ((payload: T) => true | string);
export const jwt = <T extends object>(secret: string, alg: 'HS256' | 'HS512' = 'HS256', matcher?: Matcher<T>) => {
let _key: CryptoKey;
const hash = `SHA-${alg.substring(2)}`;
const matcherFn = matcher ? (typeof matcher === 'function' ? matcher : ((p: T) => Object.keys(matcher ?? {}).every(k => p[k as keyof T] === (matcher as Partial<T>)[k as keyof T]))) : () => true;
return {
verify: async (token: string): Promise<[boolean, T | { error: string }]> => {
try {
_key = _key ?? await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash }, false, ['verify'])
const [enc_alg, enc_payload, enc_sign, ...rest] = token.split('.');
if (!enc_alg || !enc_payload || !enc_sign || rest.length) return [false, { error: 'invalid_format' }];
if (b64uToJson(enc_alg).alg != alg) return [false, { error: 'unsupported_algorithm' }];
if (!await crypto.subtle.verify('HMAC',_key, b64uToU8(enc_sign), enc.encode(`${enc_alg}.${enc_payload}`))) return [false, { error: 'bad_signature' }];
const payload = b64uToJson(enc_payload);
// run the matcher if provided
const result = matcherFn(payload as T);
if (result !== true) return [false, { error: result || 'invalid_payload' }];
return [true, payload as T];
} catch {
return [false, { error: 'verification_failed' }];
}
}
}
};