Skip to content

Commit d1535cf

Browse files
feat: refactor and fix based on integration tests
1 parent 7e793c6 commit d1535cf

File tree

9 files changed

+107
-94
lines changed

9 files changed

+107
-94
lines changed

src/adapters/web-socket-adapter.ts

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ContextMetadataKey, EventKinds } from '../constants/base'
21
import cluster from 'cluster'
2+
import { ContextMetadataKey } from '../constants/base'
33
import { EventEmitter } from 'stream'
44
import { IncomingMessage as IncomingHttpMessage } from 'http'
55
import { randomBytes } from 'crypto'
@@ -22,7 +22,6 @@ import { messageSchema } from '../schemas/message-schema'
2222
import { Settings } from '../@types/settings'
2323
import { SocketAddress } from 'net'
2424

25-
2625
const debug = createLogger('web-socket-adapter')
2726
const debugHeartbeat = debug.extend('heartbeat')
2827

@@ -99,7 +98,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
9998
this.subscriptions.set(subscriptionId, filters)
10099
}
101100

102-
public setNewAuthChallenge() {
101+
public setNewAuthChallenge(): string {
103102
const challenge = randomBytes(16).toString('hex')
104103
this.authChallenge = {
105104
createdAt: new Date(),
@@ -182,33 +181,8 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
182181
const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf8')))
183182
debug('recv client msg: %o', message)
184183

185-
if (
186-
!this.authenticated
187-
&& message[1].kind !== EventKinds.AUTH
188-
&& this.settings().authentication.enabled
189-
) {
190-
switch(message[0]) {
191-
case MessageType.REQ: {
192-
const challenge = this.setNewAuthChallenge()
193-
this.sendMessage(createAuthMessage(challenge))
194-
return
195-
}
196-
197-
case MessageType.EVENT: {
198-
const challenge = this.setNewAuthChallenge()
199-
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
200-
this.sendMessage(createAuthMessage(challenge))
201-
return
202-
}
203-
204-
default: {
205-
const challenge = this.setNewAuthChallenge()
206-
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
207-
this.sendMessage(createAuthMessage(challenge))
208-
return
209-
}
210-
}
211-
}
184+
const requiresAuthentication = this.isAuthenticationRequired(message)
185+
if (requiresAuthentication) return
212186

213187
message[ContextMetadataKey] = {
214188
remoteAddress: this.clientAddress,
@@ -322,4 +296,37 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
322296
this.removeAllListeners()
323297
this.client.removeAllListeners()
324298
}
299+
300+
private isAuthenticationRequired(message): boolean {
301+
if (
302+
!this.authenticated
303+
&& message[0] !== MessageType.AUTH
304+
&& message[0] !== MessageType.CLOSE
305+
&& this.settings().authentication.enabled
306+
) {
307+
switch(message[0]) {
308+
case MessageType.REQ: {
309+
const challenge = this.setNewAuthChallenge()
310+
this.sendMessage(createAuthMessage(challenge))
311+
return true
312+
}
313+
314+
case MessageType.EVENT: {
315+
const challenge = this.setNewAuthChallenge()
316+
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
317+
this.sendMessage(createAuthMessage(challenge))
318+
return true
319+
}
320+
321+
default: {
322+
const challenge = this.setNewAuthChallenge()
323+
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
324+
this.sendMessage(createAuthMessage(challenge))
325+
return true
326+
}
327+
}
328+
}
329+
330+
return false
331+
}
325332
}

src/factories/message-handler-factory.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { IEventRepository, IUserRepository } from '../@types/repositories'
22
import { IncomingMessage, MessageType } from '../@types/messages'
3-
import { isDelegatedEvent, isSignedAuthEvent } from '../utils/event'
43
import { AuthEventMessageHandler } from '../handlers/auth-event-message-handler'
54
import { createSettings } from './settings-factory'
65
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
76
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
87
import { EventMessageHandler } from '../handlers/event-message-handler'
98
import { eventStrategyFactory } from './event-strategy-factory'
9+
import { isDelegatedEvent } from '../utils/event'
1010
import { IWebSocketAdapter } from '../@types/adapters'
1111
import { signedAuthEventStrategyFactory } from './auth-event-strategy-factory'
1212
import { slidingWindowRateLimiterFactory } from './rate-limiter-factory'
@@ -30,16 +30,6 @@ export const messageHandlerFactory = (
3030
)
3131
}
3232

33-
if (isSignedAuthEvent(message[1])) {
34-
return new AuthEventMessageHandler(
35-
adapter,
36-
signedAuthEventStrategyFactory(),
37-
userRepository,
38-
createSettings,
39-
slidingWindowRateLimiterFactory,
40-
)
41-
}
42-
4333
return new EventMessageHandler(
4434
adapter,
4535
eventStrategyFactory(eventRepository),
@@ -50,6 +40,15 @@ export const messageHandlerFactory = (
5040
}
5141
case MessageType.REQ:
5242
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
43+
case MessageType.AUTH: {
44+
return new AuthEventMessageHandler(
45+
adapter,
46+
signedAuthEventStrategyFactory(),
47+
userRepository,
48+
createSettings,
49+
slidingWindowRateLimiterFactory,
50+
)
51+
}
5352
case MessageType.CLOSE:
5453
return new UnsubscribeMessageHandler(adapter,)
5554
default:

src/handlers/event-strategies/auth-event-strategy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ export class SignedAuthEventStrategy implements IEventStrategy<Event, Promise<vo
1717
public async execute(event: Event): Promise<void> {
1818
debug('received signedAuth event: %o', event)
1919
const { challenge, createdAt } = this.webSocket.getClientAuthChallengeData()
20-
const verified = await isValidSignedAuthEvent(event, challenge)
20+
const verified = isValidSignedAuthEvent(event, challenge)
2121

22-
const timeIsWithinBounds = (createdAt.getTime() + permittedChallengeResponseTimeDelayMs) < Date.now()
22+
const timeIsWithinBounds = (createdAt.getTime() + permittedChallengeResponseTimeDelayMs) > Date.now()
2323

24+
debug('banana', timeIsWithinBounds, verified)
2425
if (verified && timeIsWithinBounds) {
2526
this.webSocket.setClientToAuthenticated()
2627
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'authentication: succeeded'))
2728
return
2829
}
2930

30-
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'authentication: failed'))
31+
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'authentication: failed'))
3132
}
3233
}

src/utils/event.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,13 @@ export const isSignedAuthEvent = (event: Event): boolean => {
140140
return false
141141
}
142142

143-
export const isValidSignedAuthEvent = async (event: Event, challenge: string): Promise<boolean> => {
143+
export const isValidSignedAuthEvent = (event: Event, challenge: string): boolean => {
144144
const signedAuthEvent = isSignedAuthEvent(event)
145145

146146
if (signedAuthEvent) {
147-
const sig = event.tags.find(tag => tag.length >= 2 && tag[0] === EventTags.Challenge)
147+
const tag = event.tags.find(tag => tag.length >= 2 && tag[0] === EventTags.Challenge)
148148

149-
return secp256k1.schnorr.verify(sig[1], challenge, event.pubkey)
149+
return tag[1] === challenge
150150
}
151151

152152
return false
@@ -327,7 +327,7 @@ export const getEventExpiration = (event: Event): number | undefined => {
327327
const expirationTime = Number(rawExpirationTime)
328328
if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) {
329329
return expirationTime
330-
}
330+
}
331331
}
332332

333333
export const getEventProofOfWork = (eventId: EventId): number => {

test/integration/features/helpers.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,35 @@ export async function sendEvent(ws: WebSocket, event: Event, successful = true)
132132
})
133133
}
134134

135+
136+
export async function sendAuthMessage(ws: WebSocket, event: Event, successful = true) {
137+
return new Promise<OutgoingMessage>((resolve, reject) => {
138+
const observable = streams.get(ws) as Observable<OutgoingMessage>
139+
140+
const sub = observable.subscribe((message: OutgoingMessage) => {
141+
if (message[0] === MessageType.OK && message[1] === event.id) {
142+
if (message[2] === successful) {
143+
sub.unsubscribe()
144+
resolve(message)
145+
} else {
146+
sub.unsubscribe()
147+
reject(new Error(message[3]))
148+
}
149+
} else if (message[0] === MessageType.NOTICE) {
150+
sub.unsubscribe()
151+
reject(new Error(message[1]))
152+
}
153+
})
154+
155+
ws.send(JSON.stringify(['AUTH', event]), (err) => {
156+
if (err) {
157+
sub.unsubscribe()
158+
reject(err)
159+
}
160+
})
161+
})
162+
}
163+
135164
export async function waitForNextEvent(ws: WebSocket, subscription: string, content?: string): Promise<Event> {
136165
return new Promise((resolve, reject) => {
137166
const observable = streams.get(ws) as Observable<OutgoingMessage>

test/integration/features/nip-42/nip-42.feature

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ Feature: NIP-42
1111
When Alice sends a text_note event with content "hello nostr" unsuccessfully
1212
And Alice receives an authentication challenge
1313
Then Alice sends a signed_challenge_event
14+
15+
Scenario: Alice authenticates and sends an event
16+
Given someone called Alice
17+
And the relay requires the client to authenticate
18+
When Alice sends a text_note event with content "hello nostr" unsuccessfully
19+
And Alice receives an authentication challenge
20+
Then Alice sends a signed_challenge_event
21+
Then Alice sends a text_note event with content "hello nostr" successfully

test/integration/features/nip-42/nip-42.feature.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
import chai from 'chai'
77
import sinonChai from 'sinon-chai'
88

9-
import { createEvent, sendEvent, waitForAuth } from '../helpers'
10-
import { EventKinds } from '../../../../src/constants/base'
9+
import { createEvent, sendAuthMessage, waitForAuth } from '../helpers'
10+
import { EventKinds, EventTags } from '../../../../src/constants/base'
1111
import { SettingsStatic } from '../../../../src/utils/settings'
12+
import { Tag } from '../../../../src/@types/base'
1213
import { WebSocket } from 'ws'
1314

1415
chai.use(sinonChai)
@@ -31,8 +32,13 @@ Then(/(\w+) sends a signed_challenge_event/, async function (name: string) {
3132
const challenge = this.parameters.challenges[name].pop()
3233
const ws = this.parameters.clients[name] as WebSocket
3334
const { pubkey, privkey } = this.parameters.identities[name]
35+
const tags: Tag[] = [
36+
[EventTags.Relay, 'ws://yoda.test.relay'],
37+
[EventTags.Challenge, challenge],
38+
]
39+
40+
const event: any = await createEvent({ pubkey, kind: EventKinds.AUTH, tags }, privkey)
41+
await sendAuthMessage(ws, event, true)
3442

35-
const event: any = await createEvent({ pubkey, kind: EventKinds.AUTH, content: challenge }, privkey)
36-
await sendEvent(ws, event, true)
3743
this.parameters.events[name].push(event)
3844
})

test/integration/features/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ BeforeAll({ timeout: 1000 }, async function () {
5151
assocPath( ['limits', 'event', 'rateLimits'], []),
5252
assocPath( ['limits', 'invoice', 'rateLimits'], []),
5353
assocPath( ['limits', 'connection', 'rateLimits'], []),
54+
assocPath( ['info', 'relay_url'], 'ws://yoda.test.relay'),
5455
)(settings) as any
5556

5657
worker = workerFactory()

test/unit/utils/event.spec.ts

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -609,65 +609,27 @@ describe('NIP-40', () => {
609609

610610
describe('isValidSignedAuthEvent', async () => {
611611
it('returns true if event is valid client auth event', async () => {
612-
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
613-
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
614-
615612
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
616613
event.kind = EventKinds.AUTH
617614
event.tags = [
618615
[EventTags.Relay, 'wss://eden.nostr.land'],
619-
[EventTags.Challenge, signedHexChallenge],
616+
[EventTags.Challenge, 'test'],
620617
]
621618

622-
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(true)
619+
expect(await isValidSignedAuthEvent(event, 'test')).to.equal(true)
623620
})
624621
})
625622

626623
describe('isValidSignedAuthEvent', async () => {
627624
it('returns false if challenge is different', async () => {
628-
const challengeHex = '6468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
629-
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
630-
631-
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
632-
event.kind = EventKinds.AUTH
633-
event.tags = [
634-
[EventTags.Relay, 'wss://eden.nostr.land'],
635-
[EventTags.Challenge, signedHexChallenge],
636-
]
637-
638-
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
639-
})
640-
})
641-
642-
describe('isValidSignedAuthEvent', async () => {
643-
it('returns false if signed challenge is incorrect', async () => {
644-
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
645-
const signedHexChallenge = '0161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
646-
647625
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
648626
event.kind = EventKinds.AUTH
649627
event.tags = [
650628
[EventTags.Relay, 'wss://eden.nostr.land'],
651-
[EventTags.Challenge, signedHexChallenge],
652-
]
653-
654-
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
655-
})
656-
})
657-
658-
describe('isValidSignedAuthEvent', async () => {
659-
it('returns true if event is valid client auth event', async () => {
660-
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
661-
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
662-
663-
event.pubkey = 'a9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
664-
event.kind = EventKinds.AUTH
665-
event.tags = [
666-
[EventTags.Relay, 'wss://eden.nostr.land'],
667-
[EventTags.Challenge, signedHexChallenge],
629+
[EventTags.Challenge, 'incorrectChallenge'],
668630
]
669631

670-
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
632+
expect(isValidSignedAuthEvent(event, 'challenge')).to.equal(false)
671633
})
672634
})
673635
})

0 commit comments

Comments
 (0)