Skip to main content

faucet_common_nats/
auth.rs

1//! NATS authentication modes.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// NATS client authentication configuration.
8///
9/// Serializes with an adjacent `{ "type": <method>, "config": { … } }` tag in
10/// snake_case, matching every other faucet connector's auth shape.
11///
12/// The [`std::fmt::Debug`] implementation is hand-written so secret material
13/// (`token`, `password`, `nkey` seed) is never printed — only the variant name
14/// and non-secret fields appear.
15#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
16#[serde(tag = "type", content = "config", rename_all = "snake_case")]
17pub enum NatsAuth {
18    /// No authentication (anonymous connection).
19    #[default]
20    None,
21    /// Bearer/token authentication.
22    Token {
23        /// The authentication token.
24        token: String,
25    },
26    /// Username + password authentication.
27    UserPassword {
28        /// The username.
29        username: String,
30        /// The password.
31        password: String,
32    },
33    /// NKey (Ed25519 seed) authentication.
34    NKey {
35        /// The NKey seed (starts with `S`).
36        nkey: String,
37    },
38    /// Credentials-file (`.creds`) authentication — a decentralized JWT +
39    /// NKey seed bundle as produced by `nsc`.
40    CredsFile {
41        /// Path to the `.creds` file.
42        path: PathBuf,
43    },
44}
45
46impl std::fmt::Debug for NatsAuth {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        // Never render secret material — only the variant and non-secret fields.
49        match self {
50            NatsAuth::None => f.write_str("None"),
51            NatsAuth::Token { .. } => f
52                .debug_struct("Token")
53                .field("token", &"<redacted>")
54                .finish(),
55            NatsAuth::UserPassword { username, .. } => f
56                .debug_struct("UserPassword")
57                .field("username", username)
58                .field("password", &"<redacted>")
59                .finish(),
60            NatsAuth::NKey { .. } => f.debug_struct("NKey").field("nkey", &"<redacted>").finish(),
61            NatsAuth::CredsFile { path } => {
62                f.debug_struct("CredsFile").field("path", path).finish()
63            }
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use serde_json::json;
72
73    #[test]
74    fn default_is_none() {
75        assert!(matches!(NatsAuth::default(), NatsAuth::None));
76    }
77
78    #[test]
79    fn serde_round_trip_token() {
80        let auth = NatsAuth::Token {
81            token: "sekret".into(),
82        };
83        let v = serde_json::to_value(&auth).unwrap();
84        assert_eq!(v["type"], "token");
85        assert_eq!(v["config"]["token"], "sekret");
86        let parsed: NatsAuth = serde_json::from_value(v).unwrap();
87        assert!(matches!(parsed, NatsAuth::Token { token } if token == "sekret"));
88    }
89
90    #[test]
91    fn serde_round_trip_user_password() {
92        let v = json!({"type": "user_password", "config": {"username": "u", "password": "p"}});
93        let parsed: NatsAuth = serde_json::from_value(v).unwrap();
94        assert!(
95            matches!(parsed, NatsAuth::UserPassword { username, password } if username == "u" && password == "p")
96        );
97    }
98
99    #[test]
100    fn serde_round_trip_creds_file() {
101        let v = json!({"type": "creds_file", "config": {"path": "/tmp/x.creds"}});
102        let parsed: NatsAuth = serde_json::from_value(v).unwrap();
103        assert!(
104            matches!(parsed, NatsAuth::CredsFile { path } if path == std::path::Path::new("/tmp/x.creds"))
105        );
106    }
107
108    #[test]
109    fn debug_redacts_token() {
110        let auth = NatsAuth::Token {
111            token: "super-secret-token".into(),
112        };
113        let dbg = format!("{auth:?}");
114        assert!(
115            !dbg.contains("super-secret-token"),
116            "debug leaked token: {dbg}"
117        );
118        assert!(dbg.contains("redacted"));
119    }
120
121    #[test]
122    fn debug_redacts_password_but_shows_username() {
123        let auth = NatsAuth::UserPassword {
124            username: "alice".into(),
125            password: "hunter2".into(),
126        };
127        let dbg = format!("{auth:?}");
128        assert!(dbg.contains("alice"));
129        assert!(!dbg.contains("hunter2"), "debug leaked password: {dbg}");
130    }
131
132    #[test]
133    fn debug_redacts_nkey() {
134        let auth = NatsAuth::NKey {
135            nkey: "SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY".into(),
136        };
137        let dbg = format!("{auth:?}");
138        assert!(!dbg.contains("SUACSSL3"), "debug leaked nkey: {dbg}");
139    }
140
141    #[test]
142    fn schema_compiles() {
143        let _ = schemars::schema_for!(NatsAuth);
144    }
145}