Skip to main content

ferro_rs/api/
api_key.rs

1//! API key authentication for machine-to-machine access.
2//!
3//! Provides key generation (prefixed, hashed), a provider trait for
4//! storage-agnostic verification, and middleware for route protection.
5//!
6//! # Key Format
7//!
8//! Keys follow the `fe_{env}_{random}` pattern (similar to Stripe):
9//! - `fe_live_` prefix for production keys
10//! - `fe_test_` prefix for test/development keys
11//! - 43 random base62 characters for the secret portion
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use ferro_rs::{ApiKeyMiddleware, generate_api_key};
17//!
18//! // Generate a new key (show raw_key once, store prefix + hash)
19//! let key = generate_api_key("live");
20//! println!("API Key: {}", key.raw_key); // show once
21//! // Store key.prefix and key.hashed_key in database
22//!
23//! // Protect routes with middleware
24//! group!("/api/v1")
25//!     .middleware(ApiKeyMiddleware::new())
26//!     .routes([...]);
27//!
28//! // Require specific scopes
29//! group!("/api/v1/admin")
30//!     .middleware(ApiKeyMiddleware::scopes(&["admin"]))
31//!     .routes([...]);
32//! ```
33
34use crate::container::App;
35use crate::http::{HttpResponse, Request, Response};
36use crate::middleware::{Middleware, Next};
37use async_trait::async_trait;
38use sha2::{Digest, Sha256};
39use std::sync::Arc;
40use subtle::ConstantTimeEq;
41
42const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
43
44/// Result of generating a new API key.
45///
46/// `raw_key` should be shown to the user exactly once.
47/// `prefix` and `hashed_key` are stored in the database.
48#[derive(Debug, Clone)]
49pub struct GeneratedApiKey {
50    /// Full key (display once, never store)
51    pub raw_key: String,
52    /// First 16 characters for database lookup
53    pub prefix: String,
54    /// SHA-256 hex digest for verification
55    pub hashed_key: String,
56}
57
58/// Information about an authenticated API key, available in request extensions.
59#[derive(Debug, Clone)]
60pub struct ApiKeyInfo {
61    /// Database identifier for the key
62    pub id: i64,
63    /// Human-readable name (e.g., "Production Bot")
64    pub name: String,
65    /// Granted scopes (e.g., ["read", "write"])
66    pub scopes: Vec<String>,
67}
68
69/// Storage-agnostic API key verification.
70///
71/// Implement this trait to connect the middleware to your key store
72/// (database, cache, etc.).
73///
74/// # Example
75///
76/// ```rust,ignore
77/// use ferro_rs::{async_trait, ApiKeyProvider, ApiKeyInfo};
78///
79/// pub struct DbApiKeyProvider;
80///
81/// #[async_trait]
82/// impl ApiKeyProvider for DbApiKeyProvider {
83///     async fn verify_key(&self, raw_key: &str) -> Result<ApiKeyInfo, ()> {
84///         let prefix = &raw_key[..16.min(raw_key.len())];
85///         let record = api_key::Entity::find()
86///             .filter(api_key::Column::Prefix.eq(prefix))
87///             .one(&db())
88///             .await
89///             .map_err(|_| ())?
90///             .ok_or(())?;
91///
92///         if verify_api_key_hash(raw_key, &record.hashed_key) {
93///             Ok(ApiKeyInfo {
94///                 id: record.id,
95///                 name: record.name,
96///                 scopes: serde_json::from_str(&record.scopes).unwrap_or_default(),
97///             })
98///         } else {
99///             Err(())
100///         }
101///     }
102/// }
103/// ```
104#[async_trait]
105pub trait ApiKeyProvider: Send + Sync + 'static {
106    /// Look up and verify the raw key, returning key metadata on success.
107    async fn verify_key(&self, raw_key: &str) -> Result<ApiKeyInfo, ()>;
108}
109
110/// Generate a new API key for the given environment.
111///
112/// Returns a [`GeneratedApiKey`] with the raw key (show once), a prefix for
113/// database lookup, and a SHA-256 hash for storage.
114pub fn generate_api_key(environment: &str) -> GeneratedApiKey {
115    let prefix_str = format!("fe_{environment}_");
116    let mut rng = rand::thread_rng();
117    let random: String = (0..43)
118        .map(|_| {
119            let idx = rand::Rng::gen_range(&mut rng, 0..62);
120            BASE62[idx] as char
121        })
122        .collect();
123
124    let raw_key = format!("{prefix_str}{random}");
125    let prefix = raw_key[..16].to_string();
126    let hashed_key = hash_api_key(&raw_key);
127
128    GeneratedApiKey {
129        raw_key,
130        prefix,
131        hashed_key,
132    }
133}
134
135/// Compute the SHA-256 hex digest of a raw API key.
136pub fn hash_api_key(raw_key: &str) -> String {
137    let mut hasher = Sha256::new();
138    hasher.update(raw_key.as_bytes());
139    format!("{:x}", hasher.finalize())
140}
141
142/// Constant-time comparison of a raw key against a stored hash.
143///
144/// Prevents timing attacks by using `subtle::ConstantTimeEq`.
145pub fn verify_api_key_hash(raw_key: &str, stored_hash: &str) -> bool {
146    let incoming_hash = hash_api_key(raw_key);
147    incoming_hash
148        .as_bytes()
149        .ct_eq(stored_hash.as_bytes())
150        .into()
151}
152
153/// Middleware that authenticates requests via API key in the Authorization header.
154///
155/// Extracts `Bearer {key}` from the `Authorization` header, resolves an
156/// [`ApiKeyProvider`] from the service container, and verifies the key.
157/// On success, stores [`ApiKeyInfo`] in request extensions.
158///
159/// # Example
160///
161/// ```rust,ignore
162/// // Require any valid API key
163/// group!("/api/v1")
164///     .middleware(ApiKeyMiddleware::new())
165///     .routes([...]);
166///
167/// // Require specific scopes
168/// group!("/api/v1/admin")
169///     .middleware(ApiKeyMiddleware::scopes(&["admin"]))
170///     .routes([...]);
171/// ```
172pub struct ApiKeyMiddleware {
173    required_scopes: Vec<String>,
174}
175
176impl ApiKeyMiddleware {
177    /// Create middleware that accepts any valid API key.
178    pub fn new() -> Self {
179        Self {
180            required_scopes: Vec::new(),
181        }
182    }
183
184    /// Create middleware that requires specific scopes.
185    pub fn scopes(scopes: &[&str]) -> Self {
186        Self {
187            required_scopes: scopes.iter().map(|s| s.to_string()).collect(),
188        }
189    }
190}
191
192impl Default for ApiKeyMiddleware {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198/// Extract the bearer token from the Authorization header.
199fn extract_bearer_token(request: &Request) -> Result<&str, HttpResponse> {
200    let header = request.header("Authorization").ok_or_else(|| {
201        HttpResponse::json(serde_json::json!({
202            "error": "API key required",
203            "hint": "Include Authorization: Bearer <key> header"
204        }))
205        .status(401)
206    })?;
207
208    let token = header.strip_prefix("Bearer ").ok_or_else(|| {
209        HttpResponse::json(serde_json::json!({
210            "error": "Invalid API key format"
211        }))
212        .status(401)
213    })?;
214
215    if token.is_empty() {
216        return Err(HttpResponse::json(serde_json::json!({
217            "error": "Invalid API key format"
218        }))
219        .status(401));
220    }
221
222    Ok(token)
223}
224
225#[async_trait]
226impl Middleware for ApiKeyMiddleware {
227    async fn handle(&self, mut request: Request, next: Next) -> Response {
228        let raw_key = extract_bearer_token(&request)?;
229
230        let provider: Arc<dyn ApiKeyProvider> =
231            App::make::<dyn ApiKeyProvider>().ok_or_else(|| {
232                HttpResponse::json(serde_json::json!({
233                    "error": "API key authentication not configured"
234                }))
235                .status(500)
236            })?;
237
238        let key_info = provider.verify_key(raw_key).await.map_err(|()| {
239            HttpResponse::json(serde_json::json!({
240                "error": "Invalid API key"
241            }))
242            .status(401)
243        })?;
244
245        // Check scopes if required
246        if !self.required_scopes.is_empty() {
247            let has_wildcard = key_info.scopes.iter().any(|s| s == "*");
248            if !has_wildcard {
249                let missing: Vec<&String> = self
250                    .required_scopes
251                    .iter()
252                    .filter(|required| !key_info.scopes.contains(required))
253                    .collect();
254
255                if !missing.is_empty() {
256                    return Err(HttpResponse::json(serde_json::json!({
257                        "error": "Insufficient permissions",
258                        "required": self.required_scopes,
259                        "provided": key_info.scopes
260                    }))
261                    .status(403));
262                }
263            }
264        }
265
266        request.insert(key_info);
267        next(request).await
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn generate_api_key_format() {
277        let key = generate_api_key("live");
278        assert!(key.raw_key.starts_with("fe_live_"));
279        // fe_live_ = 8 chars + 43 random = 51 total
280        assert_eq!(key.raw_key.len(), 51);
281    }
282
283    #[test]
284    fn generate_api_key_test_env() {
285        let key = generate_api_key("test");
286        assert!(key.raw_key.starts_with("fe_test_"));
287        assert_eq!(key.raw_key.len(), 51);
288    }
289
290    #[test]
291    fn generate_api_key_prefix_length() {
292        let key = generate_api_key("live");
293        assert_eq!(key.prefix.len(), 16);
294        assert!(key.prefix.starts_with("fe_live_"));
295    }
296
297    #[test]
298    fn generate_api_key_hash_is_sha256_hex() {
299        let key = generate_api_key("live");
300        // SHA-256 hex = 64 characters
301        assert_eq!(key.hashed_key.len(), 64);
302        assert!(key.hashed_key.chars().all(|c| c.is_ascii_hexdigit()));
303    }
304
305    #[test]
306    fn generate_api_key_uniqueness() {
307        let key1 = generate_api_key("live");
308        let key2 = generate_api_key("live");
309        assert_ne!(key1.raw_key, key2.raw_key);
310        assert_ne!(key1.hashed_key, key2.hashed_key);
311    }
312
313    #[test]
314    fn generate_api_key_base62_chars_only() {
315        let key = generate_api_key("live");
316        let random_part = &key.raw_key[8..]; // skip "fe_live_"
317        assert!(random_part.chars().all(|c| c.is_ascii_alphanumeric()));
318    }
319
320    #[test]
321    fn hash_api_key_deterministic() {
322        let hash1 = hash_api_key("fe_live_abc123");
323        let hash2 = hash_api_key("fe_live_abc123");
324        assert_eq!(hash1, hash2);
325    }
326
327    #[test]
328    fn hash_api_key_different_inputs() {
329        let hash1 = hash_api_key("fe_live_abc123");
330        let hash2 = hash_api_key("fe_live_xyz789");
331        assert_ne!(hash1, hash2);
332    }
333
334    #[test]
335    fn verify_api_key_hash_correct() {
336        let key = generate_api_key("live");
337        assert!(verify_api_key_hash(&key.raw_key, &key.hashed_key));
338    }
339
340    #[test]
341    fn verify_api_key_hash_wrong_key() {
342        let key = generate_api_key("live");
343        assert!(!verify_api_key_hash("wrong_key", &key.hashed_key));
344    }
345
346    #[test]
347    fn verify_api_key_hash_wrong_hash() {
348        let key = generate_api_key("live");
349        assert!(!verify_api_key_hash(
350            &key.raw_key,
351            "0000000000000000000000000000000000000000000000000000000000000000"
352        ));
353    }
354
355    #[test]
356    fn verify_api_key_hash_manual() {
357        let raw = "fe_test_SomeRandomBase62String";
358        let hash = hash_api_key(raw);
359        assert!(verify_api_key_hash(raw, &hash));
360    }
361}