UNPKG

@shlomiatar/nano-jwt

Version:

A tiny, minimalistic HS256/HS512 jwt verifier using WebCryptoAPIs (for browser/bun/cloudflare)

32 lines (27 loc) 1.77 kB
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' }]; } } } };