accounts
Version:
Tempo Accounts SDK
211 lines (180 loc) • 7.1 kB
text/typescript
import { afterEach, describe, expect, test, vi } from 'vp/test'
import * as WebAuthnCeremony from './WebAuthnCeremony.js'
afterEach(() => vi.unstubAllGlobals())
describe('from', () => {
test('default: returns the ceremony', () => {
const ceremony = WebAuthnCeremony.from({
getRegistrationOptions: async () => ({ options: {} as any }),
verifyRegistration: async () => ({ credentialId: 'cred-1', publicKey: '0x1234' }),
getAuthenticationOptions: async () => ({ options: {} as any }),
verifyAuthentication: async () => ({ credentialId: 'cred-1', publicKey: '0x1234' }),
})
expect(ceremony).toBeDefined()
expect(typeof ceremony.getRegistrationOptions).toMatchInlineSnapshot(`"function"`)
expect(typeof ceremony.verifyRegistration).toMatchInlineSnapshot(`"function"`)
expect(typeof ceremony.getAuthenticationOptions).toMatchInlineSnapshot(`"function"`)
expect(typeof ceremony.verifyAuthentication).toMatchInlineSnapshot(`"function"`)
})
})
describe('local', () => {
test('default: creates a ceremony', () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
expect(ceremony).toBeDefined()
})
test('behavior: getRegistrationOptions returns serialized options', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
const { options } = await ceremony.getRegistrationOptions({ name: 'Test' })
expect(options.publicKey).toBeDefined()
expect(options.publicKey!.rp.id).toMatchInlineSnapshot(`"example.com"`)
expect(options.publicKey!.rp.name).toMatchInlineSnapshot(`"example.com"`)
expect(typeof options.publicKey!.challenge).toMatchInlineSnapshot(`"string"`)
expect(options.publicKey!.authenticatorSelection).toBeDefined()
})
test('behavior: getAuthenticationOptions returns serialized options', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
const { options } = await ceremony.getAuthenticationOptions()
expect(options.publicKey).toBeDefined()
expect(options.publicKey!.rpId).toMatchInlineSnapshot(`"example.com"`)
expect(typeof options.publicKey!.challenge).toMatchInlineSnapshot(`"string"`)
})
test('behavior: getAuthenticationOptions restricts authentication to multiple credentials', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
const { options } = await ceremony.getAuthenticationOptions({
credentialId: ['Y3JlZC0x', 'Y3JlZC0y'],
})
expect(options.publicKey?.allowCredentials).toMatchInlineSnapshot(`
[
{
"id": "Y3JlZC0x",
"type": "public-key",
},
{
"id": "Y3JlZC0y",
"type": "public-key",
},
]
`)
})
test('behavior: verifyRegistration stores credential and returns publicKey', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
const result = await ceremony.verifyRegistration({
attestationObject: 'mock',
clientDataJSON: 'mock',
id: 'cred-1',
publicKey: '0xabcd',
raw: {
id: 'cred-1',
type: 'public-key',
authenticatorAttachment: null,
rawId: 'cred-1',
response: { clientDataJSON: 'mock' },
},
})
expect(result).toMatchInlineSnapshot(`
{
"credentialId": "cred-1",
"publicKey": "0xabcd",
}
`)
})
test('behavior: verifyAuthentication returns stored publicKey', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
// Register first to store the credential
await ceremony.verifyRegistration({
attestationObject: 'mock',
clientDataJSON: 'mock',
id: 'cred-1',
publicKey: '0xabcd',
raw: {
id: 'cred-1',
type: 'public-key',
authenticatorAttachment: null,
rawId: 'cred-1',
response: { clientDataJSON: 'mock' },
},
})
const result = await ceremony.verifyAuthentication({
id: 'cred-1',
metadata: {
authenticatorData: '0x00',
clientDataJSON: '{}',
},
raw: {
id: 'cred-1',
type: 'public-key',
authenticatorAttachment: null,
rawId: 'cred-1',
response: { clientDataJSON: 'mock' },
},
signature: '0x00',
})
expect(result).toMatchInlineSnapshot(`
{
"credentialId": "cred-1",
"publicKey": "0xabcd",
}
`)
})
test('error: verifyAuthentication throws for unknown credential', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
await expect(
ceremony.verifyAuthentication({
id: 'unknown',
metadata: {
authenticatorData: '0x00',
clientDataJSON: '{}',
},
raw: {
id: 'unknown',
type: 'public-key',
authenticatorAttachment: null,
rawId: 'unknown',
response: { clientDataJSON: 'mock' },
},
signature: '0x00',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Unknown credential: unknown]`)
})
test('behavior: each call to getRegistrationOptions generates a unique challenge', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
const { options: a } = await ceremony.getRegistrationOptions({ name: 'Test' })
const { options: b } = await ceremony.getRegistrationOptions({ name: 'Test' })
expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
})
test('behavior: each call to getAuthenticationOptions generates a unique challenge', async () => {
const ceremony = WebAuthnCeremony.local({ rpId: 'example.com' })
const { options: a } = await ceremony.getAuthenticationOptions()
const { options: b } = await ceremony.getAuthenticationOptions()
expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
})
})
describe('server', () => {
test('uses the default credential mode when omitted', async () => {
let request: RequestInit | undefined
vi.stubGlobal('fetch', async (_input: string | URL, init?: RequestInit) => {
request = init
return Response.json({ options: {} })
})
const ceremony = WebAuthnCeremony.server({ url: 'https://wallet.example.com/webauthn' })
await ceremony.getRegistrationOptions({ name: 'Test' })
expect(request?.credentials).toMatchInlineSnapshot(`undefined`)
})
test('includes configured credentials in ceremony requests', async () => {
let request: RequestInit | undefined
vi.stubGlobal('fetch', async (_input: string | URL, init?: RequestInit) => {
request = init
return Response.json({ options: {} })
})
const ceremony = WebAuthnCeremony.server({
credentials: 'include',
url: 'https://wallet.example.com/webauthn',
})
await ceremony.getRegistrationOptions({ name: 'Test' })
expect({ credentials: request?.credentials, method: request?.method }).toMatchInlineSnapshot(`
{
"credentials": "include",
"method": "POST",
}
`)
})
})