Skip to main content

rustauth_plugins/api_key/
options.rs

1use std::collections::{BTreeMap, HashSet};
2use std::fmt;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use rustauth_core::context::AuthContext;
8use rustauth_core::error::RustAuthError;
9use rustauth_core::options::SecondaryStorage;
10use rustauth_core::plugin::PluginRequest;
11use serde::{Deserialize, Serialize};
12use time::Duration;
13
14mod duration_serde {
15    use serde::{Deserialize, Deserializer, Serialize, Serializer};
16    use time::Duration;
17
18    pub mod as_millis {
19        use super::*;
20
21        pub fn serialize<S: Serializer>(
22            duration: &Duration,
23            serializer: S,
24        ) -> Result<S::Ok, S::Error> {
25            duration.whole_milliseconds().serialize(serializer)
26        }
27
28        pub fn deserialize<'de, D: Deserializer<'de>>(
29            deserializer: D,
30        ) -> Result<Duration, D::Error> {
31            let millis = i64::deserialize(deserializer)?;
32            Ok(Duration::milliseconds(millis))
33        }
34    }
35
36    pub mod as_secs_optional {
37        use super::*;
38
39        pub fn serialize<S: Serializer>(
40            duration: &Option<Duration>,
41            serializer: S,
42        ) -> Result<S::Ok, S::Error> {
43            match duration {
44                Some(value) => value.whole_seconds().serialize(serializer),
45                None => serializer.serialize_none(),
46            }
47        }
48
49        pub fn deserialize<'de, D: Deserializer<'de>>(
50            deserializer: D,
51        ) -> Result<Option<Duration>, D::Error> {
52            let seconds = Option::<i64>::deserialize(deserializer)?;
53            Ok(seconds.map(Duration::seconds))
54        }
55    }
56}
57
58pub type ApiKeyPermissions = BTreeMap<String, Vec<String>>;
59pub type ApiKeyGeneratorFuture =
60    Pin<Box<dyn Future<Output = Result<String, RustAuthError>> + Send + 'static>>;
61pub type ApiKeyGenerator = Arc<dyn Fn(ApiKeyGeneratorInput) -> ApiKeyGeneratorFuture + Send + Sync>;
62pub type ApiKeyGetterFuture<'a> =
63    Pin<Box<dyn Future<Output = Result<Option<String>, RustAuthError>> + Send + 'a>>;
64pub type ApiKeyGetter =
65    Arc<dyn for<'a> Fn(&'a AuthContext, &'a PluginRequest) -> ApiKeyGetterFuture<'a> + Send + Sync>;
66pub type ApiKeyValidatorFuture<'a> =
67    Pin<Box<dyn Future<Output = Result<bool, RustAuthError>> + Send + 'a>>;
68pub type ApiKeyValidator =
69    Arc<dyn for<'a> Fn(&'a AuthContext, &'a str) -> ApiKeyValidatorFuture<'a> + Send + Sync>;
70pub type DefaultPermissionsFuture<'a> =
71    Pin<Box<dyn Future<Output = Result<Option<ApiKeyPermissions>, RustAuthError>> + Send + 'a>>;
72pub type DefaultPermissionsResolver =
73    Arc<dyn for<'a> Fn(&'a AuthContext, &'a str) -> DefaultPermissionsFuture<'a> + Send + Sync>;
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ApiKeyGeneratorInput {
77    pub length: usize,
78    pub prefix: Option<String>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub enum ApiKeyStorageMode {
84    Database,
85    SecondaryStorage,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub enum ApiKeyReference {
91    User,
92    Organization,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct ApiKeyRateLimitOptions {
98    pub enabled: bool,
99    #[serde(with = "duration_serde::as_millis")]
100    pub time_window: Duration,
101    pub max_requests: i64,
102}
103
104impl Default for ApiKeyRateLimitOptions {
105    fn default() -> Self {
106        Self {
107            enabled: true,
108            time_window: Duration::days(1),
109            max_requests: 10,
110        }
111    }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct ApiKeyExpirationOptions {
117    #[serde(with = "duration_serde::as_secs_optional")]
118    pub default_expires_in: Option<Duration>,
119    pub disable_custom_expires_time: bool,
120    pub min_expires_in_days: i64,
121    pub max_expires_in_days: i64,
122}
123
124impl Default for ApiKeyExpirationOptions {
125    fn default() -> Self {
126        Self {
127            default_expires_in: None,
128            disable_custom_expires_time: false,
129            min_expires_in_days: 1,
130            max_expires_in_days: 365,
131        }
132    }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct StartingCharactersConfig {
138    pub should_store: bool,
139    pub characters_length: usize,
140}
141
142impl Default for StartingCharactersConfig {
143    fn default() -> Self {
144        Self {
145            should_store: true,
146            characters_length: 6,
147        }
148    }
149}
150
151#[derive(Clone, Serialize, Deserialize)]
152#[serde(default, rename_all = "camelCase")]
153pub struct ApiKeyConfiguration {
154    pub config_id: Option<String>,
155    pub api_key_headers: Vec<String>,
156    pub disable_key_hashing: bool,
157    pub default_key_length: usize,
158    pub default_prefix: Option<String>,
159    pub maximum_prefix_length: usize,
160    pub minimum_prefix_length: usize,
161    pub require_name: bool,
162    pub maximum_name_length: usize,
163    pub minimum_name_length: usize,
164    pub enable_metadata: bool,
165    pub key_expiration: ApiKeyExpirationOptions,
166    pub rate_limit: ApiKeyRateLimitOptions,
167    pub enable_session_for_api_keys: bool,
168    pub default_permissions: Option<ApiKeyPermissions>,
169    #[serde(skip)]
170    pub default_permissions_resolver: Option<DefaultPermissionsResolver>,
171    #[serde(skip)]
172    pub custom_key_generator: Option<ApiKeyGenerator>,
173    #[serde(skip)]
174    pub custom_api_key_getter: Option<ApiKeyGetter>,
175    #[serde(skip)]
176    pub custom_api_key_validator: Option<ApiKeyValidator>,
177    pub storage: ApiKeyStorageMode,
178    pub fallback_to_database: bool,
179    /// When `true` (and [`Self::fallback_to_database`] is enabled), reads that
180    /// hit the secondary-storage cache are reconciled against the database
181    /// before being returned: a missing row is treated as revoked and a newer
182    /// `updated_at` refreshes the cache. This trades a per-read database lookup
183    /// for immediate revocation of out-of-band database edits and keys without
184    /// a TTL. Defaults to `false` to preserve the cache-first behavior that
185    /// matches upstream Better Auth.
186    pub revalidate_secondary_against_database: bool,
187    #[serde(skip)]
188    pub custom_storage: Option<Arc<dyn SecondaryStorage>>,
189    pub defer_updates: bool,
190    pub reference: ApiKeyReference,
191    pub starting_characters: StartingCharactersConfig,
192}
193
194impl Default for ApiKeyConfiguration {
195    fn default() -> Self {
196        Self {
197            config_id: None,
198            api_key_headers: vec!["x-api-key".to_owned()],
199            disable_key_hashing: false,
200            default_key_length: 64,
201            default_prefix: None,
202            maximum_prefix_length: 32,
203            minimum_prefix_length: 1,
204            require_name: false,
205            maximum_name_length: 32,
206            minimum_name_length: 1,
207            enable_metadata: false,
208            key_expiration: ApiKeyExpirationOptions::default(),
209            rate_limit: ApiKeyRateLimitOptions::default(),
210            enable_session_for_api_keys: false,
211            default_permissions: None,
212            default_permissions_resolver: None,
213            custom_key_generator: None,
214            custom_api_key_getter: None,
215            custom_api_key_validator: None,
216            storage: ApiKeyStorageMode::Database,
217            fallback_to_database: false,
218            revalidate_secondary_against_database: false,
219            custom_storage: None,
220            defer_updates: false,
221            reference: ApiKeyReference::User,
222            starting_characters: StartingCharactersConfig::default(),
223        }
224    }
225}
226
227impl fmt::Debug for ApiKeyConfiguration {
228    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229        formatter
230            .debug_struct("ApiKeyConfiguration")
231            .field("config_id", &self.config_id)
232            .field("api_key_headers", &self.api_key_headers)
233            .field("disable_key_hashing", &self.disable_key_hashing)
234            .field("default_key_length", &self.default_key_length)
235            .field("default_prefix", &self.default_prefix)
236            .field("maximum_prefix_length", &self.maximum_prefix_length)
237            .field("minimum_prefix_length", &self.minimum_prefix_length)
238            .field("require_name", &self.require_name)
239            .field("maximum_name_length", &self.maximum_name_length)
240            .field("minimum_name_length", &self.minimum_name_length)
241            .field("enable_metadata", &self.enable_metadata)
242            .field("key_expiration", &self.key_expiration)
243            .field("rate_limit", &self.rate_limit)
244            .field(
245                "enable_session_for_api_keys",
246                &self.enable_session_for_api_keys,
247            )
248            .field("default_permissions", &self.default_permissions)
249            .field(
250                "custom_key_generator",
251                &self
252                    .custom_key_generator
253                    .as_ref()
254                    .map(|_| "<custom-key-generator>"),
255            )
256            .field(
257                "custom_api_key_getter",
258                &self
259                    .custom_api_key_getter
260                    .as_ref()
261                    .map(|_| "<custom-api-key-getter>"),
262            )
263            .field(
264                "custom_api_key_validator",
265                &self
266                    .custom_api_key_validator
267                    .as_ref()
268                    .map(|_| "<custom-api-key-validator>"),
269            )
270            .field("storage", &self.storage)
271            .field("fallback_to_database", &self.fallback_to_database)
272            .field(
273                "revalidate_secondary_against_database",
274                &self.revalidate_secondary_against_database,
275            )
276            .field(
277                "custom_storage",
278                &self.custom_storage.as_ref().map(|_| "<custom-storage>"),
279            )
280            .field("defer_updates", &self.defer_updates)
281            .field("reference", &self.reference)
282            .field("starting_characters", &self.starting_characters)
283            .finish()
284    }
285}
286
287#[derive(Debug, Clone, Default, Serialize, Deserialize)]
288#[serde(default, rename_all = "camelCase")]
289pub struct ApiKeyOptions {
290    pub configurations: Vec<ApiKeyConfiguration>,
291    pub schema: crate::api_key::schema::ApiKeySchemaOptions,
292}
293
294impl ApiKeyOptions {
295    #[must_use]
296    pub fn builder() -> ApiKeyOptionsBuilder {
297        ApiKeyOptionsBuilder::default()
298    }
299
300    pub(crate) fn resolve(self) -> Result<ResolvedConfigurations, RustAuthError> {
301        if self.configurations.is_empty() {
302            return Ok(ResolvedConfigurations::with_schema(
303                ApiKeyConfiguration::default(),
304                self.schema,
305            ));
306        }
307        if self.configurations.len() == 1 {
308            return Ok(ResolvedConfigurations::with_schema(
309                self.configurations[0].clone(),
310                self.schema,
311            ));
312        }
313        ResolvedConfigurations::multiple(self.configurations, self.schema).map_err(Into::into)
314    }
315
316    pub fn validate(&self) -> Result<(), RustAuthError> {
317        if self.configurations.len() <= 1 {
318            return Ok(());
319        }
320        ResolvedConfigurations::multiple(self.configurations.clone(), self.schema.clone())?;
321        Ok(())
322    }
323}
324
325#[derive(Debug, Clone, Default)]
326pub struct ApiKeyOptionsBuilder {
327    configurations: Vec<ApiKeyConfiguration>,
328    schema: crate::api_key::schema::ApiKeySchemaOptions,
329}
330
331impl ApiKeyOptionsBuilder {
332    #[must_use]
333    pub fn configuration(mut self, configuration: ApiKeyConfiguration) -> Self {
334        self.configurations = vec![configuration];
335        self
336    }
337
338    #[must_use]
339    pub fn configurations(mut self, configurations: Vec<ApiKeyConfiguration>) -> Self {
340        self.configurations = configurations;
341        self
342    }
343
344    #[must_use]
345    pub fn schema(mut self, schema: crate::api_key::schema::ApiKeySchemaOptions) -> Self {
346        self.schema = schema;
347        self
348    }
349
350    pub fn build(self) -> Result<ApiKeyOptions, RustAuthError> {
351        let options = ApiKeyOptions {
352            configurations: self.configurations,
353            schema: self.schema,
354        };
355        options.validate()?;
356        Ok(options)
357    }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
361pub enum ApiKeyOptionsError {
362    #[error("config_id is required for each API key configuration in the api-key plugin")]
363    MissingConfigId,
364    #[error("config_id must be unique for each API key configuration in the api-key plugin")]
365    DuplicateConfigId,
366}
367
368impl From<ApiKeyOptionsError> for RustAuthError {
369    fn from(error: ApiKeyOptionsError) -> Self {
370        Self::InvalidConfig(error.to_string())
371    }
372}
373
374#[derive(Debug, Clone)]
375pub(crate) struct ResolvedConfigurations {
376    configurations: Vec<ApiKeyConfiguration>,
377    schema: crate::api_key::schema::ApiKeySchemaOptions,
378}
379
380impl ResolvedConfigurations {
381    pub fn with_schema(
382        configuration: ApiKeyConfiguration,
383        schema: crate::api_key::schema::ApiKeySchemaOptions,
384    ) -> Self {
385        Self {
386            configurations: vec![configuration],
387            schema,
388        }
389    }
390
391    pub fn multiple(
392        configurations: Vec<ApiKeyConfiguration>,
393        schema: crate::api_key::schema::ApiKeySchemaOptions,
394    ) -> Result<Self, ApiKeyOptionsError> {
395        let mut seen = HashSet::new();
396        for configuration in &configurations {
397            let Some(config_id) = configuration.config_id.as_deref() else {
398                return Err(ApiKeyOptionsError::MissingConfigId);
399            };
400            if !seen.insert(config_id.to_owned()) {
401                return Err(ApiKeyOptionsError::DuplicateConfigId);
402            }
403        }
404        Ok(Self {
405            configurations,
406            schema,
407        })
408    }
409
410    pub fn schema(&self) -> &crate::api_key::schema::ApiKeySchemaOptions {
411        &self.schema
412    }
413
414    pub fn all(&self) -> &[ApiKeyConfiguration] {
415        &self.configurations
416    }
417
418    pub fn resolve(&self, config_id: Option<&str>) -> Result<ApiKeyConfiguration, RustAuthError> {
419        if let Some(config_id) = config_id {
420            if let Some(configuration) = self
421                .configurations
422                .iter()
423                .find(|configuration| configuration.config_id.as_deref() == Some(config_id))
424            {
425                return Ok(with_default_config_id(configuration.clone()));
426            }
427        }
428
429        self.configurations
430            .iter()
431            .find(|configuration| {
432                configuration.config_id.is_none()
433                    || configuration.config_id.as_deref() == Some("default")
434            })
435            .cloned()
436            .map(with_default_config_id)
437            .ok_or_else(|| {
438                RustAuthError::Api(
439                    crate::api_key::errors::message(
440                        crate::api_key::errors::NO_DEFAULT_API_KEY_CONFIGURATION_FOUND,
441                    )
442                    .to_owned(),
443                )
444            })
445    }
446}
447
448fn with_default_config_id(mut configuration: ApiKeyConfiguration) -> ApiKeyConfiguration {
449    if configuration.config_id.is_none() {
450        configuration.config_id = Some("default".to_owned());
451    }
452    configuration
453}