1use metrics::{counter, describe_counter};
2use nostr_relay::db::now;
3use nostr_relay::{
4 message::{ClientMessage, IncomingMessage, OutgoingMessage},
5 setting::SettingWrapper,
6 Extension, ExtensionMessageResult, List, Session,
7};
8use serde::Deserialize;
9use uuid::Uuid;
10
11#[derive(Deserialize, Default, Debug)]
12#[serde(default)]
13pub struct Permission {
14 pub ip_whitelist: Option<List>,
15 pub pubkey_whitelist: Option<List>,
16 pub ip_blacklist: Option<List>,
17 pub pubkey_blacklist: Option<List>,
18 pub event_pubkey_whitelist: Option<List>,
19 pub event_pubkey_blacklist: Option<List>,
20 pub allow_mentioning_whitelisted_pubkeys: bool,
21}
22
23#[derive(Deserialize, Default, Debug)]
24#[serde(default)]
25pub struct AuthSetting {
26 pub enabled: bool,
27 pub req: Option<Permission>,
29 pub event: Option<Permission>,
31}
32
33#[derive(Default, Debug)]
34pub struct Auth {
35 setting: AuthSetting,
36}
37
38pub enum AuthState {
39 Challenge(String),
41 Pubkey(String),
43}
44
45impl AuthState {
46 pub fn authed(&self) -> bool {
47 matches!(self, Self::Pubkey(_))
48 }
49
50 pub fn pubkey(&self) -> Option<&String> {
51 match self {
52 Self::Pubkey(p) => Some(p),
53 Self::Challenge(_) => None,
54 }
55 }
56}
57
58impl Auth {
59 pub fn new() -> Self {
60 describe_counter!(
61 "nostr_relay_auth_unauthorized",
62 "The total count of unauthorized messages"
63 );
64 Self {
65 setting: AuthSetting::default(),
66 }
67 }
68
69 pub fn verify_permission(
70 permission: Option<&Permission>,
71 pubkey: Option<&String>,
72 event_pubkey: Option<&String>,
73 event_tags: Option<&Vec<Vec<String>>>,
74 ip: &String,
75 ) -> Result<(), &'static str> {
76 if let Some(permission) = permission {
77 if let Some(list) = &permission.ip_whitelist {
78 if !list.contains(ip) {
79 return Err("ip not in whitelist");
80 }
81 }
82 if let Some(list) = &permission.ip_blacklist {
83 if list.contains(ip) {
84 return Err("ip in blacklist");
85 }
86 }
87
88 if let Some(pubkey) = event_pubkey {
89 if let Some(list) = &permission.event_pubkey_whitelist {
90 let whitelisted_pubkey_is_mentioned =
91 if permission.allow_mentioning_whitelisted_pubkeys {
92 let mut event_mentioned_pubkeys = event_tags
93 .iter()
94 .flat_map(|i| i.iter())
95 .filter(|t| t.len() > 1 && t[0] == "p")
96 .map(|t| &t[1]);
97 event_mentioned_pubkeys.any(|i| list.contains(i))
98 } else {
99 false
100 };
101 if !whitelisted_pubkey_is_mentioned && !list.contains(pubkey) {
102 return Err("event author pubkey not in whitelist");
103 }
104 }
105 if let Some(list) = &permission.event_pubkey_blacklist {
106 if list.contains(pubkey) {
107 return Err("event author pubkey in blacklist");
108 }
109 }
110 }
111
112 if let Some(list) = &permission.pubkey_whitelist {
113 if let Some(pubkey) = pubkey {
114 if !list.contains(pubkey) {
115 return Err("pubkey not in whitelist");
116 }
117 } else {
118 return Err("NIP-42 auth required");
119 }
120 }
121 if let Some(list) = &permission.pubkey_blacklist {
122 if let Some(pubkey) = pubkey {
123 if list.contains(pubkey) {
124 return Err("pubkey in blacklist");
125 }
126 } else {
127 return Err("NIP-42 auth required");
128 }
129 }
130 }
131 Ok(())
132 }
133}
134
135impl Extension for Auth {
136 fn name(&self) -> &'static str {
137 "auth"
138 }
139
140 fn setting(&mut self, setting: &SettingWrapper) {
141 let mut w = setting.write();
142 self.setting = w.parse_extension(self.name());
143 if self.setting.enabled {
144 w.add_nip(42);
145 }
146 }
147
148 fn connected(&self, session: &mut Session, ctx: &mut <Session as actix::Actor>::Context) {
149 if self.setting.enabled {
150 let uuid = Uuid::new_v4().to_string();
151 let state = AuthState::Challenge(uuid.clone());
152 session.set(state);
153 ctx.text(format!(r#"["AUTH", "{uuid}"]"#));
154 }
155 }
156
157 fn message(
158 &self,
159 msg: ClientMessage,
160 session: &mut Session,
161 _ctx: &mut <Session as actix::Actor>::Context,
162 ) -> ExtensionMessageResult {
163 let mut msg = msg;
164
165 if self.setting.enabled {
166 let state = session.get::<AuthState>();
167 msg.nip70_checked = true;
168 match &msg.msg {
169 IncomingMessage::Auth(event) => {
170 if let Some(AuthState::Challenge(challenge)) = state {
171 if let Err(err) = event.validate(now(), 0, 0) {
172 return OutgoingMessage::ok(
173 &event.id_str(),
174 false,
175 &format!("auth-required: {}", err),
176 )
177 .into();
178 } else if event.kind() == 22242 {
179 for tag in event.tags() {
180 if tag.len() > 1 && tag[0] == "challenge" && &tag[1] == challenge {
181 session.set(AuthState::Pubkey(event.pubkey_str()));
182 return OutgoingMessage::ok(&event.id_str(), true, "").into();
183 }
184 }
185 }
186 }
187 return OutgoingMessage::ok(
188 &event.id_str(),
189 false,
190 "auth-required: need reconnect",
191 )
192 .into();
193 }
194 IncomingMessage::Event(event) => {
195 if let Err(err) = Self::verify_permission(
196 self.setting.event.as_ref(),
197 state.and_then(|s| s.pubkey()),
198 Some(&event.pubkey_str()),
199 Some(event.tags()),
200 session.ip(),
201 ) {
202 counter!("nostr_relay_auth_unauthorized", "command" => "EVENT", "reason" => err).increment(1);
203 return OutgoingMessage::ok(
204 &event.id_str(),
205 false,
206 &format!("auth-required: {}", err),
207 )
208 .into();
209 } else {
210 for tag in event.tags() {
212 if tag.len() == 1 && tag[0] == "-" {
213 if let Some(AuthState::Pubkey(pubkey)) = state {
214 if pubkey != &event.pubkey_str() {
215 return OutgoingMessage::ok(
216 &event.id_str(),
217 false,
218 "auth-required: this event may only be published by its author",
219 )
220 .into();
221 }
222 } else {
223 return OutgoingMessage::ok(
224 &event.id_str(),
225 false,
226 "auth-required: this event require authorization",
227 )
228 .into();
229 }
230 break;
231 }
232 }
233 }
234 }
235 IncomingMessage::Req(sub) | IncomingMessage::Count(sub) => {
236 if let Err(err) = Self::verify_permission(
237 self.setting.req.as_ref(),
238 state.and_then(|s| s.pubkey()),
239 None,
240 None,
241 session.ip(),
242 ) {
243 counter!("nostr_relay_auth_unauthorized", "command" => "REQ", "reason" => err).increment(1);
244 let msg = format!("auth-required: {}", err);
245 return OutgoingMessage::closed(&sub.id, &msg).into();
246 }
247 }
248 _ => {}
249 }
250 }
251 ExtensionMessageResult::Continue(msg)
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::create_test_app;
259 use actix_web::web;
260 use actix_web_actors::ws;
261 use anyhow::Result;
262 use futures_util::{SinkExt as _, StreamExt as _};
263 use nostr_relay::create_web_app;
264 use nostr_relay::db::{
265 secp256k1::{rand::thread_rng, Keypair, XOnlyPublicKey},
266 Event,
267 };
268
269 fn parse_text<T: serde::de::DeserializeOwned>(frame: &ws::Frame) -> Result<T> {
270 if let ws::Frame::Text(text) = &frame {
271 let data: T = serde_json::from_slice(text)?;
272 Ok(data)
273 } else {
274 Err(nostr_relay::Error::Message("invalid frame type".to_string()).into())
275 }
276 }
277
278 #[test]
279 fn verify() -> Result<()> {
280 assert!(Auth::verify_permission(
281 Some(&Permission {
282 ip_whitelist: Some(vec!["127.0.0.1".to_string()].into()),
283 ..Default::default()
284 }),
285 None,
286 None,
287 None,
288 &"127.0.0.1".to_owned()
289 )
290 .is_ok());
291 assert!(Auth::verify_permission(
292 Some(&Permission {
293 ip_whitelist: Some(vec!["127.0.0.1".to_string()].into()),
294 ..Default::default()
295 }),
296 None,
297 None,
298 None,
299 &"127.0.0.2".to_owned()
300 )
301 .is_err());
302
303 assert!(Auth::verify_permission(
304 Some(&Permission {
305 ip_blacklist: Some(vec!["127.0.0.1".to_string()].into()),
306 ..Default::default()
307 }),
308 None,
309 None,
310 None,
311 &"127.0.0.1".to_owned()
312 )
313 .is_err());
314 assert!(Auth::verify_permission(
315 Some(&Permission {
316 ip_blacklist: Some(vec!["127.0.0.1".to_string()].into()),
317 ..Default::default()
318 }),
319 None,
320 None,
321 None,
322 &"127.0.0.2".to_owned()
323 )
324 .is_ok());
325
326 assert!(Auth::verify_permission(
327 Some(&Permission {
328 pubkey_whitelist: Some(vec!["xx".to_string()].into()),
329 ..Default::default()
330 }),
331 Some(&"xx".to_owned()),
332 None,
333 None,
334 &"127.0.0.1".to_owned()
335 )
336 .is_ok());
337 assert!(Auth::verify_permission(
338 Some(&Permission {
339 pubkey_whitelist: Some(vec!["xx".to_string()].into()),
340 ..Default::default()
341 }),
342 Some(&"xxxx".to_owned()),
343 None,
344 None,
345 &"127.0.0.1".to_owned()
346 )
347 .is_err());
348
349 assert!(Auth::verify_permission(
350 Some(&Permission {
351 pubkey_blacklist: Some(vec!["xx".to_string()].into()),
352 ..Default::default()
353 }),
354 Some(&"xx".to_owned()),
355 None,
356 None,
357 &"127.0.0.1".to_owned()
358 )
359 .is_err());
360 assert!(Auth::verify_permission(
361 Some(&Permission {
362 pubkey_blacklist: Some(vec!["xx".to_string()].into()),
363 ..Default::default()
364 }),
365 Some(&"xxxx".to_owned()),
366 None,
367 None,
368 &"127.0.0.1".to_owned()
369 )
370 .is_ok());
371
372 assert!(Auth::verify_permission(
373 Some(&Permission {
374 event_pubkey_whitelist: Some(vec!["xx".to_string()].into()),
375 ..Default::default()
376 }),
377 None,
378 Some(&"xx".to_owned()),
379 None,
380 &"127.0.0.1".to_owned()
381 )
382 .is_ok());
383 assert!(Auth::verify_permission(
384 Some(&Permission {
385 event_pubkey_whitelist: Some(vec!["xx".to_string()].into()),
386 ..Default::default()
387 }),
388 None,
389 Some(&"xxxx".to_owned()),
390 None,
391 &"127.0.0.1".to_owned()
392 )
393 .is_err());
394
395 assert!(Auth::verify_permission(
396 Some(&Permission {
397 event_pubkey_blacklist: Some(vec!["xx".to_string()].into()),
398 ..Default::default()
399 }),
400 None,
401 Some(&"xx".to_owned()),
402 None,
403 &"127.0.0.1".to_owned()
404 )
405 .is_err());
406 assert!(Auth::verify_permission(
407 Some(&Permission {
408 event_pubkey_blacklist: Some(vec!["xx".to_string()].into()),
409 ..Default::default()
410 }),
411 None,
412 Some(&"xxxx".to_owned()),
413 None,
414 &"127.0.0.1".to_owned()
415 )
416 .is_ok());
417 Ok(())
418 }
419
420 #[actix_rt::test]
421 async fn auth() -> Result<()> {
422 let mut rng = thread_rng();
423 let key_pair = Keypair::new_global(&mut rng);
424
425 let app = create_test_app("auth")?;
426 {
427 let mut w = app.setting.write();
428 w.extra = serde_json::from_str(
429 r#"{
430 "auth": {
431 "enabled": true
432 }
433 }"#,
434 )?;
435 }
436 let app = app.add_extension(Auth::new());
437 let app = web::Data::new(app);
438
439 let mut srv = actix_test::start(move || create_web_app(app.clone()));
440
441 let mut framed = srv.ws_at("/").await.unwrap();
443
444 let item = framed.next().await.unwrap()?;
445 assert!(matches!(item, ws::Frame::Text(_)));
446 let state: (String, String) = parse_text(&item)?;
447 assert_eq!(state.0, "AUTH");
448
449 let event = Event::create(&key_pair, 0, 1, vec![], "".to_owned())?;
450 let event = Event::new(
451 event.id().clone(),
452 event.pubkey().clone(),
453 event.created_at(),
454 2,
455 vec![],
456 "".to_owned(),
457 event.sig().clone(),
458 )?;
459 framed
460 .send(ws::Message::Text(
461 format!(r#"["AUTH", {}]"#, event.to_string()).into(),
462 ))
463 .await?;
464 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
465 assert!(notice.3.contains("invalid"));
466
467 let event = Event::create(&key_pair, now(), 22242, vec![], "".to_owned())?;
468 framed
469 .send(ws::Message::Text(
470 format!(r#"["AUTH", {}]"#, event.to_string()).into(),
471 ))
472 .await?;
473 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
474 assert!(notice.3.contains("need"));
475
476 let event = Event::create(
477 &key_pair,
478 now(),
479 22242,
480 vec![vec!["challenge".to_owned(), state.1.clone()]],
481 "".to_owned(),
482 )?;
483 framed
484 .send(ws::Message::Text(
485 format!(r#"["AUTH", {}]"#, event.to_string()).into(),
486 ))
487 .await?;
488 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
489 assert!(notice.2);
490
491 framed
492 .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
493 .await?;
494 let item = framed.next().await.unwrap()?;
495 assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
496 Ok(())
497 }
498
499 #[actix_rt::test]
500 async fn pubkey_whitelist() -> Result<()> {
501 let mut rng = thread_rng();
502 let key_pair = Keypair::new_global(&mut rng);
503 let pubkey = XOnlyPublicKey::from_keypair(&key_pair).0;
504
505 let app = create_test_app("auth-whitelist")?;
506 {
507 let mut w = app.setting.write();
508 w.extra = serde_json::from_str(&format!(
509 r#"{{
510 "auth": {{
511 "enabled": true,
512 "req": {{
513 "pubkey_whitelist": ["{}"]
514 }},
515 "event": {{
516 "pubkey_whitelist": ["{}"]
517 }}
518 }}
519 }}"#,
520 pubkey.to_string(),
521 pubkey.to_string()
522 ))?;
523 }
524 let app = app.add_extension(Auth::new());
525 let app = web::Data::new(app);
526
527 let mut srv = actix_test::start(move || create_web_app(app.clone()));
528
529 let mut framed = srv.ws_at("/").await.unwrap();
531
532 let item = framed.next().await.unwrap()?;
533 assert!(matches!(item, ws::Frame::Text(_)));
534 let state: (String, String) = parse_text(&item)?;
535 assert_eq!(state.0, "AUTH");
536
537 framed
539 .send(ws::Message::Text(r#"["REQ", "1", {}]"#.into()))
540 .await?;
541
542 let notice: (String, String, String) = parse_text(&framed.next().await.unwrap()?)?;
543 assert_eq!(notice.0, "CLOSED");
544 assert!(notice.2.contains("auth-required"));
545
546 let event = Event::create(
547 &key_pair,
548 now(),
549 22242,
550 vec![vec!["challenge".to_owned(), state.1.clone()]],
551 "".to_owned(),
552 )?;
553 framed
554 .send(ws::Message::Text(
555 format!(r#"["AUTH", {}]"#, event.to_string()).into(),
556 ))
557 .await?;
558 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
559 assert!(notice.2);
560
561 let event = Event::create(&key_pair, now(), 1, vec![], "test".to_owned())?;
563 framed
564 .send(ws::Message::Text(
565 format!(r#"["EVENT", {}]"#, event.to_string()).into(),
566 ))
567 .await?;
568 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
569 assert!(notice.2);
570
571 framed
572 .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
573 .await?;
574 let item = framed.next().await.unwrap()?;
575 assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
576
577 let key_pair1 = Keypair::new_global(&mut rng);
578 let mut framed = srv.ws_at("/").await.unwrap();
580
581 let item = framed.next().await.unwrap()?;
582 assert!(matches!(item, ws::Frame::Text(_)));
583 let state: (String, String) = parse_text(&item)?;
584 assert_eq!(state.0, "AUTH");
585
586 let event = Event::create(
587 &key_pair1,
588 now(),
589 22242,
590 vec![vec!["challenge".to_owned(), state.1.clone()]],
591 "".to_owned(),
592 )?;
593 framed
594 .send(ws::Message::Text(
595 format!(r#"["AUTH", {}]"#, event.to_string()).into(),
596 ))
597 .await?;
598 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
599 assert!(notice.2);
600
601 let event = Event::create(&key_pair, now(), 1, vec![], "test".to_owned())?;
603 framed
604 .send(ws::Message::Text(
605 format!(r#"["EVENT", {}]"#, event.to_string()).into(),
606 ))
607 .await?;
608 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
609 assert!(notice.3.contains("auth-required"));
610 assert!(!notice.2);
611
612 framed
613 .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
614 .await?;
615 let item = framed.next().await.unwrap()?;
616 assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
617
618 Ok(())
619 }
620
621 #[actix_rt::test]
622 async fn nip70() -> Result<()> {
623 let mut rng = thread_rng();
624 let key_pair = Keypair::new_global(&mut rng);
625
626 let app = create_test_app("auth-nip70")?;
627 {
628 let mut w = app.setting.write();
629 w.extra = serde_json::from_str(r#"{ "auth": { "enabled": false } }"#)?;
630 }
631 let app = app.add_extension(Auth::new());
632 let app = web::Data::new(app);
633
634 let mut srv = actix_test::start(move || create_web_app(app.clone()));
635
636 let mut framed = srv.ws_at("/").await.unwrap();
638
639 let event = Event::create(
641 &key_pair,
642 now(),
643 1,
644 vec![vec!["-".to_owned()]],
645 "test".to_owned(),
646 )?;
647 framed
648 .send(ws::Message::Text(
649 format!(r#"["EVENT", {}]"#, event.to_string()).into(),
650 ))
651 .await?;
652 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
653 assert!(!notice.2);
654 assert!(notice.3.contains("blocked"));
655
656 framed
657 .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
658 .await?;
659 let item = framed.next().await.unwrap()?;
660 assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
661
662 Ok(())
663 }
664
665 #[actix_rt::test]
666 async fn nip70_with_auth() -> Result<()> {
667 let mut rng = thread_rng();
668 let key_pair = Keypair::new_global(&mut rng);
669 let key_pair1 = Keypair::new_global(&mut rng);
670
671 let app = create_test_app("auth-nip70-auth")?;
674 {
675 let mut w = app.setting.write();
676 w.extra = serde_json::from_str(r#"{ "auth": { "enabled": true } }"#)?;
677 }
678 let app = app.add_extension(Auth::new());
679 let app = web::Data::new(app);
680
681 let mut srv = actix_test::start(move || create_web_app(app.clone()));
682
683 let mut framed = srv.ws_at("/").await.unwrap();
685
686 let item = framed.next().await.unwrap()?;
687 assert!(matches!(item, ws::Frame::Text(_)));
688 let state: (String, String) = parse_text(&item)?;
689 assert_eq!(state.0, "AUTH");
690
691 let event = Event::create(
693 &key_pair,
694 now(),
695 1,
696 vec![vec!["-".to_owned()]],
697 "test".to_owned(),
698 )?;
699 framed
700 .send(ws::Message::Text(
701 format!(r#"["EVENT", {}]"#, event.to_string()).into(),
702 ))
703 .await?;
704 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
705 assert!(!notice.2);
706 assert!(notice.3.contains("authorization"));
707
708 let event = Event::create(
709 &key_pair,
710 now(),
711 22242,
712 vec![vec!["challenge".to_owned(), state.1.clone()]],
713 "".to_owned(),
714 )?;
715 framed
716 .send(ws::Message::Text(
717 format!(r#"["AUTH", {}]"#, event.to_string()).into(),
718 ))
719 .await?;
720 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
721 assert!(notice.2);
722
723 let event = Event::create(
724 &key_pair1,
725 now(),
726 1,
727 vec![vec!["-".to_owned()]],
728 "test".to_owned(),
729 )?;
730 framed
731 .send(ws::Message::Text(
732 format!(r#"["EVENT", {}]"#, event.to_string()).into(),
733 ))
734 .await?;
735 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
736 assert!(!notice.2);
737 assert!(notice.3.contains("author"));
738
739 let event = Event::create(
740 &key_pair,
741 now(),
742 1,
743 vec![vec!["-".to_owned()]],
744 "test".to_owned(),
745 )?;
746 framed
747 .send(ws::Message::Text(
748 format!(r#"["EVENT", {}]"#, event.to_string()).into(),
749 ))
750 .await?;
751 let notice: (String, String, bool, String) = parse_text(&framed.next().await.unwrap()?)?;
752 assert!(notice.2);
753
754 framed
755 .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
756 .await?;
757 let item = framed.next().await.unwrap()?;
758 assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
759
760 Ok(())
761 }
762}