nullnet_libtoken/
lib.rs

1pub mod models;
2mod utils;
3
4use base64::Engine as _;
5use serde::Deserialize;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8const EXPIRATION_MARGIN: u64 = 60 * 5;
9
10/// Represents a decoded JWT payload containing account information and metadata.
11/// Includes issue and expiration times for the token.
12#[derive(Debug, Deserialize)]
13pub struct Token {
14    pub account: models::Account,
15    // It is there, but we dont need it
16    // pub signed_in_account: models::Account,
17    pub iat: u64,
18    pub exp: u64,
19    #[serde(skip)]
20    pub jwt: String,
21}
22
23impl Token {
24    /// Decodes a JWT and parses its payload into a `Token` struct.
25    ///
26    /// # Arguments
27    /// * `jwt` - A JWT string consisting of three parts separated by periods (`.`).
28    ///
29    /// # Returns
30    /// * `Ok(Token)` if the token is successfully decoded and parsed.
31    /// * `Err(Error)` if the token is malformed, Base64 decoding fails, or payload deserialization fails.
32    #[allow(clippy::missing_errors_doc)]
33    pub fn from_jwt(jwt: &str) -> Result<Self, String> {
34        let parts: Vec<&str> = jwt.split('.').collect();
35
36        if parts.len() != 3 {
37            return Err(String::from("Malformed JWT"));
38        }
39
40        let decoded_payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
41            .decode(parts[1])
42            .map_err(|e| e.to_string())?;
43
44        let mut token: Token =
45            serde_json::from_slice(&decoded_payload).map_err(|e| e.to_string())?;
46        token.jwt = jwt.to_string();
47
48        Ok(token)
49    }
50
51    /// Checks if the token has expired.
52    #[must_use]
53    pub fn is_expired(&self) -> bool {
54        // consider the token expired if duration_since fails
55        let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) else {
56            return true;
57        };
58        self.exp <= (duration.as_secs() - EXPIRATION_MARGIN)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use crate::Token;
65
66    #[test]
67    fn test_device_issued_to_a_device() {
68        let token = concat!(
69            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50Ijp7InByb2ZpbGUiOnsiaWQiOiIwMUp",
70            "ZMkM0NVQ3VjNLRVE1TjBKOU43SFpKRiIsImZpcnN0X25hbWUiOm51bGwsImxhc3RfbmFtZSI6bnVsbCw",
71            "iZW1haWwiOiJzeXN0ZW1fZGV2aWNlIiwiYWNjb3VudF9pZCI6IjAxSlQxUjFCMlhNRERCN1dZM0pDODR",
72            "EVjU1IiwiY2F0ZWdvcmllcyI6W10sImNvZGUiOm51bGwsInN0YXR1cyI6IkFjdGl2ZSIsIm9yZ2FuaXp",
73            "hdGlvbl9pZCI6IjAxSlkyQzQ1TkdXMVJUSERHV1NFV0Y5TkhGIn0sImNvbnRhY3QiOm51bGwsImRldml",
74            "jZSI6eyJpZCI6IjAxSlQxUjFCMlhNRERCN1dZM0pDODREVjU1IiwiY29kZSI6IkRWMDAwMDAxIiwiY2F",
75            "0ZWdvcmllcyI6WyJEZXZpY2UiXSwic3RhdHVzIjoiRHJhZnQiLCJvcmdhbml6YXRpb25faWQiOiIwMUp",
76            "CSEtYSFlTS1BQMjQ3SFpaV0hBM0pDVCIsInRpbWVzdGFtcCI6bnVsbH0sIm9yZ2FuaXphdGlvbiI6eyJ",
77            "pZCI6IjAxSkJIS1hIWVNLUFAyNDdIWlpXSEEzSkNUIiwibmFtZSI6Imdsb2JhbC1vcmdhbml6YXRpb24",
78            "iLCJjb2RlIjoiT1IwMDAwMDIiLCJjYXRlZ29yaWVzIjpbIlRlYW0iXSwic3RhdHVzIjoiQWN0aXZlIiw",
79            "ib3JnYW5pemF0aW9uX2lkIjoiMDFKQkhLWEhZU0tQUDI0N0haWldIQTNKQ1QiLCJwYXJlbnRfb3JnYW5",
80            "pemF0aW9uX2lkIjpudWxsfSwiaWQiOiIwMUpUMVIxQjJYTUREQjdXWTNKQzg0RFY1NSIsImFjY291bnR",
81            "faWQiOiJzeXN0ZW1fZGV2aWNlIiwib3JnYW5pemF0aW9uX2lkIjoiMDFKQkhLWEhZU0tQUDI0N0haWld",
82            "IQTNKQ1QiLCJhY2NvdW50X29yZ2FuaXphdGlvbl9pZCI6IjAxSlkyQzQ1WEo5MEtTU1laWFNNU0YyMDI",
83            "4IiwiYWNjb3VudF9zdGF0dXMiOiJBY3RpdmUiLCJyb2xlX2lkIjpudWxsfSwic2lnbmVkX2luX2FjY29",
84            "1bnQiOnsicHJvZmlsZSI6eyJpZCI6IjAxSlkyQzQ1VDdWM0tFUTVOMEo5TjdIWkpGIiwiZmlyc3RfbmF",
85            "tZSI6bnVsbCwibGFzdF9uYW1lIjpudWxsLCJlbWFpbCI6InN5c3RlbV9kZXZpY2UiLCJhY2NvdW50X2l",
86            "kIjoiMDFKVDFSMUIyWE1EREI3V1kzSkM4NERWNTUiLCJjYXRlZ29yaWVzIjpbXSwiY29kZSI6bnVsbCw",
87            "ic3RhdHVzIjoiQWN0aXZlIiwib3JnYW5pemF0aW9uX2lkIjoiMDFKWTJDNDVOR1cxUlRIREdXU0VXRjl",
88            "OSEYifSwiY29udGFjdCI6bnVsbCwiZGV2aWNlIjp7ImlkIjoiMDFKVDFSMUIyWE1EREI3V1kzSkM4NER",
89            "WNTUiLCJjb2RlIjoiRFYwMDAwMDEiLCJjYXRlZ29yaWVzIjpbIkRldmljZSJdLCJzdGF0dXMiOiJEcmF",
90            "mdCIsIm9yZ2FuaXphdGlvbl9pZCI6IjAxSkJIS1hIWVNLUFAyNDdIWlpXSEEzSkNUIiwidGltZXN0YW1",
91            "wIjpudWxsfSwib3JnYW5pemF0aW9uIjp7ImlkIjoiMDFKQkhLWEhZU0tQUDI0N0haWldIQTNKQ1QiLCJ",
92            "uYW1lIjoiZ2xvYmFsLW9yZ2FuaXphdGlvbiIsImNvZGUiOiJPUjAwMDAwMiIsImNhdGVnb3JpZXMiOls",
93            "iVGVhbSJdLCJzdGF0dXMiOiJBY3RpdmUiLCJvcmdhbml6YXRpb25faWQiOiIwMUpCSEtYSFlTS1BQMjQ",
94            "3SFpaV0hBM0pDVCIsInBhcmVudF9vcmdhbml6YXRpb25faWQiOm51bGx9LCJpZCI6IjAxSlQxUjFCMlh",
95            "NRERCN1dZM0pDODREVjU1IiwiYWNjb3VudF9pZCI6InN5c3RlbV9kZXZpY2UiLCJvcmdhbml6YXRpb25",
96            "faWQiOiIwMUpCSEtYSFlTS1BQMjQ3SFpaV0hBM0pDVCIsImFjY291bnRfb3JnYW5pemF0aW9uX2lkIjo",
97            "iMDFKWTJDNDVYSjkwS1NTWVpYU01TRjIwMjgiLCJhY2NvdW50X3N0YXR1cyI6IkFjdGl2ZSIsInJvbGV",
98            "faWQiOm51bGx9LCJpYXQiOjE3NTAyODc3NjIsImV4cCI6MTc1MDQ2MDU2Mn0.y6B0BNCuYQeiVsrqiCQ",
99            "kkwdYCvwj5x2tqFEHRCSXkxY"
100        );
101
102        let token = Token::from_jwt(token);
103        assert!(token.is_ok());
104
105        let token = token.unwrap();
106        assert!(token.account.device.is_some());
107        assert!(token.account.contact.is_none());
108    }
109
110    #[test]
111    fn test_device_issued_to_a_user() {
112        let token = concat!(
113            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50Ijp7InByb2ZpbGUiOnsiaWQiOiIwMUp",
114            "ZMkM0NUFOWkMwNjJaQ0Y2QzU4MlJQRyIsImZpcnN0X25hbWUiOiJTdXBlciIsImxhc3RfbmFtZSI6IkF",
115            "kbWluIiwiZW1haWwiOiJhZG1pbkBkbmFtaWNyby5jb20iLCJhY2NvdW50X2lkIjoiMDFKQ1NBRzc5S1E",
116            "xV00wRjlCNDdRNzAwUDEiLCJjYXRlZ29yaWVzIjpbXSwiY29kZSI6bnVsbCwic3RhdHVzIjoiQWN0aXZ",
117            "lIiwib3JnYW5pemF0aW9uX2lkIjoiMDFKWTJDNDU0UjZHTTM2R0gwQ1RDREcwQ0gifSwiY29udGFjdCI",
118            "6eyJpZCI6IjAxSkNTQUc3OUtRMVdNMEY5QjQ3UTcwMFAxIiwiZmlyc3RfbmFtZSI6IlN1cGVyIiwibGF",
119            "zdF9uYW1lIjoiQWRtaW4iLCJhY2NvdW50X2lkIjoiMDFKQ1NBRzc5S1ExV00wRjlCNDdRNzAwUDEiLCJ",
120            "jb2RlIjoiQ08wMDAwMDEiLCJjYXRlZ29yaWVzIjpbIkNvbnRhY3QiXSwic3RhdHVzIjoiQWN0aXZlIiw",
121            "ib3JnYW5pemF0aW9uX2lkIjoiMDFKQkhLWEhZU0tQUDI0N0haWldIQTNKQ1QiLCJkYXRlX29mX2JpcnR",
122            "oIjpudWxsfSwiZGV2aWNlIjpudWxsLCJvcmdhbml6YXRpb24iOnsiaWQiOiIwMUpCSEtYSFlTS1BQMjQ",
123            "3SFpaV0hBM0pDVCIsIm5hbWUiOiJnbG9iYWwtb3JnYW5pemF0aW9uIiwiY29kZSI6Ik9SMDAwMDAyIiw",
124            "iY2F0ZWdvcmllcyI6WyJUZWFtIl0sInN0YXR1cyI6IkFjdGl2ZSIsIm9yZ2FuaXphdGlvbl9pZCI6IjA",
125            "xSkJIS1hIWVNLUFAyNDdIWlpXSEEzSkNUIiwicGFyZW50X29yZ2FuaXphdGlvbl9pZCI6bnVsbH0sIml",
126            "kIjoiMDFKQ1NBRzc5S1ExV00wRjlCNDdRNzAwUDEiLCJhY2NvdW50X2lkIjoiYWRtaW5AZG5hbWljcm8",
127            "uY29tIiwib3JnYW5pemF0aW9uX2lkIjoiMDFKQkhLWEhZU0tQUDI0N0haWldIQTNKQ1QiLCJhY2NvdW5",
128            "0X29yZ2FuaXphdGlvbl9pZCI6IjAxSlkyQzQ1SlRCMFEySDE5QjM0Q1FOOVZNIiwiYWNjb3VudF9zdGF",
129            "0dXMiOiJBY3RpdmUiLCJyb2xlX2lkIjpudWxsfSwic2lnbmVkX2luX2FjY291bnQiOnsicHJvZmlsZSI",
130            "6eyJpZCI6IjAxSlkyQzQ1QU5aQzA2MlpDRjZDNTgyUlBHIiwiZmlyc3RfbmFtZSI6IlN1cGVyIiwibGF",
131            "zdF9uYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGRuYW1pY3JvLmNvbSIsImFjY291bnRfaWQiOiI",
132            "wMUpDU0FHNzlLUTFXTTBGOUI0N1E3MDBQMSIsImNhdGVnb3JpZXMiOltdLCJjb2RlIjpudWxsLCJzdGF",
133            "0dXMiOiJBY3RpdmUiLCJvcmdhbml6YXRpb25faWQiOiIwMUpZMkM0NTRSNkdNMzZHSDBDVENERzBDSCJ",
134            "9LCJjb250YWN0Ijp7ImlkIjoiMDFKQ1NBRzc5S1ExV00wRjlCNDdRNzAwUDEiLCJmaXJzdF9uYW1lIjo",
135            "iU3VwZXIiLCJsYXN0X25hbWUiOiJBZG1pbiIsImFjY291bnRfaWQiOiIwMUpDU0FHNzlLUTFXTTBGOUI",
136            "0N1E3MDBQMSIsImNvZGUiOiJDTzAwMDAwMSIsImNhdGVnb3JpZXMiOlsiQ29udGFjdCJdLCJzdGF0dXM",
137            "iOiJBY3RpdmUiLCJvcmdhbml6YXRpb25faWQiOiIwMUpCSEtYSFlTS1BQMjQ3SFpaV0hBM0pDVCIsImR",
138            "hdGVfb2ZfYmlydGgiOm51bGx9LCJkZXZpY2UiOm51bGwsIm9yZ2FuaXphdGlvbiI6eyJpZCI6IjAxSkJ",
139            "IS1hIWVNLUFAyNDdIWlpXSEEzSkNUIiwibmFtZSI6Imdsb2JhbC1vcmdhbml6YXRpb24iLCJjb2RlIjo",
140            "iT1IwMDAwMDIiLCJjYXRlZ29yaWVzIjpbIlRlYW0iXSwic3RhdHVzIjoiQWN0aXZlIiwib3JnYW5pemF",
141            "0aW9uX2lkIjoiMDFKQkhLWEhZU0tQUDI0N0haWldIQTNKQ1QiLCJwYXJlbnRfb3JnYW5pemF0aW9uX2l",
142            "kIjpudWxsfSwiaWQiOiIwMUpDU0FHNzlLUTFXTTBGOUI0N1E3MDBQMSIsImFjY291bnRfaWQiOiJhZG1",
143            "pbkBkbmFtaWNyby5jb20iLCJvcmdhbml6YXRpb25faWQiOiIwMUpCSEtYSFlTS1BQMjQ3SFpaV0hBM0p",
144            "DVCIsImFjY291bnRfb3JnYW5pemF0aW9uX2lkIjoiMDFKWTJDNDVKVEIwUTJIMTlCMzRDUU45Vk0iLCJ",
145            "hY2NvdW50X3N0YXR1cyI6IkFjdGl2ZSIsInJvbGVfaWQiOm51bGx9LCJpYXQiOjE3NTAyODY5MTIsImV",
146            "4cCI6MTc1MDQ1OTcxMn0.vzG9BDQH_upGkzOavcIPdAfImN9E4KPH9La5Eo2jKIU"
147        );
148
149        let token = Token::from_jwt(token);
150        assert!(token.is_ok());
151
152        let token = token.unwrap();
153        assert!(token.account.device.is_none());
154        assert!(token.account.contact.is_some());
155    }
156
157    #[test]
158    fn test_device_issued_to_root_1() {
159        let token = concat!(
160            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50Ijp7InByb2ZpbGUiOnsiaWQiOiIwMUp",
161            "NM0dUV0NIUjNDTTJOUDg1QzBRMktOMSIsImZpcnN0X25hbWUiOm51bGwsImxhc3RfbmFtZSI6bnVsbCw",
162            "iZW1haWwiOiJyb290IiwiYWNjb3VudF9pZCI6IjAxSk0zR1RXQ0hSM0NNMk5QODVDMFEyS04xIiwiY2F",
163            "0ZWdvcmllcyI6W10sImNvZGUiOm51bGwsInN0YXR1cyI6IkFjdGl2ZSIsIm9yZ2FuaXphdGlvbl9pZCI",
164            "6IjAxSlNONFhBMkMzQTdSSE4zTU5aWkpHQlIzIn0sImNvbnRhY3QiOm51bGwsImRldmljZSI6bnVsbCw",
165            "ib3JnYW5pemF0aW9uIjp7ImlkIjoiMDFKU040WEEyQzNBN1JITjNNTlpaSkdCUjMiLCJuYW1lIjoiUm9",
166            "vdCBQZXJzb25hbCBPcmdhbml6YXRpb24iLCJjb2RlIjoiT1IwMDAwMDAiLCJjYXRlZ29yaWVzIjpbIlJ",
167            "vb3QiLCJQZXJzb25hbCJdLCJzdGF0dXMiOiJBY3RpdmUiLCJvcmdhbml6YXRpb25faWQiOiIwMUpTTjR",
168            "YQTJDM0E3UkhOM01OWlpKR0JSMyIsInBhcmVudF9vcmdhbml6YXRpb25faWQiOm51bGx9LCJpZCI6IjA",
169            "xSk0zR1RXQ0hSM0NNMk5QODVDMFEyS04xIiwiYWNjb3VudF9pZCI6InJvb3QiLCJvcmdhbml6YXRpb25",
170            "faWQiOiIwMUpTTjRYQTJDM0E3UkhOM01OWlpKR0JSMyIsImFjY291bnRfb3JnYW5pemF0aW9uX2lkIjo",
171            "iMDFKTTNHVFdDSFIzQ00yTlA4NUMwUTJLTjEiLCJhY2NvdW50X3N0YXR1cyI6IkFjdGl2ZSIsInJvbGV",
172            "faWQiOm51bGx9LCJzaWduZWRfaW5fYWNjb3VudCI6eyJwcm9maWxlIjp7ImlkIjoiMDFKTTNHVFdDSFI",
173            "zQ00yTlA4NUMwUTJLTjEiLCJmaXJzdF9uYW1lIjpudWxsLCJsYXN0X25hbWUiOm51bGwsImVtYWlsIjo",
174            "icm9vdCIsImFjY291bnRfaWQiOiIwMUpNM0dUV0NIUjNDTTJOUDg1QzBRMktOMSIsImNhdGVnb3JpZXM",
175            "iOltdLCJjb2RlIjpudWxsLCJzdGF0dXMiOiJBY3RpdmUiLCJvcmdhbml6YXRpb25faWQiOiIwMUpTTjR",
176            "YQTJDM0E3UkhOM01OWlpKR0JSMyJ9LCJjb250YWN0IjpudWxsLCJkZXZpY2UiOm51bGwsIm9yZ2FuaXp",
177            "hdGlvbiI6eyJpZCI6IjAxSlNONFhBMkMzQTdSSE4zTU5aWkpHQlIzIiwibmFtZSI6IlJvb3QgUGVyc29",
178            "uYWwgT3JnYW5pemF0aW9uIiwiY29kZSI6Ik9SMDAwMDAwIiwiY2F0ZWdvcmllcyI6WyJSb290IiwiUGV",
179            "yc29uYWwiXSwic3RhdHVzIjoiQWN0aXZlIiwib3JnYW5pemF0aW9uX2lkIjoiMDFKU040WEEyQzNBN1J",
180            "ITjNNTlpaSkdCUjMiLCJwYXJlbnRfb3JnYW5pemF0aW9uX2lkIjpudWxsfSwiaWQiOiIwMUpNM0dUV0N",
181            "IUjNDTTJOUDg1QzBRMktOMSIsImFjY291bnRfaWQiOiJyb290Iiwib3JnYW5pemF0aW9uX2lkIjoiMDF",
182            "KU040WEEyQzNBN1JITjNNTlpaSkdCUjMiLCJhY2NvdW50X29yZ2FuaXphdGlvbl9pZCI6IjAxSk0zR1R",
183            "XQ0hSM0NNMk5QODVDMFEyS04xIiwiYWNjb3VudF9zdGF0dXMiOiJBY3RpdmUiLCJyb2xlX2lkIjpudWx",
184            "sfSwiaWF0IjoxNzUwMjg3OTIwLCJleHAiOjE3NTA0NjA3MjB9.wHqECSTYAe6rDsZd3Pff66yklRmGw4",
185            "olmJSYi502Q_M"
186        );
187
188        let token = Token::from_jwt(token);
189        assert!(token.is_ok());
190
191        let token = token.unwrap();
192        assert!(token.account.device.is_none());
193        assert!(token.account.contact.is_none());
194    }
195
196    #[test]
197    fn test_device_issued_to_root_2() {
198        let token = concat!(
199            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50Ijp7ImlzX3Jvb3RfYWNjb3VudCI6dHJ",
200            "1ZSwicHJvZmlsZSI6eyJpZCI6IjAxSk0zR1RXQ0hSM0NNMk5QODVDMFEyS04xIiwiZmlyc3RfbmFtZSI",
201            "6bnVsbCwibGFzdF9uYW1lIjpudWxsLCJlbWFpbCI6InJvb3QiLCJhY2NvdW50X2lkIjoiMDFKTTNHVFd",
202            "DSFIzQ00yTlA4NUMwUTJLTjEiLCJjYXRlZ29yaWVzIjpbXSwiY29kZSI6bnVsbCwic3RhdHVzIjoiQWN",
203            "0aXZlIiwib3JnYW5pemF0aW9uX2lkIjoiMDFKU040WEEyQzNBN1JITjNNTlpaSkdCUjMifSwib3JnYW5",
204            "pemF0aW9uIjp7ImlkIjoiMDFKU040WEEyQzNBN1JITjNNTlpaSkdCUjMiLCJuYW1lIjoiUm9vdCBQZXJ",
205            "zb25hbCBPcmdhbml6YXRpb24iLCJjb2RlIjoiT1IwMDAwMDAiLCJjYXRlZ29yaWVzIjpbIlJvb3QiLCJ",
206            "QZXJzb25hbCJdLCJzdGF0dXMiOiJBY3RpdmUiLCJvcmdhbml6YXRpb25faWQiOiIwMUpTTjRYQTJDM0E",
207            "3UkhOM01OWlpKR0JSMyIsInBhcmVudF9vcmdhbml6YXRpb25faWQiOm51bGx9LCJpZCI6IjAxSk0zR1R",
208            "XQ0hSM0NNMk5QODVDMFEyS04xIiwiYWNjb3VudF9pZCI6InJvb3QiLCJvcmdhbml6YXRpb25faWQiOiI",
209            "wMUpTTjRYQTJDM0E3UkhOM01OWlpKR0JSMyIsImFjY291bnRfb3JnYW5pemF0aW9uX2lkIjoiMDFKTTN",
210            "HVFdDSFIzQ00yTlA4NUMwUTJLTjEiLCJhY2NvdW50X3N0YXR1cyI6IkFjdGl2ZSIsInJvbGVfaWQiOm5",
211            "1bGwsImNvbnRhY3QiOnt9LCJkZXZpY2UiOnt9fSwicHJldmlvdXNseV9sb2dnZWRfaW4iOiIiLCJpYXQ",
212            "iOjE3NTAyOTkzMzIsImV4cCI6MTc1MDQ3MjEzMn0.6UiIYItNoAptPBLv3S8QC5eWrs2FEkzmo1OPD65",
213            "pfoA"
214        );
215
216        let token = Token::from_jwt(token);
217        assert!(token.is_ok());
218    }
219}