nats_io_jwt/
lib.rs

1//! Generate JWTs signed using NKEYs for use with [NATS](https://nats.io)
2//!
3//! **NOTE** - This is still a work in progress and will be published to crates.io once it's ready.
4//!
5//! Supports generating JWTs for Operator, Account, User and Activation claims.
6//!
7//! ## Example
8//!
9//! ```
10//! use nats_io_jwt::{KeyPair, Token, Account, User, Permission, SigningKeys};
11//!
12//! // You would probably load the operator's seed via a config and use KeyPair::from_seed
13//! let operator_signing_key = KeyPair::new_operator();
14//!
15//! let account_keypair = KeyPair::new_account();
16//! let account_signing_key = KeyPair::new_account();
17//! let account: Account = Account::builder()
18//!     .signing_keys(SigningKeys::from(&account_signing_key))
19//!     .try_into()
20//!     .expect("Account to be valid");
21//! let account_token = Token::new(account_keypair.public_key())
22//!     .name("My Account")
23//!     .claims(account)
24//!     .sign(&operator_signing_key);
25//! println!("account_token: {}", account_token);
26//!
27//! let user_keypair = KeyPair::new_user();
28//! let user: User = User::builder()
29//!    .pub_(Permission::from("service.hello.world"))
30//!    .sub(Permission::from("_INBOX."))
31//!    .subs(10)
32//!    .payload(1024 * 1024) // 1MiB
33//!    .bearer_token(true)
34//!    .try_into()
35//!    .expect("Account to be valid");
36//! let user_token = Token::new(user_keypair.public_key())
37//!     .name("My User")
38//!     .claims(user)
39//!     .sign(&account_signing_key);
40//! println!("user_token: {}", user_token);
41//! ```
42//!
43//! ## License
44//!
45//! Some parts taken from `https://github.com/AircastDev/nats-jwt`
46//!
47//! Licensed under
48//!
49//! - MIT license
50//!   ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
51//!
52mod nats_jwt_schema;
53
54use data_encoding::{BASE32HEX_NOPAD, BASE64URL_NOPAD};
55use std::time::SystemTime;
56
57use sha2::{Digest, Sha256};
58
59// flatten the module into this one
60pub use nats_jwt_schema::*;
61
62/// Re-export some `KeyPair` things from the nkeys crate.
63pub 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/// JWT token builder.
112#[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    /// Set the friendly name for the token, can be anything, defaults to the token subject
131    #[must_use]
132    pub fn name(mut self, name: impl Into<String>) -> Self {
133        self.name = Some(name.into());
134        self
135    }
136
137    /// Set the Claims inside the Token.
138    #[must_use]
139    pub fn claims(mut self, claims: impl Into<Claims>) -> Self {
140        self.claims = Some(claims.into());
141        self
142    }
143
144    /// Set expiration
145    #[must_use]
146    pub fn expires(mut self, expires: i64) -> Self {
147        self.expires = Some(expires);
148        self
149    }
150
151    /// Sign the token with the given signing key, returning a JWT string.
152    ///
153    /// If this is a User token, this should be the Account signing key.
154    /// If this is an Account token, this should be the Operator key
155    ///
156    /// # Panics
157    ///
158    /// - If system time is before UNIX epoch.
159    /// - If the seconds from UNIX epoch cannot be represented in a i64.
160    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    // Note this useful idiom: importing names from outer (for mod tests) scope.
200    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}