micromegas_auth/
api_key.rs

1use crate::types::{AuthContext, AuthProvider, AuthType};
2use anyhow::{Result, anyhow};
3use serde::Deserialize;
4use std::{collections::HashMap, fmt::Display};
5use subtle::ConstantTimeEq;
6
7/// Represents a key in the keyring.
8#[derive(Hash, Eq, PartialEq)]
9pub struct Key {
10    /// The key value
11    pub value: String,
12}
13
14impl Key {
15    /// Creates a new `Key` from a string value.
16    pub fn new(value: String) -> Self {
17        Self { value }
18    }
19}
20
21impl From<String> for Key {
22    fn from(value: String) -> Self {
23        Self { value }
24    }
25}
26
27impl Display for Key {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "<sensitive key>")
30    }
31}
32
33/// Deserializes a string into a `Key`.
34fn key_from_string<'de, D>(deserializer: D) -> Result<Key, D::Error>
35where
36    D: serde::Deserializer<'de>,
37{
38    let s: String = Deserialize::deserialize(deserializer)?;
39    Ok(Key::new(s))
40}
41
42/// Represents an entry in the keyring, mapping a key to a name.
43#[derive(Deserialize)]
44pub struct KeyRingEntry {
45    /// The name associated with the key
46    pub name: String,
47    /// The key
48    #[serde(deserialize_with = "key_from_string")]
49    pub key: Key,
50}
51
52/// A map from `Key` to `String` (name).
53pub type KeyRing = HashMap<Key, String>;
54
55/// Parses a JSON string into a `KeyRing`.
56///
57/// The JSON string is expected to be an array of objects, each with a `name` and `key` field.
58pub fn parse_key_ring(json: &str) -> Result<KeyRing> {
59    let entries: Vec<KeyRingEntry> = serde_json::from_str(json)?;
60    let mut ring = KeyRing::new();
61    for entry in entries {
62        ring.insert(entry.key, entry.name);
63    }
64    Ok(ring)
65}
66
67/// API key authentication provider
68pub struct ApiKeyAuthProvider {
69    keyring: KeyRing,
70}
71
72impl ApiKeyAuthProvider {
73    /// Create a new API key authentication provider
74    pub fn new(keyring: KeyRing) -> Self {
75        Self { keyring }
76    }
77}
78
79#[async_trait::async_trait]
80impl AuthProvider for ApiKeyAuthProvider {
81    /// Validate an API key request using constant-time comparison
82    ///
83    /// This implementation protects against timing attacks by:
84    /// 1. Comparing the provided token against ALL keys in the keyring
85    /// 2. Using constant-time comparison from the `subtle` crate
86    /// 3. Always iterating through all keys regardless of match status
87    ///
88    /// This ensures the operation takes the same amount of time whether:
89    /// - The key is found early in the iteration
90    /// - The key is found late in the iteration
91    /// - The key is not found at all
92    async fn validate_request(
93        &self,
94        parts: &dyn crate::types::RequestParts,
95    ) -> Result<AuthContext> {
96        let token = parts
97            .bearer_token()
98            .ok_or_else(|| anyhow!("missing bearer token"))?;
99
100        let token_bytes = token.as_bytes();
101        let mut found: Option<AuthContext> = None;
102
103        // Compare against all keys in constant time
104        // IMPORTANT: We iterate through ALL keys, even if we find a match,
105        // to ensure constant-time operation
106        for (stored_key, name) in &self.keyring {
107            let stored_bytes = stored_key.value.as_bytes();
108
109            // Constant-time comparison
110            // Returns 1 if equal, 0 if not equal
111            let matches = token_bytes.ct_eq(stored_bytes).unwrap_u8() == 1;
112
113            // Conditionally set the result without branching on the match
114            // If matches is true, we set found; if matches is false, found stays as-is
115            if matches {
116                found = Some(AuthContext {
117                    subject: name.clone(),
118                    email: None,
119                    issuer: "api_key".to_string(),
120                    audience: None,
121                    expires_at: None,
122                    auth_type: AuthType::ApiKey,
123                    // SECURITY: API keys can NEVER be admins - only OIDC users can have admin privileges
124                    is_admin: false,
125                    // SECURITY: API keys CAN delegate (act on behalf of users)
126                    allow_delegation: true,
127                });
128            }
129            // Note: We do NOT break or return early - we continue checking all keys
130        }
131
132        found.ok_or_else(|| anyhow!("invalid API token"))
133    }
134}