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#[derive(Debug, Deserialize)]
13pub struct Token {
14 pub account: models::Account,
15 pub iat: u64,
18 pub exp: u64,
19 #[serde(skip)]
20 pub jwt: String,
21}
22
23impl Token {
24 #[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 #[must_use]
53 pub fn is_expired(&self) -> bool {
54 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}