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