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:
co-authored by
Claude Opus 4.6
parent
34a83fb0fc
commit
ca9f5f36e6
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user