1use crate::config::{
22 AuthConfig, BearerConfig, SignatureAlgorithm, SignatureConfig, SignatureEncoding,
23};
24use anyhow::{anyhow, Result};
25use hmac::{Hmac, Mac};
26use sha1::Sha1;
27use sha2::Sha256;
28use std::env;
29use subtle::ConstantTimeEq;
30
31#[derive(Debug, Clone, PartialEq)]
33pub enum AuthResult {
34 Success,
36 NotConfigured,
38 Failed(String),
40}
41
42impl AuthResult {
43 pub fn is_ok(&self) -> bool {
45 matches!(self, AuthResult::Success | AuthResult::NotConfigured)
46 }
47}
48
49pub fn verify_auth(
53 auth_config: Option<&AuthConfig>,
54 headers: &axum::http::HeaderMap,
55 body: &[u8],
56) -> AuthResult {
57 let Some(config) = auth_config else {
58 return AuthResult::NotConfigured;
59 };
60
61 if let Some(ref sig_config) = config.signature {
63 match verify_signature(sig_config, headers, body) {
64 Ok(()) => {}
65 Err(e) => return AuthResult::Failed(format!("Signature verification failed: {e}")),
66 }
67 }
68
69 if let Some(ref bearer_config) = config.bearer {
71 match verify_bearer(bearer_config, headers) {
72 Ok(()) => {}
73 Err(e) => return AuthResult::Failed(format!("Bearer token verification failed: {e}")),
74 }
75 }
76
77 if config.signature.is_some() || config.bearer.is_some() {
79 AuthResult::Success
80 } else {
81 AuthResult::NotConfigured
82 }
83}
84
85fn verify_signature(
87 config: &SignatureConfig,
88 headers: &axum::http::HeaderMap,
89 body: &[u8],
90) -> Result<()> {
91 let secret = env::var(&config.secret_env).map_err(|_| {
93 anyhow!(
94 "Environment variable '{}' not set for signature secret",
95 config.secret_env
96 )
97 })?;
98
99 let signature_header = headers
101 .get(&config.header)
102 .ok_or_else(|| anyhow!("Signature header '{}' not found", config.header))?
103 .to_str()
104 .map_err(|_| anyhow!("Invalid signature header value"))?;
105
106 let signature_value = if let Some(ref prefix) = config.prefix {
108 signature_header
109 .strip_prefix(prefix)
110 .ok_or_else(|| anyhow!("Signature header missing expected prefix '{prefix}'"))?
111 } else {
112 signature_header
113 };
114
115 let received_signature = match config.encoding {
117 SignatureEncoding::Hex => {
118 hex::decode(signature_value).map_err(|e| anyhow!("Invalid hex signature: {e}"))?
119 }
120 SignatureEncoding::Base64 => {
121 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, signature_value)
122 .map_err(|e| anyhow!("Invalid base64 signature: {e}"))?
123 }
124 };
125
126 let expected_signature = match config.algorithm {
128 SignatureAlgorithm::HmacSha1 => compute_hmac_sha1(secret.as_bytes(), body)?,
129 SignatureAlgorithm::HmacSha256 => compute_hmac_sha256(secret.as_bytes(), body)?,
130 };
131
132 if constant_time_compare(&received_signature, &expected_signature) {
134 Ok(())
135 } else {
136 Err(anyhow!("Signature mismatch"))
137 }
138}
139
140fn compute_hmac_sha1(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
142 let mut mac =
143 Hmac::<Sha1>::new_from_slice(key).map_err(|e| anyhow!("HMAC-SHA1 key error: {e}"))?;
144 mac.update(data);
145 Ok(mac.finalize().into_bytes().to_vec())
146}
147
148fn compute_hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
150 let mut mac =
151 Hmac::<Sha256>::new_from_slice(key).map_err(|e| anyhow!("HMAC-SHA256 key error: {e}"))?;
152 mac.update(data);
153 Ok(mac.finalize().into_bytes().to_vec())
154}
155
156fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
158 if a.len() != b.len() {
159 return false;
160 }
161 a.ct_eq(b).into()
162}
163
164fn verify_bearer(config: &BearerConfig, headers: &axum::http::HeaderMap) -> Result<()> {
166 let expected_token = env::var(&config.token_env).map_err(|_| {
168 anyhow!(
169 "Environment variable '{}' not set for bearer token",
170 config.token_env
171 )
172 })?;
173
174 let auth_header = headers
176 .get(axum::http::header::AUTHORIZATION)
177 .ok_or_else(|| anyhow!("Authorization header not found"))?
178 .to_str()
179 .map_err(|_| anyhow!("Invalid Authorization header value"))?;
180
181 let received_token = auth_header
183 .strip_prefix("Bearer ")
184 .or_else(|| auth_header.strip_prefix("bearer "))
185 .ok_or_else(|| anyhow!("Authorization header is not a Bearer token"))?;
186
187 if constant_time_compare(received_token.as_bytes(), expected_token.as_bytes()) {
189 Ok(())
190 } else {
191 Err(anyhow!("Bearer token mismatch"))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use axum::http::HeaderMap;
199
200 fn create_headers(headers: &[(&str, &str)]) -> HeaderMap {
201 let mut map = HeaderMap::new();
202 for (name, value) in headers {
203 map.insert(
204 axum::http::HeaderName::from_bytes(name.as_bytes()).unwrap(),
205 axum::http::HeaderValue::from_str(value).unwrap(),
206 );
207 }
208 map
209 }
210
211 #[test]
212 fn test_auth_not_configured() {
213 let headers = HeaderMap::new();
214 let result = verify_auth(None, &headers, b"body");
215 assert_eq!(result, AuthResult::NotConfigured);
216 }
217
218 #[test]
219 fn test_auth_empty_config() {
220 let config = AuthConfig {
221 signature: None,
222 bearer: None,
223 };
224 let headers = HeaderMap::new();
225 let result = verify_auth(Some(&config), &headers, b"body");
226 assert_eq!(result, AuthResult::NotConfigured);
227 }
228
229 #[test]
230 fn test_hmac_sha256_github_style() {
231 env::set_var("TEST_GITHUB_SECRET", "test-secret");
233
234 let body = b"test payload";
235
236 let expected_sig = compute_hmac_sha256(b"test-secret", body).unwrap();
238 let sig_hex = hex::encode(&expected_sig);
239 let sig_header = format!("sha256={sig_hex}");
240
241 let config = AuthConfig {
242 signature: Some(SignatureConfig {
243 algorithm: SignatureAlgorithm::HmacSha256,
244 secret_env: "TEST_GITHUB_SECRET".to_string(),
245 header: "X-Hub-Signature-256".to_string(),
246 prefix: Some("sha256=".to_string()),
247 encoding: SignatureEncoding::Hex,
248 }),
249 bearer: None,
250 };
251
252 let headers = create_headers(&[("X-Hub-Signature-256", &sig_header)]);
253 let result = verify_auth(Some(&config), &headers, body);
254 assert_eq!(result, AuthResult::Success);
255
256 env::remove_var("TEST_GITHUB_SECRET");
257 }
258
259 #[test]
260 fn test_hmac_sha256_base64_shopify_style() {
261 env::set_var("TEST_SHOPIFY_SECRET", "shopify-secret");
263
264 let body = b"order data";
265
266 let expected_sig = compute_hmac_sha256(b"shopify-secret", body).unwrap();
268 let sig_base64 =
269 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &expected_sig);
270
271 let config = AuthConfig {
272 signature: Some(SignatureConfig {
273 algorithm: SignatureAlgorithm::HmacSha256,
274 secret_env: "TEST_SHOPIFY_SECRET".to_string(),
275 header: "X-Shopify-Hmac-Sha256".to_string(),
276 prefix: None,
277 encoding: SignatureEncoding::Base64,
278 }),
279 bearer: None,
280 };
281
282 let headers = create_headers(&[("X-Shopify-Hmac-Sha256", &sig_base64)]);
283 let result = verify_auth(Some(&config), &headers, body);
284 assert_eq!(result, AuthResult::Success);
285
286 env::remove_var("TEST_SHOPIFY_SECRET");
287 }
288
289 #[test]
290 fn test_hmac_sha1() {
291 env::set_var("TEST_SHA1_SECRET", "sha1-secret");
292
293 let body = b"test data";
294 let expected_sig = compute_hmac_sha1(b"sha1-secret", body).unwrap();
295 let sig_hex = hex::encode(&expected_sig);
296 let sig_header = format!("sha1={sig_hex}");
297
298 let config = AuthConfig {
299 signature: Some(SignatureConfig {
300 algorithm: SignatureAlgorithm::HmacSha1,
301 secret_env: "TEST_SHA1_SECRET".to_string(),
302 header: "X-Signature".to_string(),
303 prefix: Some("sha1=".to_string()),
304 encoding: SignatureEncoding::Hex,
305 }),
306 bearer: None,
307 };
308
309 let headers = create_headers(&[("X-Signature", &sig_header)]);
310 let result = verify_auth(Some(&config), &headers, body);
311 assert_eq!(result, AuthResult::Success);
312
313 env::remove_var("TEST_SHA1_SECRET");
314 }
315
316 #[test]
317 fn test_signature_mismatch() {
318 env::set_var("TEST_SECRET_MISMATCH", "correct-secret");
319
320 let config = AuthConfig {
321 signature: Some(SignatureConfig {
322 algorithm: SignatureAlgorithm::HmacSha256,
323 secret_env: "TEST_SECRET_MISMATCH".to_string(),
324 header: "X-Signature".to_string(),
325 prefix: None,
326 encoding: SignatureEncoding::Hex,
327 }),
328 bearer: None,
329 };
330
331 let headers = create_headers(&[(
333 "X-Signature",
334 "0000000000000000000000000000000000000000000000000000000000000000",
335 )]);
336 let result = verify_auth(Some(&config), &headers, b"body");
337
338 match result {
339 AuthResult::Failed(msg) => assert!(msg.contains("mismatch")),
340 _ => panic!("Expected AuthResult::Failed"),
341 }
342
343 env::remove_var("TEST_SECRET_MISMATCH");
344 }
345
346 #[test]
347 fn test_missing_signature_header() {
348 env::set_var("TEST_SECRET_MISSING", "secret");
349
350 let config = AuthConfig {
351 signature: Some(SignatureConfig {
352 algorithm: SignatureAlgorithm::HmacSha256,
353 secret_env: "TEST_SECRET_MISSING".to_string(),
354 header: "X-Signature".to_string(),
355 prefix: None,
356 encoding: SignatureEncoding::Hex,
357 }),
358 bearer: None,
359 };
360
361 let headers = HeaderMap::new();
362 let result = verify_auth(Some(&config), &headers, b"body");
363
364 match result {
365 AuthResult::Failed(msg) => assert!(msg.contains("not found")),
366 _ => panic!("Expected AuthResult::Failed"),
367 }
368
369 env::remove_var("TEST_SECRET_MISSING");
370 }
371
372 #[test]
373 fn test_bearer_token_success() {
374 env::set_var("TEST_BEARER_TOKEN", "my-secret-token");
375
376 let config = AuthConfig {
377 signature: None,
378 bearer: Some(BearerConfig {
379 token_env: "TEST_BEARER_TOKEN".to_string(),
380 }),
381 };
382
383 let headers = create_headers(&[("authorization", "Bearer my-secret-token")]);
384 let result = verify_auth(Some(&config), &headers, b"body");
385 assert_eq!(result, AuthResult::Success);
386
387 env::remove_var("TEST_BEARER_TOKEN");
388 }
389
390 #[test]
391 fn test_bearer_token_mismatch() {
392 env::set_var("TEST_BEARER_MISMATCH", "correct-token");
393
394 let config = AuthConfig {
395 signature: None,
396 bearer: Some(BearerConfig {
397 token_env: "TEST_BEARER_MISMATCH".to_string(),
398 }),
399 };
400
401 let headers = create_headers(&[("authorization", "Bearer wrong-token")]);
402 let result = verify_auth(Some(&config), &headers, b"body");
403
404 match result {
405 AuthResult::Failed(msg) => assert!(msg.contains("mismatch")),
406 _ => panic!("Expected AuthResult::Failed"),
407 }
408
409 env::remove_var("TEST_BEARER_MISMATCH");
410 }
411
412 #[test]
413 fn test_missing_bearer_header() {
414 env::set_var("TEST_BEARER_MISSING", "token");
415
416 let config = AuthConfig {
417 signature: None,
418 bearer: Some(BearerConfig {
419 token_env: "TEST_BEARER_MISSING".to_string(),
420 }),
421 };
422
423 let headers = HeaderMap::new();
424 let result = verify_auth(Some(&config), &headers, b"body");
425
426 match result {
427 AuthResult::Failed(msg) => assert!(msg.contains("not found")),
428 _ => panic!("Expected AuthResult::Failed"),
429 }
430
431 env::remove_var("TEST_BEARER_MISSING");
432 }
433
434 #[test]
435 fn test_both_signature_and_bearer() {
436 env::set_var("TEST_BOTH_SECRET", "sig-secret");
437 env::set_var("TEST_BOTH_TOKEN", "bearer-token");
438
439 let body = b"body";
440 let expected_sig = compute_hmac_sha256(b"sig-secret", body).unwrap();
441 let sig_hex = hex::encode(&expected_sig);
442
443 let config = AuthConfig {
444 signature: Some(SignatureConfig {
445 algorithm: SignatureAlgorithm::HmacSha256,
446 secret_env: "TEST_BOTH_SECRET".to_string(),
447 header: "X-Signature".to_string(),
448 prefix: None,
449 encoding: SignatureEncoding::Hex,
450 }),
451 bearer: Some(BearerConfig {
452 token_env: "TEST_BOTH_TOKEN".to_string(),
453 }),
454 };
455
456 let headers = create_headers(&[
457 ("X-Signature", &sig_hex),
458 ("authorization", "Bearer bearer-token"),
459 ]);
460 let result = verify_auth(Some(&config), &headers, body);
461 assert_eq!(result, AuthResult::Success);
462
463 env::remove_var("TEST_BOTH_SECRET");
464 env::remove_var("TEST_BOTH_TOKEN");
465 }
466
467 #[test]
468 fn test_both_auth_signature_fails() {
469 env::set_var("TEST_BOTH_SIG_FAIL_SECRET", "sig-secret");
470 env::set_var("TEST_BOTH_SIG_FAIL_TOKEN", "bearer-token");
471
472 let config = AuthConfig {
473 signature: Some(SignatureConfig {
474 algorithm: SignatureAlgorithm::HmacSha256,
475 secret_env: "TEST_BOTH_SIG_FAIL_SECRET".to_string(),
476 header: "X-Signature".to_string(),
477 prefix: None,
478 encoding: SignatureEncoding::Hex,
479 }),
480 bearer: Some(BearerConfig {
481 token_env: "TEST_BOTH_SIG_FAIL_TOKEN".to_string(),
482 }),
483 };
484
485 let headers = create_headers(&[
487 (
488 "X-Signature",
489 "0000000000000000000000000000000000000000000000000000000000000000",
490 ),
491 ("authorization", "Bearer bearer-token"),
492 ]);
493 let result = verify_auth(Some(&config), &headers, b"body");
494
495 match result {
496 AuthResult::Failed(msg) => assert!(msg.contains("Signature")),
497 _ => panic!("Expected AuthResult::Failed"),
498 }
499
500 env::remove_var("TEST_BOTH_SIG_FAIL_SECRET");
501 env::remove_var("TEST_BOTH_SIG_FAIL_TOKEN");
502 }
503
504 #[test]
505 fn test_constant_time_compare() {
506 assert!(constant_time_compare(b"hello", b"hello"));
507 assert!(!constant_time_compare(b"hello", b"world"));
508 assert!(!constant_time_compare(b"hello", b"hell"));
509 assert!(!constant_time_compare(b"", b"a"));
510 assert!(constant_time_compare(b"", b""));
511 }
512}