1mod nats_jwt_schema;
53
54use data_encoding::{BASE32HEX_NOPAD, BASE64URL_NOPAD};
55use std::time::SystemTime;
56
57use sha2::{Digest, Sha256};
58
59pub use nats_jwt_schema::*;
61
62pub use nkeys::{decode_seed, from_public_key, KeyPair, KeyPairType};
64
65const JWT_HEADER: &str = r#"{"typ":"JWT","alg":"ed25519-nkey"}"#;
66
67impl From<String> for SigningKeys {
68 fn from(signing_public_key: String) -> Self {
69 vec![SigningKeysItem::PublicKey(signing_public_key)].into()
70 }
71}
72
73impl From<&str> for SigningKeys {
74 fn from(signing_public_key: &str) -> Self {
75 vec![SigningKeysItem::PublicKey(signing_public_key.into())].into()
76 }
77}
78
79impl From<&KeyPair> for SigningKeys {
80 fn from(signing_key: &KeyPair) -> Self {
81 vec![SigningKeysItem::PublicKey(signing_key.public_key())].into()
82 }
83}
84
85impl From<Vec<String>> for Permission {
86 fn from(allow: Vec<String>) -> Self {
87 Permission::try_from(Permission::builder().allow(Some(allow.into()))).unwrap()
88 }
89}
90
91impl From<Vec<&str>> for Permission {
92 fn from(allow: Vec<&str>) -> Self {
93 let allow_vec: Vec<String> = allow.iter().map(|s| s.to_string()).collect();
94 Permission::try_from(Permission::builder().allow(Some(allow_vec.into()))).unwrap()
95 }
96}
97
98impl From<String> for Permission {
99 fn from(allow: String) -> Self {
100 Permission::try_from(Permission::builder().allow(Some(vec![allow].into()))).unwrap()
101 }
102}
103
104impl From<&str> for Permission {
105 fn from(allow: &str) -> Self {
106 let allow_vec: Vec<String> = vec![allow.to_string()];
107 Permission::try_from(Permission::builder().allow(Some(allow_vec.into()))).unwrap()
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct Token {
114 subject: String,
115 name: Option<String>,
116 claims: Option<Claims>,
117 expires: Option<i64>,
118}
119
120impl Token {
121 pub fn new(subject: impl Into<String>) -> Self {
122 Self {
123 subject: subject.into(),
124 name: None,
125 claims: None,
126 expires: None,
127 }
128 }
129
130 #[must_use]
132 pub fn name(mut self, name: impl Into<String>) -> Self {
133 self.name = Some(name.into());
134 self
135 }
136
137 #[must_use]
139 pub fn claims(mut self, claims: impl Into<Claims>) -> Self {
140 self.claims = Some(claims.into());
141 self
142 }
143
144 #[must_use]
146 pub fn expires(mut self, expires: i64) -> Self {
147 self.expires = Some(expires);
148 self
149 }
150
151 pub fn sign(self, signing_key: &KeyPair) -> String {
161 let issued_at: i64 = SystemTime::now()
162 .duration_since(SystemTime::UNIX_EPOCH)
163 .expect("system time is after the unix epoch")
164 .as_secs()
165 .try_into()
166 .expect("seconds from UNIX epoch cannot be represented in a i64");
167 let subject = self.subject.clone();
168 let mut jwt: Jwt = Jwt::builder()
169 .iat(issued_at)
170 .iss(signing_key.public_key())
171 .jti("")
172 .sub(subject)
173 .name(self.name)
174 .nats(self.claims.expect("Claims to be set"))
175 .exp(self.expires)
176 .try_into()
177 .expect("Jwt should be well formed");
178
179 let claims_str = serde_json::to_string(&jwt).expect("claims serialisation cannot fail");
180 let mut hasher = Sha256::new();
181 hasher.update(claims_str);
182 let claims_hash = hasher.finalize();
183 jwt.jti = BASE32HEX_NOPAD.encode(claims_hash.as_slice());
184
185 let claims_str = serde_json::to_string(&jwt).expect("claims serialisation cannot fail");
186
187 let b64_header = BASE64URL_NOPAD.encode(JWT_HEADER.as_bytes());
188 let b64_body = BASE64URL_NOPAD.encode(claims_str.as_bytes());
189 let jwt_half = format!("{b64_header}.{b64_body}");
190 let sig = signing_key.sign(jwt_half.as_bytes()).unwrap();
191 let b64_sig = BASE64URL_NOPAD.encode(&sig);
192
193 format!("{jwt_half}.{b64_sig}")
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
201
202 #[test]
203 fn test_new_token() {
204 let id_key = nkeys::KeyPair::new_account();
205 let operator_key_pair = nkeys::KeyPair::new_operator();
206
207 let limits: OperatorLimits = OperatorLimits::default();
208
209 let account: Account = Account::builder()
210 .limits(limits)
211 .signing_keys(SigningKeys::from("key"))
212 .try_into()
213 .expect("Account to be valid");
214
215 let jwt_str = Token::new(id_key.public_key())
216 .name("test")
217 .claims(account)
218 .sign(&operator_key_pair);
219
220 let claims_b64 = jwt_str.split('.').skip(1).next().unwrap().as_bytes();
221 let claims_raw = BASE64URL_NOPAD.decode(claims_b64).unwrap();
222 let jwt: Jwt = serde_json::from_slice(&claims_raw).unwrap();
223 assert_eq!(jwt.iss, operator_key_pair.public_key());
224 assert_eq!(jwt.sub, id_key.public_key());
225 match &jwt.nats {
226 Claims::Account(account) => {
227 if let Some(limits) = &account.limits {
228 assert_eq!(limits.conn, -1);
229 assert_eq!(limits.consumer, -1);
230 assert_eq!(limits.data, -1);
231 assert_eq!(limits.disallow_bearer, false);
232 assert_eq!(limits.disk_max_stream_bytes, 0);
233 assert_eq!(limits.disk_storage, -1);
234 assert_eq!(limits.exports, -1);
235 assert_eq!(limits.imports, -1);
236 assert_eq!(limits.leaf, -1);
237 assert_eq!(limits.max_ack_pending, 0);
238 assert_eq!(limits.max_bytes_required, false);
239 assert_eq!(limits.mem_max_stream_bytes, 0);
240 assert_eq!(limits.mem_storage, -1);
241 assert_eq!(limits.payload, -1);
242 assert_eq!(limits.streams, -1);
243 assert_eq!(limits.subs, -1);
244 assert_eq!(limits.wildcards, true);
245 } else {
246 panic!("Expected Default Limits");
247 };
248 }
249 Claims::User(_) => panic!("Expected Account, was User"),
250 Claims::Activation(_) => panic!("Expected Account, was Activation"),
251 Claims::Operator(_) => panic!("Expected Account, was Operator"),
252 }
253
254 let operator_signing_key = KeyPair::new_operator();
255
256 let account_keypair = KeyPair::new_account();
257 let account_signing_key = KeyPair::new_account();
258 let account: Account = Account::builder()
259 .signing_keys(SigningKeys::from(&account_signing_key))
260 .try_into()
261 .expect("Account to be valid");
262 let account_token = Token::new(account_keypair.public_key())
263 .name("My Account")
264 .claims(account)
265 .sign(&operator_signing_key);
266 println!("account_token: {}", account_token);
267 }
268}