UNPKG

@sd-jwt/core

Version:

sd-jwt draft 7 implementation in typescript

302 lines (261 loc) 8.59 kB
import Crypto from 'node:crypto'; import { describe, expect, test } from 'vitest'; import { Jwt } from '../jwt'; import type { Signer, Verifier } from '../types'; import { base64urlEncode, SDJWTException } from '../utils'; describe('JWT', () => { test('create', async () => { const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, }); expect(jwt.header).toEqual({ alg: 'EdDSA' }); expect(jwt.payload).toEqual({ foo: 'bar' }); }); test('returns decoded JWT when correct JWT string is provided', () => { const jwt = `${base64urlEncode( JSON.stringify({ alg: 'HS256', typ: 'JWT' }), )}.${base64urlEncode( JSON.stringify({ sub: '1234567890', name: 'John Doe' }), )}.signature`; const result = Jwt.decodeJWT(jwt); expect(result).toEqual({ header: { alg: 'HS256', typ: 'JWT' }, payload: { sub: '1234567890', name: 'John Doe' }, signature: 'signature', }); }); test('throws an error when JWT string is not correctly formed', () => { const jwt = 'abc.def'; expect(() => Jwt.decodeJWT(jwt)).toThrow('Invalid JWT as input'); }); test('throws an error when JWT parts are missing', () => { const jwt = `${base64urlEncode( JSON.stringify({ alg: 'HS256', typ: 'JWT' }), )}`; expect(() => Jwt.decodeJWT(jwt)).toThrow('Invalid JWT as input'); }); test('set', async () => { const jwt = new Jwt(); jwt.setHeader({ alg: 'EdDSA' }); jwt.setPayload({ foo: 'bar' }); expect(jwt.header).toEqual({ alg: 'EdDSA' }); expect(jwt.payload).toEqual({ foo: 'bar' }); }); test('sign', async () => { const { privateKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, }); const encodedJwt = await jwt.sign(testSigner); expect(typeof encodedJwt).toBe('string'); }); test('verify', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const testVerifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, }); const encodedJwt = await jwt.sign(testSigner); const newJwt = Jwt.fromEncode(encodedJwt); const verified = await newJwt.verify(testVerifier); expect(verified).toStrictEqual({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, }); try { await newJwt.verify(() => false); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); } }); test('encode', async () => { const { privateKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, }); const encodedJwt = await jwt.sign(testSigner); const newJwt = Jwt.fromEncode(encodedJwt); const newEncodedJwt = newJwt.encodeJwt(); expect(newEncodedJwt).toBe(encodedJwt); }); test('decode failed', () => { expect(() => Jwt.fromEncode('asfasfas')).toThrow(); }); test('encode failed', async () => { const jwt = new Jwt({ header: { alg: 'EdDSA' }, }); try { jwt.encodeJwt(); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); } }); test('getUnsignedToken failed', async () => { const { privateKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, }); try { await jwt.sign(testSigner); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); } }); test('wrong encoded field', async () => { const { privateKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, encoded: 'asfasfafaf.dfasfafafasf', // it has to be 3 parts }); try { await jwt.sign(testSigner); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); } }); test('verify failed no signature', async () => { const { publicKey } = Crypto.generateKeyPairSync('ed25519'); const testVerifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { foo: 'bar' }, }); try { await jwt.verify(testVerifier); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); } }); test('verify with issuance date in the future', async () => { const { publicKey } = Crypto.generateKeyPairSync('ed25519'); const testVerifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { iat: Math.floor(Date.now() / 1000) + 100 }, }); try { await jwt.verify(testVerifier); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); expect((e as SDJWTException).message).toBe( 'Verify Error: JWT is not yet valid', ); } }); test('verify with not before in the future', async () => { const { publicKey } = Crypto.generateKeyPairSync('ed25519'); const testVerifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { nbf: Math.floor(Date.now() / 1000) + 100 }, }); try { await jwt.verify(testVerifier); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); expect((e as SDJWTException).message).toBe( 'Verify Error: JWT is not yet valid', ); } }); test('verify with expired', async () => { const { publicKey } = Crypto.generateKeyPairSync('ed25519'); const testVerifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { exp: Math.floor(Date.now() / 1000) }, }); try { await jwt.verify(testVerifier, { currentDate: Math.floor(Date.now() / 1000) + 100, }); } catch (e: unknown) { expect(e).toBeInstanceOf(SDJWTException); expect((e as SDJWTException).message).toBe( 'Verify Error: JWT is expired', ); } }); test('verify with skew', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testVerifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const jwt = new Jwt({ header: { alg: 'EdDSA' }, payload: { exp: Math.floor(Date.now() / 1000) - 1 }, }); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; await jwt.sign(testSigner); await jwt.verify(testVerifier, { skewSeconds: 2 }); }); });