nostr_extensions/
auth.rs

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    /// read auth: ["REQ"]
28    pub req: Option<Permission>,
29    /// write auth: ["EVENT"]
30    pub event: Option<Permission>,
31}
32
33#[derive(Default, Debug)]
34pub struct Auth {
35    setting: AuthSetting,
36}
37
38pub enum AuthState {
39    /// The AUTH challenge
40    Challenge(String),
41    /// Authenticated with pubkey
42    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                        // check nip70 protected event
211                        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        // client service
442        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        // client service
530        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        // req
538        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        // write
562        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        // client service
579        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        // write
602        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        // client service
637        let mut framed = srv.ws_at("/").await.unwrap();
638
639        // protected event
640        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 pubkey = XOnlyPublicKey::from_keypair(&key_pair).0;
672
673        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        // client service
684        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        // protected event without auth
692        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}