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}