UNPKG

accounts

Version:

Tempo Accounts SDK

213 lines (196 loc) 6.47 kB
import { decode, verify } from 'hono/jwt' import { describe, expect, test } from 'vp/test' import { oidcProvider } from './oidcProvider.js' const signingKey = JSON.stringify({ alg: 'Ed25519', crv: 'Ed25519', d: 'tx-s_Aj4ltT_rpY_AIEKexmitq2eyWMkuuIy5JMzmn4', x: 'eZEsf-38KiwfrWnn88cokaJmAoOVgTocC1TndJsz_uQ', kty: 'OKP', }) const publicKey = JSON.stringify({ alg: 'Ed25519', crv: 'Ed25519', x: 'eZEsf-38KiwfrWnn88cokaJmAoOVgTocC1TndJsz_uQ', kty: 'OKP', }) const issuer = 'https://wallet.example.com' const address = '0x0000000000000000000000000000000000001234' function setup(options: Partial<Parameters<typeof oidcProvider>[0]> = {}) { return oidcProvider({ issuer, publicKey, signingKey, authenticate: () => address, getClaims: () => ({ email: 'alice@example.com', email_verified: true }), ...options, }) } describe('token', () => { test('mints a signed id_token with standard + custom claims', async () => { const app = setup() const res = await app.request('/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ audience: 'https://app.example.com', nonce: 'n-123' }), }) expect(res.status).toBe(200) const { idToken } = (await res.json()) as { idToken: string } const verified = (await verify(idToken, JSON.parse(publicKey), 'EdDSA')) as Record< string, unknown > const { iat, exp, ...claims } = verified expect(typeof iat).toBe('number') expect(exp).toBe((iat as number) + 300) expect(claims).toMatchInlineSnapshot(` { "aud": "https://app.example.com", "email": "alice@example.com", "email_verified": true, "iss": "https://wallet.example.com", "nonce": "n-123", "sub": "0x0000000000000000000000000000000000001234", } `) }) test('omits nonce when not supplied', async () => { const app = setup() const res = await app.request('/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ audience: 'https://app.example.com' }), }) const { idToken } = (await res.json()) as { idToken: string } const { payload } = decode(idToken) expect('nonce' in payload).toBe(false) }) test('authenticate throwing rejects with 401', async () => { const app = setup({ authenticate: () => { throw new Error('no session') }, }) const res = await app.request('/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ audience: 'https://app.example.com' }), }) expect(res.status).toBe(401) expect(await res.json()).toMatchInlineSnapshot(` { "error": "no session", } `) }) test('getClaims throwing rejects with 400', async () => { const app = setup({ getClaims: () => { throw new Error('no verified email') }, }) const res = await app.request('/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ audience: 'https://app.example.com' }), }) expect(res.status).toBe(400) expect(await res.json()).toMatchInlineSnapshot(` { "error": "no verified email", } `) }) test('falls back to body subject when no authenticate callback', async () => { const app = setup({ authenticate: undefined }) const res = await app.request('/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ audience: 'https://app.example.com', subject: address }), }) expect(res.status).toBe(200) const { idToken } = (await res.json()) as { idToken: string } const { payload } = decode(idToken) expect(payload.sub).toBe(address) }) test('missing subject (no authenticate, no body subject) rejects with 400', async () => { const app = setup({ authenticate: undefined }) const res = await app.request('/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ audience: 'https://app.example.com' }), }) expect(res.status).toBe(400) expect(await res.json()).toMatchInlineSnapshot(` { "error": "missing subject", } `) }) }) describe('discovery', () => { test('serves an openid-configuration document', async () => { const app = setup() const res = await app.request('/.well-known/openid-configuration') expect(res.status).toBe(200) expect(await res.json()).toMatchInlineSnapshot(` { "claims_supported": [ "iss", "aud", "sub", "iat", "exp", "nonce", ], "id_token_signing_alg_values_supported": [ "EdDSA", ], "issuer": "https://wallet.example.com", "jwks_uri": "https://wallet.example.com/.well-known/jwks.json", "response_types_supported": [ "id_token", ], "subject_types_supported": [ "public", ], } `) }) test('appends deployment claims via claimsSupported', async () => { const app = setup({ claimsSupported: ['iss', 'aud', 'sub', 'iat', 'exp', 'nonce', 'email', 'email_verified'], }) const res = await app.request('/.well-known/openid-configuration') const doc = (await res.json()) as { claims_supported: string[] } expect(doc.claims_supported).toContain('email') expect(doc.claims_supported).toContain('email_verified') }) test('honors a custom jwksUri', async () => { const app = setup({ jwksUri: 'https://keys.example.com/jwks.json' }) const res = await app.request('/.well-known/openid-configuration') const doc = (await res.json()) as { jwks_uri: string } expect(doc.jwks_uri).toBe('https://keys.example.com/jwks.json') }) }) describe('jwks', () => { test('serves the public signing key', async () => { const app = setup() const res = await app.request('/.well-known/jwks.json') expect(res.status).toBe(200) expect(await res.json()).toMatchInlineSnapshot(` { "keys": [ { "alg": "EdDSA", "crv": "Ed25519", "kid": "oidc-1", "kty": "OKP", "use": "sig", "x": "eZEsf-38KiwfrWnn88cokaJmAoOVgTocC1TndJsz_uQ", }, ], } `) }) })