Skip to main content

awsim_core/
auth.rs

1use crate::router::RequestContext;
2
3/// Credentials extracted from an AWS SigV4 Authorization header.
4#[derive(Debug, Clone)]
5pub struct SigV4Credentials {
6    pub access_key: String,
7    pub region: String,
8    pub service: String,
9    pub date: String,
10    pub signed_headers: Vec<String>,
11    pub signature: String,
12}
13
14/// Parse the Authorization header to extract SigV4 credential components.
15///
16/// Format: `AWS4-HMAC-SHA256 Credential={access_key}/{date}/{region}/{service}/aws4_request,
17///          SignedHeaders={headers}, Signature={sig}`
18pub fn parse_authorization(header: &str) -> Option<SigV4Credentials> {
19    let header = header.strip_prefix("AWS4-HMAC-SHA256")?.trim_start();
20
21    let mut credential = None;
22    let mut signed_headers = None;
23    let mut signature = None;
24
25    // Split on comma — handle both ", " and "," and ", " with extra whitespace
26    for part in header.split(',') {
27        let part = part.trim();
28        if let Some(val) = part.strip_prefix("Credential=") {
29            credential = Some(val.trim());
30        } else if let Some(val) = part.strip_prefix("SignedHeaders=") {
31            signed_headers = Some(val.trim());
32        } else if let Some(val) = part.strip_prefix("Signature=") {
33            signature = Some(val.trim());
34        }
35    }
36
37    let credential = credential?;
38    let parts: Vec<&str> = credential.split('/').collect();
39    if parts.len() < 5 {
40        return None;
41    }
42
43    Some(SigV4Credentials {
44        access_key: parts[0].to_string(),
45        date: parts[1].to_string(),
46        region: parts[2].to_string(),
47        service: parts[3].to_string(),
48        signed_headers: signed_headers
49            .unwrap_or("")
50            .split(';')
51            .map(|s| s.to_string())
52            .collect(),
53        signature: signature.unwrap_or("").to_string(),
54    })
55}
56
57/// Build a RequestContext from parsed SigV4 credentials.
58pub fn build_request_context(
59    creds: &SigV4Credentials,
60    method: &str,
61    uri: &str,
62    default_account_id: &str,
63) -> RequestContext {
64    let mut ctx = RequestContext::new(&creds.service, &creds.region);
65    ctx.account_id = default_account_id.to_string();
66    ctx.access_key = Some(creds.access_key.clone());
67    ctx.method = method.to_string();
68    ctx.uri = uri.to_string();
69    ctx
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_parse_sigv4_authorization() {
78        let header = "AWS4-HMAC-SHA256 \
79            Credential=AKIAIOSFODNN7EXAMPLE/20230101/us-east-1/s3/aws4_request, \
80            SignedHeaders=host;x-amz-date, \
81            Signature=abcdef1234567890";
82
83        let creds = parse_authorization(header).unwrap();
84        assert_eq!(creds.access_key, "AKIAIOSFODNN7EXAMPLE");
85        assert_eq!(creds.date, "20230101");
86        assert_eq!(creds.region, "us-east-1");
87        assert_eq!(creds.service, "s3");
88        assert_eq!(creds.signed_headers, vec!["host", "x-amz-date"]);
89        assert_eq!(creds.signature, "abcdef1234567890");
90    }
91
92    #[test]
93    fn test_parse_invalid_header() {
94        assert!(parse_authorization("Bearer token123").is_none());
95        assert!(parse_authorization("").is_none());
96    }
97
98    #[test]
99    fn test_build_request_context() {
100        let creds = SigV4Credentials {
101            access_key: "AKID".to_string(),
102            region: "eu-west-1".to_string(),
103            service: "dynamodb".to_string(),
104            date: "20230101".to_string(),
105            signed_headers: vec!["host".to_string()],
106            signature: "sig".to_string(),
107        };
108
109        let ctx = build_request_context(&creds, "POST", "/", "123456789012");
110        assert_eq!(ctx.account_id, "123456789012");
111        assert_eq!(ctx.region, "eu-west-1");
112        assert_eq!(ctx.service, "dynamodb");
113        assert_eq!(ctx.access_key.unwrap(), "AKID");
114    }
115}