1use mtv_crypto::{
2 authenticator::WithoutTransport, Address, AuthChain, AuthLink, Authenticator, Web3Transport,
3};
4use std::{
5 collections::HashMap,
6 time::{SystemTime, UNIX_EPOCH},
7};
8
9const AUTH_CHAIN_HEADER_PREFIX: &str = "x-identity-auth-chain-";
10const AUTH_TIMESTAMP_HEADER: &str = "x-identity-timestamp";
11const AUTH_METADATA_HEADER: &str = "x-identity-metadata";
12const DEFAULT_EXPIRATION: u32 = 1000 * 60;
13
14#[derive(Debug)]
16pub enum AuthMiddlewareError {
17 InvalidMessage,
19 InvalidTimestamp,
21 InvalidMetadata,
23 Unauthotized,
25 Expired,
27}
28
29pub struct VerificationOptions<T> {
31 authenticator: Authenticator<T>,
33 expirtation: Option<u32>,
35}
36
37impl Default for VerificationOptions<WithoutTransport> {
38 fn default() -> Self {
39 Self {
40 authenticator: Authenticator::new(),
41 expirtation: None,
42 }
43 }
44}
45
46impl<T> VerificationOptions<T> {
47 pub fn with_authenticator(authenticator: Authenticator<T>) -> Self {
48 Self {
49 authenticator,
50 expirtation: None,
51 }
52 }
53
54 pub fn authenticator<U>(self, authenticator: Authenticator<U>) -> VerificationOptions<U> {
55 VerificationOptions {
56 authenticator,
57 expirtation: self.expirtation,
58 }
59 }
60
61 pub fn expiration(self, exp: u32) -> Self {
62 Self {
63 authenticator: self.authenticator,
64 expirtation: Some(exp),
65 }
66 }
67}
68
69pub async fn verify<T: Web3Transport>(
80 method: &str,
81 path: &str,
82 headers: HashMap<String, String>,
83 options: VerificationOptions<T>,
84) -> Result<Address, AuthMiddlewareError> {
85 let headers = normalize_headers(headers);
86
87 let auth_chain = extract_auth_chain(&headers)?;
88 let timestamp = if let Some(ts) = headers.get(AUTH_TIMESTAMP_HEADER) {
89 ts
90 } else {
91 return Err(AuthMiddlewareError::InvalidTimestamp);
92 };
93
94 let ts_number = verify_ts(timestamp)?;
95
96 let metadata = if let Some(metadata) = headers.get(AUTH_METADATA_HEADER) {
97 metadata
98 } else {
99 return Err(AuthMiddlewareError::InvalidMetadata);
100 };
101
102 let payload = create_payload(method, path, timestamp, metadata);
103
104 let exp = options.expirtation.unwrap_or(DEFAULT_EXPIRATION);
105
106 verify_expiration(ts_number, exp)?;
107 verify_sign(options.authenticator, auth_chain, &payload).await
108}
109
110fn extract_auth_chain(headers: &HashMap<String, String>) -> Result<AuthChain, AuthMiddlewareError> {
111 let mut index = 0;
112
113 let mut auth_links = vec![];
114 while let Some(header) = headers.get(&format!("{}{}", AUTH_CHAIN_HEADER_PREFIX, index)) {
115 if let Ok(auth_link) = AuthLink::parse(header) {
116 auth_links.push(auth_link);
117 } else {
118 return Err(AuthMiddlewareError::InvalidMessage);
119 }
120
121 index += 1;
122 }
123
124 Ok(AuthChain::from(auth_links))
125}
126
127fn normalize_headers(headers: HashMap<String, String>) -> HashMap<String, String> {
128 headers
129 .iter()
130 .map(|(key, val)| (key.to_ascii_lowercase(), val.clone()))
131 .collect::<HashMap<String, String>>()
132}
133
134fn verify_ts(ts: &str) -> Result<u128, AuthMiddlewareError> {
135 ts.parse::<u128>()
136 .map_err(|_| AuthMiddlewareError::InvalidTimestamp)
137}
138
139fn create_payload(method: &str, path: &str, timestamp: &str, metadata: &str) -> String {
140 [method, path, timestamp, metadata].join(":").to_lowercase()
141}
142
143async fn verify_sign<T: Web3Transport>(
144 authenticator: Authenticator<T>,
145 auth_chain: AuthChain,
146 payload: &str,
147) -> Result<Address, AuthMiddlewareError> {
148 Ok(authenticator
149 .verify_signature(&auth_chain, payload)
150 .await
151 .map_err(|_| AuthMiddlewareError::Unauthotized)?
152 .to_owned())
153}
154
155fn verify_expiration(ts: u128, expiration: u32) -> Result<(), AuthMiddlewareError> {
156 let now = SystemTime::now()
157 .duration_since(UNIX_EPOCH)
158 .expect("not unix epoch time")
159 .as_millis();
160
161 let expected = ts + expiration as u128;
162
163 if expected < now {
164 return Err(AuthMiddlewareError::Expired);
165 }
166
167 Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172 use std::time::Duration;
173
174 use crate::test_utils::create_test_identity;
175
176 use super::*;
177
178 #[tokio::test]
179 async fn verify_should_return_ok() {
180 let identity = create_test_identity();
181 let now = SystemTime::now()
182 .duration_since(UNIX_EPOCH)
183 .unwrap()
184 .as_millis();
185 let chain = identity.sign_payload(format!("get:/:{}:{}", now, "{}"));
186 let mapped_headers = HashMap::from([
188 (
189 "X-Identity-Auth-Chain-0".to_string(),
190 serde_json::to_string(chain.get(0).unwrap()).unwrap(),
191 ),
192 (
193 "X-Identity-Auth-Chain-1".to_string(),
194 serde_json::to_string(chain.get(1).unwrap()).unwrap(),
195 ),
196 (
197 "X-Identity-Auth-Chain-2".to_string(),
198 serde_json::to_string(chain.get(2).unwrap()).unwrap(),
199 ),
200 ("X-Identity-Timestamp".to_string(), format!("{}", now)),
201 ("X-Identity-Metadata".to_string(), "{}".to_string()),
202 ]);
203
204 verify(
205 "GET",
206 "/",
207 mapped_headers,
208 VerificationOptions {
209 authenticator: Authenticator::new(),
210 expirtation: None,
211 },
212 )
213 .await
214 .unwrap();
215 }
216
217 #[tokio::test]
218 async fn verify_should_return_err() {
219 let mapped_headers = HashMap::from([
220 (
221 "x-identity-auth-chain-0".to_string(),
222 r#"{"type": "SIGNER", "payload": "0x7949f9F239D1a0816ce5Eb364A1F588AE9Cc1Bf5","signature": ""}"#.to_string(),
223 ),
224 (
225 "x-identity-auth-chain-1".to_string(),
226 r#"{"type":"ECDSA_EPHEMERAL","payload":"Memetaverse Login\nEphemeral address: 0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34\nExpiration: 3021-10-16T22:32:29.626Z","signature":"0x39dd4ddf131ad2435d56c81c994c4417daef5cf5998258027ef8a1401470876a1365a6b79810dc0c4a2e9352befb63a9e4701d67b38007d83ffc4cd2b7a38ad51b"}"#.to_string(),
227 ),
228 (
229 "x-identity-auth-chain-2".to_string(),
230 r#"{"type":"ECDSA_SIGNED_ENTITY","payload":"get:/api/events:1684936391789:{}","signature":"0xc1511b724b986925896fa7f67f1004b1dbca331f32bea806456ea205904a70f723d1ecb9c0f8c52a930fccb2d2eb61ca715120d57b3226d66d8ce5e63567f27c1c"}"#.to_string(),
231 ),
232 ("x-identity-timestamp".to_string(), "".to_string()),
233 ("x-identity-metadata".to_string(), "{}".to_string()),
234 ]);
235
236 assert!(matches!(
237 verify(
238 "GET",
239 "/",
240 mapped_headers,
241 VerificationOptions {
242 authenticator: Authenticator::new(),
243 expirtation: None,
244 },
245 )
246 .await
247 .unwrap_err(),
248 AuthMiddlewareError::InvalidTimestamp
249 ));
250
251 let mapped_headers = HashMap::from([
252 (
253 "x-identity-auth-chain-0".to_string(),
254 r#"{"type": "SIGNER", "payload": "0x7949f9F239D1a0816ce5Eb364A1F588AE9Cc1Bf5","signature": ""}"#.to_string(),
255 ),
256 (
257 "x-identity-auth-chain-1".to_string(),
258 r#"{"type":"ECDSA_EPHEMERAL","payload":"Memetaverse Login\nEphemeral address: 0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34\nExpiration: 3021-10-16T22:32:29.626Z","signature":"0x39dd4ddf131ad2435d56c81c994c4417daef5cf5998258027ef8a1401470876a1365a6b79810dc0c4a2e9352befb63a9e4701d67b38007d83ffc4cd2b7a38ad51b"}"#.to_string(),
259 ),
260 (
261 "x-identity-auth-chain-2".to_string(),
262 r#"{"type":"ECDSA_SIGNED_ENTITY","payload":"get:/api/events:1684936391789:{}","signature":"0xc1511b724b986925896fa7f67f1004b1dbca331f32bea806456ea205904a70f723d1ecb9c0f8c52a930fccb2d2eb61ca715120d57b3226d66d8ce5e63567f27c1c"}"#.to_string(),
263 ),
264 ("x-identity-metadata".to_string(), "{}".to_string()),
265 ]);
266
267 assert!(matches!(
268 verify(
269 "GET",
270 "/",
271 mapped_headers,
272 VerificationOptions {
273 authenticator: Authenticator::new(),
274 expirtation: None,
275 },
276 )
277 .await
278 .unwrap_err(),
279 AuthMiddlewareError::InvalidTimestamp
280 ));
281
282 let mapped_headers = HashMap::from([
283 (
284 "x-identity-auth-chain-0".to_string(),
285 r#"{"type": "SIGNER", "payload": "0x7949f9F239D1a0816ce5Eb364A1F588AE9Cc1Bf5","signature": ""}"#.to_string(),
286 ),
287 (
288 "x-identity-auth-chain-1".to_string(),
289 r#"{"type":"ECDSA_EPHEMERAL","payload":"Memetaverse Login\nEphemeral address: 0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34\nExpiration: 3021-10-16T22:32:29.626Z","signature":"0x39dd4ddf131ad2435d56c81c994c4417daef5cf5998258027ef8a1401470876a1365a6b79810dc0c4a2e9352befb63a9e4701d67b38007d83ffc4cd2b7a38ad51b"}"#.to_string(),
290 ),
291 (
292 "x-identity-auth-chain-2".to_string(),
293 r#"{"type":"ECDSA_SIGNED_ENTITY","payload":"get:/api/events:1684936391789:{}","signature":"0xc1511b724b986925896fa7f67f1004b1dbca331f32bea806456ea205904a70f723d1ecb9c0f8c52a930fccb2d2eb61ca715120d57b3226d66d8ce5e63567f27c1c"}"#.to_string(),
294 ),
295 ("x-identity-timestamp".to_string(), "1684937236359".to_string()),
296 ]);
297
298 assert!(matches!(
299 verify(
300 "GET",
301 "/",
302 mapped_headers,
303 VerificationOptions {
304 authenticator: Authenticator::new(),
305 expirtation: None,
306 },
307 )
308 .await
309 .unwrap_err(),
310 AuthMiddlewareError::InvalidMetadata
311 ));
312
313 let past_timestamp = SystemTime::now()
314 .duration_since(UNIX_EPOCH)
315 .unwrap()
316 .checked_sub(Duration::from_secs(120))
317 .unwrap()
318 .as_millis();
319
320 let mapped_headers = HashMap::from([
321 (
322 "x-identity-auth-chain-0".to_string(),
323 r#"{"type": "SIGNER", "payload": "0x7949f9F239D1a0816ce5Eb364A1F588AE9Cc1Bf5","signature": ""}"#.to_string(),
324 ),
325 (
326 "x-identity-auth-chain-1".to_string(),
327 r#"{"type":"ECDSA_EPHEMERAL","payload":"Memetaverse Login\nEphemeral address: 0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34\nExpiration: 3021-10-16T22:32:29.626Z","signature":"0x39dd4ddf131ad2435d56c81c994c4417daef5cf5998258027ef8a1401470876a1365a6b79810dc0c4a2e9352befb63a9e4701d67b38007d83ffc4cd2b7a38ad51b"}"#.to_string(),
328 ),
329 (
330 "x-identity-auth-chain-2".to_string(),
331 r#"{"type":"ECDSA_SIGNED_ENTITY","payload":"get:/api/events:1684936391789:{}","signature":"0xc1511b724b986925896fa7f67f1004b1dbca331f32bea806456ea205904a70f723d1ecb9c0f8c52a930fccb2d2eb61ca715120d57b3226d66d8ce5e63567f27c1c"}"#.to_string(),
332 ),
333 ("x-identity-timestamp".to_string(), format!("{}", past_timestamp)),
334 ("x-identity-metadata".to_string(), "{}".to_string()),
335 ]);
336
337 assert!(matches!(
338 verify(
339 "GET",
340 "/",
341 mapped_headers,
342 VerificationOptions {
343 authenticator: Authenticator::new(),
344 expirtation: None,
345 },
346 )
347 .await
348 .unwrap_err(),
349 AuthMiddlewareError::Expired
350 ));
351
352 let identity = create_test_identity();
353 let now = SystemTime::now()
354 .duration_since(UNIX_EPOCH)
355 .unwrap()
356 .as_millis();
357 let chain = identity.sign_payload(format!("get:/api/events:{}:{}", now, "{}"));
358
359 let mapped_headers = HashMap::from([
361 (
362 "X-Identity-Auth-Chain-0".to_string(),
363 serde_json::to_string(chain.get(0).unwrap()).unwrap(),
364 ),
365 (
366 "X-Identity-Auth-Chain-1".to_string(),
367 serde_json::to_string(chain.get(1).unwrap()).unwrap(),
368 ),
369 (
370 "X-Identity-Auth-Chain-2".to_string(),
371 serde_json::to_string(chain.get(2).unwrap()).unwrap(),
372 ),
373 ("X-Identity-Timestamp".to_string(), format!("{}", now)),
374 ("X-Identity-Metadata".to_string(), "{}".to_string()),
375 ]);
376
377 assert!(matches!(
378 verify(
379 "GET",
380 "/",
381 mapped_headers,
382 VerificationOptions {
383 authenticator: Authenticator::new(),
384 expirtation: None,
385 },
386 )
387 .await
388 .unwrap_err(),
389 AuthMiddlewareError::Unauthotized
390 ));
391 }
392
393 #[test]
394 fn extract_authchain_should_return_ok() {
395 let mapped_headers = HashMap::from([
396 (
397 "x-identity-auth-chain-0".to_string(),
398 r#"{"type": "SIGNER", "payload": "0x7949f9F239D1a0816ce5Eb364A1F588AE9Cc1Bf5","signature": ""}"#.to_string(),
399 ),
400 (
401 "x-identity-auth-chain-1".to_string(),
402 r#"{"type":"ECDSA_EPHEMERAL","payload":"Memetaverse Login\nEphemeral address: 0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34\nExpiration: 3021-10-16T22:32:29.626Z","signature":"0x39dd4ddf131ad2435d56c81c994c4417daef5cf5998258027ef8a1401470876a1365a6b79810dc0c4a2e9352befb63a9e4701d67b38007d83ffc4cd2b7a38ad51b"}"#.to_string(),
403 ),
404 (
405 "x-identity-auth-chain-2".to_string(),
406 r#"{"type":"ECDSA_SIGNED_ENTITY","payload":"get:/api/events:1684936391789:{}","signature":"0xc1511b724b986925896fa7f67f1004b1dbca331f32bea806456ea205904a70f723d1ecb9c0f8c52a930fccb2d2eb61ca715120d57b3226d66d8ce5e63567f27c1c"}"#.to_string(),
407 ),
408 ("x-identity-timestamp".to_string(), "1684937236359".to_string()),
409 ("x-identity-metadata".to_string(), "{}".to_string()),
410 ]);
411
412 assert!(extract_auth_chain(&mapped_headers).is_ok())
413 }
414
415 #[test]
416 fn extract_authchain_should_return_err() {
417 let mapped_headers = HashMap::from([
418 (
419 "x-identity-auth-chain-0".to_string(),
420 r#"{"type": "SIGNER", "payload": "0x7949f9F239D1a0816ce5Eb364A1F588AE9Cc1Bf5","signature": ""}"#.to_string(),
421 ),
422 (
423 "x-identity-auth-chain-1".to_string(),
424 r#"{}"#.to_string(),
425 ),
426 (
427 "x-identity-auth-chain-2".to_string(),
428 r#"{"type":"ECDSA_SIGNED_ENTITY","payload":"get:/api/events:1684936391789:{}","signature":"0xc1511b724b986925896fa7f67f1004b1dbca331f32bea806456ea205904a70f723d1ecb9c0f8c52a930fccb2d2eb61ca715120d57b3226d66d8ce5e63567f27c1c"}"#.to_string(),
429 ),
430 ("x-identity-timestamp".to_string(), "1684937236359".to_string()),
431 ("x-identity-metadata".to_string(), "{}".to_string()),
432 ]);
433
434 assert!(matches!(
435 extract_auth_chain(&mapped_headers).unwrap_err(),
436 AuthMiddlewareError::InvalidMessage
437 ))
438 }
439
440 #[test]
441 fn verify_ts_should_return_ok() {
442 let ts = "1684869538587";
443
444 assert_eq!(verify_ts(ts).unwrap(), 1684869538587)
445 }
446
447 #[test]
448 fn verify_ts_should_return_err() {
449 let ts = "1684869538d587";
450
451 assert!(matches!(
452 verify_ts(ts).unwrap_err(),
453 AuthMiddlewareError::InvalidTimestamp
454 ));
455 }
456
457 #[tokio::test]
458 async fn verify_sign_should_return_ok() {
459 let identity = create_test_identity();
460 let signed_fetch = identity.sign_payload("get:/api/events:1684869538587:{}");
461
462 let address: Address = verify_sign(
463 Authenticator::new(),
464 signed_fetch,
465 "get:/api/events:1684869538587:{}",
466 )
467 .await
468 .unwrap();
469
470 assert_eq!(
471 address.to_string(),
472 "0x13fe90239bfda363ec33a849b716616958c04f0f"
473 )
474 }
475
476 #[tokio::test]
477 async fn verify_sign_should_return_err() {
478 let identity = create_test_identity();
479 let signed_fetch = identity.sign_payload("get:/api/events:1684869538587:{}");
480
481 assert!(matches!(
482 verify_sign(
483 Authenticator::new(),
484 signed_fetch,
485 "get:/api/events:1684869538687:{}",
486 )
487 .await
488 .unwrap_err(),
489 AuthMiddlewareError::Unauthotized
490 ));
491 }
492
493 #[test]
494 fn expiration_should_return_ok() {
495 let now = SystemTime::now()
496 .duration_since(UNIX_EPOCH)
497 .unwrap()
498 .as_millis();
499
500 assert!(verify_expiration(now, DEFAULT_EXPIRATION).is_ok());
501 }
502
503 #[test]
504 fn expiration_should_return_error() {
505 let past = SystemTime::now()
506 .duration_since(UNIX_EPOCH)
507 .unwrap()
508 .checked_sub(Duration::from_secs(120))
509 .unwrap()
510 .as_millis();
511
512 assert!(verify_expiration(past, DEFAULT_EXPIRATION).is_err());
513 }
514}