Skip to main content

yule_api/
auth.rs

1use axum::extract::Request;
2use axum::http::StatusCode;
3use axum::middleware::Next;
4use axum::response::Response;
5use axum::Extension;
6use std::sync::Arc;
7
8const TOKEN_PREFIX: &str = "yule_";
9
10pub struct TokenAuthority {
11    master: [u8; 32],
12    hashes: Vec<[u8; 32]>,
13}
14
15impl TokenAuthority {
16    pub fn new() -> Self {
17        let mut seed = [0u8; 32];
18        getrandom::fill(&mut seed).expect("os entropy failed");
19        Self { master: seed, hashes: Vec::new() }
20    }
21
22    pub fn generate_token(&mut self) -> String {
23        let input = [
24            &self.master[..],
25            &(self.hashes.len() as u64).to_le_bytes(),
26            &std::time::SystemTime::now()
27                .duration_since(std::time::UNIX_EPOCH)
28                .unwrap()
29                .as_nanos()
30                .to_le_bytes(),
31        ].concat();
32
33        let derived = blake3::hash(&input);
34        let hex: String = derived.as_bytes()[..24]
35            .iter()
36            .map(|b| format!("{b:02x}"))
37            .collect();
38        let token = format!("{TOKEN_PREFIX}{hex}");
39
40        self.hashes.push(*blake3::hash(token.as_bytes()).as_bytes());
41        token
42    }
43
44    pub fn from_existing(token: &str) -> Self {
45        let mut auth = Self {
46            master: [0u8; 32],
47            hashes: Vec::new(),
48        };
49        auth.hashes.push(*blake3::hash(token.as_bytes()).as_bytes());
50        auth
51    }
52
53    pub fn verify(&self, provided: &str) -> bool {
54        let hash = *blake3::hash(provided.as_bytes()).as_bytes();
55        self.hashes.iter().any(|h| h == &hash)
56    }
57}
58
59pub async fn require_auth(
60    Extension(auth): Extension<Arc<TokenAuthority>>,
61    req: Request,
62    next: Next,
63) -> std::result::Result<Response, StatusCode> {
64    let token = req.headers()
65        .get("authorization")
66        .and_then(|v| v.to_str().ok())
67        .and_then(|v| v.strip_prefix("Bearer "));
68
69    match token {
70        Some(t) if auth.verify(t) => Ok(next.run(req).await),
71        _ => Err(StatusCode::UNAUTHORIZED),
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn generate_and_verify() {
81        let mut auth = TokenAuthority::new();
82        let token = auth.generate_token();
83        assert!(token.starts_with("yule_"));
84        assert_eq!(token.len(), 5 + 48); // prefix + 24 bytes as hex
85        assert!(auth.verify(&token));
86    }
87
88    #[test]
89    fn reject_garbage() {
90        let mut auth = TokenAuthority::new();
91        let _ = auth.generate_token();
92        assert!(!auth.verify("not-a-real-token"));
93        assert!(!auth.verify("yule_000000000000000000000000000000000000000000000000"));
94        assert!(!auth.verify(""));
95    }
96
97    #[test]
98    fn multiple_tokens_all_valid() {
99        let mut auth = TokenAuthority::new();
100        let t1 = auth.generate_token();
101        let t2 = auth.generate_token();
102        let t3 = auth.generate_token();
103        assert!(auth.verify(&t1));
104        assert!(auth.verify(&t2));
105        assert!(auth.verify(&t3));
106    }
107
108    #[test]
109    fn tokens_are_unique() {
110        let mut auth = TokenAuthority::new();
111        let t1 = auth.generate_token();
112        let t2 = auth.generate_token();
113        assert_ne!(t1, t2);
114    }
115
116    #[test]
117    fn from_existing_verifies() {
118        let auth = TokenAuthority::from_existing("my-secret-token");
119        assert!(auth.verify("my-secret-token"));
120        assert!(!auth.verify("wrong-token"));
121    }
122
123    #[test]
124    fn stores_hashes_not_plaintext() {
125        let mut auth = TokenAuthority::new();
126        let token = auth.generate_token();
127        // hashes vec stores 32-byte blake3 digests, not the raw token
128        for h in &auth.hashes {
129            assert_ne!(h, token.as_bytes().get(..32).unwrap_or(&[]));
130        }
131    }
132}