test: add NIP-98 edge cases — replay, future clock drift, URL path mismatch

Documents finding: no replay protection in NIP-98 verification.
Token replay within 120s window succeeds (mitigated by JWT issuance being idempotent).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 23:44:56 +00:00
co-authored by Claude Opus 4.6
parent 34a83fb0fc
commit ca9f5f36e6
+40
View File
@@ -120,4 +120,44 @@ describe('verifyNip98Token', () => {
expect(result.valid).toBe(false)
expect(result.error).toContain('Wrong event kind')
})
it('token replay: same valid token accepted twice within 120s (no replay protection)', () => {
// FINDING: NIP-98 spec doesn't mandate replay protection.
// The server verifies signature and timestamp but does not track seen event IDs.
// A valid token can be replayed within its 120s window.
// Mitigation: JWT is issued on first use, so replay only re-issues JWT for same pubkey.
const sk = generateSecretKey()
const event = createNip98Event(sk, 'https://example.com/api/auth', 'POST')
const header = toAuthHeader(event)
const result1 = verifyNip98Token(header, '/api/auth', 'POST')
const result2 = verifyNip98Token(header, '/api/auth', 'POST')
expect(result1.valid).toBe(true)
expect(result2.valid).toBe(true) // No replay protection — documenting behavior
})
it('rejects clock drift >120s in the future', () => {
const sk = generateSecretKey()
const event = finalizeEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000) + 200, // 200s in the future
tags: [['u', 'https://example.com/api/auth'], ['method', 'POST']],
content: '',
}, sk)
const header = toAuthHeader(event)
const result = verifyNip98Token(header, '/api/auth', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('expired')
})
it('rejects URL path mismatch', () => {
const sk = generateSecretKey()
const event = createNip98Event(sk, 'https://example.com/api/auth/login', 'POST')
const header = toAuthHeader(event)
const result = verifyNip98Token(header, '/api/auth/session', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('URL path mismatch')
})
})