UNPKG

@sd-jwt/core

Version:

sd-jwt draft 7 implementation in typescript

308 lines (267 loc) 9.62 kB
import Crypto from 'node:crypto'; import type { Signer, Verifier } from '@sd-jwt/types'; import { SDJWTException } from '@sd-jwt/utils'; import { describe, expect, test } from 'vitest'; import { Jwt } from '../jwt'; 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', () => { // Two objects are created separately, the first: { alg: 'HS256', typ: 'JWT' } represents a JWT Header and the second: { sub: '1234567890', name: 'John Doe' } represents a JWT Payload. // These objects are turned into strings with JSON.stringify. The resulting strings are encoded with base64 encoding using Buffer.from(string).toString('base64'). // These base64 encoded strings are concatenated with a period (.) between them, following the structure of a JWT, which is composed of three Base64-URL strings separated by dots (header.payload.signature). // A 'signature' string is added at the end to represent a JWT signature. // So, the jwt variable ends up being a string with the format of a base64Url encoded Header, a period, a base64Url encoded Payload, another period, and a 'signature' string. // It's important to note that the 'signature' here is just a placeholder string and not an actual cryptographic signature generated from the header and payload data. const jwt = `${Buffer.from( JSON.stringify({ alg: 'HS256', typ: 'JWT' }), ).toString('base64')}.${Buffer.from( JSON.stringify({ sub: '1234567890', name: 'John Doe' }), ).toString('base64')}.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 = `${Buffer.from( JSON.stringify({ alg: 'HS256', typ: 'JWT' }), ).toString('base64')}`; 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 }); }); });