Skip to main content

better_auth_api/plugins/api_key/
mod.rs

1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use rand::seq::SliceRandom;
4use sha2::{Digest, Sha256};
5use std::sync::{Arc, Mutex};
6
7use better_auth_core::entity::AuthUser;
8use better_auth_core::{AuthContext, AuthError, AuthResult, BeforeRequestAction};
9use better_auth_core::{AuthRequest, AuthResponse};
10
11pub(super) mod handlers;
12pub(crate) mod storage;
13pub use storage::ApiKeyStorage;
14mod callbacks;
15mod metadata;
16mod request;
17pub(super) mod types;
18mod verification;
19
20pub use callbacks::{
21    ApiKeyDefaultPermissions, ApiKeyEndpoint, ApiKeyGenerator, ApiKeyGetter, ApiKeyPermissions,
22    ApiKeyValidator,
23};
24
25pub use verification::{
26    ApiKeyErrorDetails, ApiKeyErrorMessage, ApiKeyValidationError, ApiKeyVerificationError,
27    VerifyApiKey,
28};
29
30#[cfg(test)]
31mod tests;
32
33#[cfg(test)]
34mod crud_tests;
35
36#[cfg(test)]
37mod expiration_tests;
38
39use handlers::*;
40use types::*;
41pub use types::{CreateKeyRequest, CreateKeyResponse, UpdateKeyRequest};
42
43// ---------------------------------------------------------------------------
44// Error codes -- mirrors the TypeScript `API_KEY_ERROR_CODES`
45// ---------------------------------------------------------------------------
46
47/// Dedicated API Key error codes aligned with the TypeScript `API_KEY_ERROR_CODES`.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ApiKeyErrorCode {
50    InvalidApiKey,
51    KeyDisabled,
52    KeyExpired,
53    UsageExceeded,
54    KeyNotFound,
55    RateLimited,
56    UnauthorizedSession,
57    InvalidPrefixLength,
58    InvalidNameLength,
59    MetadataDisabled,
60    NoValuesToUpdate,
61    KeyDisabledExpiration,
62    ExpiresInTooSmall,
63    ExpiresInTooLarge,
64    InvalidRemaining,
65    RefillAmountAndIntervalRequired,
66    RefillIntervalAndAmountRequired,
67    NameRequired,
68    InvalidUserIdFromApiKey,
69    InvalidReferenceIdFromApiKey,
70    NoDefaultConfiguration,
71    OrganizationIdRequired,
72    OrganizationPluginRequired,
73    UserNotMemberOfOrganization,
74    InsufficientApiKeyPermissions,
75    ServerOnlyProperty,
76    FailedToUpdateApiKey,
77    InvalidMetadataType,
78}
79
80impl ApiKeyErrorCode {
81    pub fn as_str(self) -> &'static str {
82        match self {
83            Self::InvalidApiKey => "INVALID_API_KEY",
84            Self::KeyDisabled => "KEY_DISABLED",
85            Self::KeyExpired => "KEY_EXPIRED",
86            Self::UsageExceeded => "USAGE_EXCEEDED",
87            Self::KeyNotFound => "KEY_NOT_FOUND",
88            Self::RateLimited => "RATE_LIMITED",
89            Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
90            Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
91            Self::InvalidNameLength => "INVALID_NAME_LENGTH",
92            Self::MetadataDisabled => "METADATA_DISABLED",
93            Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
94            Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
95            Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
96            Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
97            Self::InvalidRemaining => "INVALID_REMAINING",
98            Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
99            Self::RefillIntervalAndAmountRequired => "REFILL_INTERVAL_AND_AMOUNT_REQUIRED",
100            Self::NameRequired => "NAME_REQUIRED",
101            Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
102            Self::InvalidReferenceIdFromApiKey => "INVALID_REFERENCE_ID_FROM_API_KEY",
103            Self::NoDefaultConfiguration => "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND",
104            Self::OrganizationIdRequired => "ORGANIZATION_ID_REQUIRED",
105            Self::OrganizationPluginRequired => "ORGANIZATION_PLUGIN_REQUIRED",
106            Self::UserNotMemberOfOrganization => "USER_NOT_MEMBER_OF_ORGANIZATION",
107            Self::InsufficientApiKeyPermissions => "INSUFFICIENT_API_KEY_PERMISSIONS",
108            Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
109            Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
110            Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
111        }
112    }
113
114    pub fn message(self) -> &'static str {
115        match self {
116            Self::InvalidApiKey => "Invalid API key.",
117            Self::KeyDisabled => "API Key is disabled",
118            Self::KeyExpired => "API Key has expired",
119            Self::UsageExceeded => "API Key has reached its usage limit",
120            Self::KeyNotFound => "API Key not found",
121            Self::RateLimited => "Rate limit exceeded.",
122            Self::UnauthorizedSession => "Unauthorized or invalid session",
123            Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
124            Self::InvalidNameLength => "The name length is either too large or too small.",
125            Self::MetadataDisabled => "Metadata is disabled.",
126            Self::NoValuesToUpdate => "No values to update.",
127            Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
128            Self::ExpiresInTooSmall => {
129                "The expiresIn is smaller than the predefined minimum value."
130            }
131            Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
132            Self::InvalidRemaining => "The remaining count is either too large or too small.",
133            Self::RefillAmountAndIntervalRequired => {
134                "refillAmount is required when refillInterval is provided"
135            }
136            Self::RefillIntervalAndAmountRequired => {
137                "refillInterval is required when refillAmount is provided"
138            }
139            Self::NameRequired => "API Key name is required.",
140            Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
141            Self::InvalidReferenceIdFromApiKey => "The reference id from the API key is invalid.",
142            Self::NoDefaultConfiguration => "No default api-key configuration found.",
143            Self::OrganizationIdRequired => {
144                "Organization ID is required for organization-owned API keys."
145            }
146            Self::OrganizationPluginRequired => {
147                "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
148            }
149            Self::UserNotMemberOfOrganization => {
150                "You are not a member of the organization that owns this API key."
151            }
152            Self::InsufficientApiKeyPermissions => {
153                "You do not have permission to perform this action on organization API keys."
154            }
155            Self::ServerOnlyProperty => {
156                "The property you're trying to set can only be set from the server auth instance only."
157            }
158            Self::FailedToUpdateApiKey => "Failed to update API key",
159            Self::InvalidMetadataType => "metadata must be an object or undefined",
160        }
161    }
162}
163
164impl serde::Serialize for ApiKeyErrorCode {
165    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166        serializer.serialize_str(self.as_str())
167    }
168}
169
170pub(super) fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
171    let status = match code {
172        ApiKeyErrorCode::UnauthorizedSession => 401,
173        ApiKeyErrorCode::OrganizationPluginRequired => 500,
174        ApiKeyErrorCode::UserNotMemberOfOrganization
175        | ApiKeyErrorCode::InsufficientApiKeyPermissions => 403,
176        _ => 400,
177    };
178    AuthError::Upstream {
179        status,
180        code: code.as_str(),
181        message: code.message(),
182    }
183}
184
185// ---------------------------------------------------------------------------
186// Configuration
187// ---------------------------------------------------------------------------
188
189/// Which kind of entity a configuration's keys belong to.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum ApiKeyReferences {
192    /// Keys are owned by the signed-in user.
193    #[default]
194    User,
195    /// Keys are owned by an organization; callers pass `organizationId` and
196    /// must hold the matching `apiKey` permission in that organization.
197    Organization,
198}
199
200/// API Key management plugin.
201#[derive(Clone)]
202pub struct ApiKeyPlugin {
203    /// Registered configurations. Unscoped requests use the default configuration.
204    pub(super) configurations: Vec<ApiKeyConfig>,
205    /// Throttle for `delete_expired_api_keys` -- stores the last check instant.
206    last_expired_check: Arc<Mutex<Option<std::time::Instant>>>,
207}
208
209impl ApiKeyPlugin {
210    /// Register an additional named configuration.
211    ///
212    /// Upstream allows several api-key configurations side by side, each with
213    /// its own `config_id`, ownership model and limits.
214    pub fn configuration(mut self, config: ApiKeyConfig) -> Self {
215        self.configurations.push(config.normalized());
216        self
217    }
218
219    /// Pick the configuration a request addressed, mirroring upstream's
220    /// `resolveConfiguration`: an unknown or absent `config_id` falls back to
221    /// the default one, and a missing default is a client error.
222    pub(super) fn resolve_configuration(
223        &self,
224        config_id: Option<&str>,
225    ) -> AuthResult<&ApiKeyConfig> {
226        if let Some(config_id) = config_id
227            && let Some(found) = self
228                .configurations
229                .iter()
230                .find(|config| config.config_id == config_id)
231        {
232            return Ok(found);
233        }
234
235        self.configurations
236            .iter()
237            .find(|config| is_default_config_id(&config.config_id))
238            .ok_or_else(|| api_key_error(ApiKeyErrorCode::NoDefaultConfiguration))
239    }
240}
241
242/// Keys written before `config_id` existed carry no value, so absent and
243/// `"default"` denote the same configuration.
244pub(super) fn is_default_config_id(config_id: &str) -> bool {
245    config_id.is_empty() || config_id == "default"
246}
247
248/// Whether a stored key belongs to the addressed configuration.
249pub(super) fn config_id_matches(key_config_id: &str, expected: &str) -> bool {
250    if is_default_config_id(key_config_id) && is_default_config_id(expected) {
251        return true;
252    }
253    key_config_id == expected
254}
255
256/// Configuration for the API Key plugin, aligned with the TypeScript `ApiKeyOptions`.
257#[derive(Clone)]
258pub struct ApiKeyConfig {
259    /// Name of this configuration, stored on every key it creates.
260    /// Upstream defaults it to `"default"`.
261    pub config_id: String,
262    /// Whether keys from this configuration belong to a user or to an
263    /// organization.
264    pub references: ApiKeyReferences,
265
266    // -- key generation --
267    /// Random characters per key. Zero selects the upstream default of 64.
268    pub key_length: usize,
269    pub prefix: Option<String>,
270    /// Permissions applied when creation does not supply explicit permissions.
271    pub default_permissions: Option<std::collections::HashMap<String, Vec<String>>>,
272    /// Custom credential generator, receiving the resolved length and prefix.
273    pub custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
274    /// Dynamic defaults, evaluated even when creation supplies explicit permissions.
275    pub default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
276
277    // -- header --
278    pub api_key_headers: Vec<String>,
279    /// Overrides header extraction for session emulation.
280    pub custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
281    /// Validates the presented plaintext credential before normal key validation.
282    pub custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
283
284    // -- hashing --
285    pub disable_key_hashing: bool,
286
287    // -- starting characters --
288    pub starting_characters_length: usize,
289    pub store_starting_characters: bool,
290
291    // -- prefix length validation --
292    pub max_prefix_length: usize,
293    pub min_prefix_length: usize,
294
295    // -- name validation --
296    pub max_name_length: usize,
297    pub min_name_length: usize,
298    pub require_name: bool,
299
300    // -- metadata --
301    pub enable_metadata: bool,
302
303    // -- key expiration --
304    pub key_expiration: KeyExpirationConfig,
305
306    // -- rate limit defaults --
307    pub rate_limit: RateLimitDefaults,
308
309    // -- session emulation --
310    pub enable_session_for_api_keys: bool,
311    /// Persistence mode. Custom storage overrides the global secondary storage.
312    pub storage: ApiKeyStorage,
313    pub custom_storage: Option<Arc<dyn better_auth_core::store::SecondaryStorage>>,
314    /// Keep database rows authoritative and repopulate cache misses.
315    pub fallback_to_database: bool,
316    /// Defer secondary-only usage writes and expired/exhausted key deletion.
317    pub defer_updates: bool,
318}
319
320impl ApiKeyConfig {
321    fn normalized(mut self) -> Self {
322        if self.key_length == 0 {
323            self.key_length = 64;
324        }
325        self
326    }
327}
328
329impl std::fmt::Debug for ApiKeyConfig {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        f.debug_struct("ApiKeyConfig")
332            .field("config_id", &self.config_id)
333            .field("references", &self.references)
334            .field("key_length", &self.key_length)
335            .field("prefix", &self.prefix)
336            .field("default_permissions", &self.default_permissions)
337            .field("custom_key_generator", &self.custom_key_generator.is_some())
338            .field(
339                "default_permissions_callback",
340                &self.default_permissions_callback.is_some(),
341            )
342            .field(
343                "custom_api_key_getter",
344                &self.custom_api_key_getter.is_some(),
345            )
346            .field(
347                "custom_api_key_validator",
348                &self.custom_api_key_validator.is_some(),
349            )
350            .field("api_key_headers", &self.api_key_headers)
351            .field("disable_key_hashing", &self.disable_key_hashing)
352            .field(
353                "starting_characters_length",
354                &self.starting_characters_length,
355            )
356            .field("store_starting_characters", &self.store_starting_characters)
357            .field("max_prefix_length", &self.max_prefix_length)
358            .field("min_prefix_length", &self.min_prefix_length)
359            .field("max_name_length", &self.max_name_length)
360            .field("min_name_length", &self.min_name_length)
361            .field("require_name", &self.require_name)
362            .field("enable_metadata", &self.enable_metadata)
363            .field("key_expiration", &self.key_expiration)
364            .field("rate_limit", &self.rate_limit)
365            .field(
366                "enable_session_for_api_keys",
367                &self.enable_session_for_api_keys,
368            )
369            .field("storage", &self.storage)
370            .field("custom_storage", &self.custom_storage.is_some())
371            .field("fallback_to_database", &self.fallback_to_database)
372            .field("defer_updates", &self.defer_updates)
373            .finish_non_exhaustive()
374    }
375}
376
377/// Key expiration constraints.
378#[derive(Debug, Clone)]
379pub struct KeyExpirationConfig {
380    /// Default `expiresIn` (in seconds) when none is provided. `None` = no default.
381    pub default_expires_in: Option<f64>,
382    /// If true, clients cannot set a custom `expiresIn`.
383    pub disable_custom_expires_time: bool,
384    /// Maximum `expiresIn` in **days**.
385    pub max_expires_in: f64,
386    /// Minimum `expiresIn` in **days**.
387    pub min_expires_in: f64,
388}
389
390impl Default for KeyExpirationConfig {
391    fn default() -> Self {
392        Self {
393            default_expires_in: None,
394            disable_custom_expires_time: false,
395            max_expires_in: 365.0,
396            min_expires_in: 1.0,
397        }
398    }
399}
400
401/// Global rate-limit defaults applied to newly-created keys.
402#[derive(Debug, Clone)]
403pub struct RateLimitDefaults {
404    pub enabled: bool,
405    /// Default time window in milliseconds.
406    pub time_window: f64,
407    /// Default max requests per window.
408    pub max_requests: f64,
409}
410
411impl Default for RateLimitDefaults {
412    fn default() -> Self {
413        Self {
414            enabled: true,
415            time_window: 86_400_000.0, // 24 hours
416            max_requests: 10.0,
417        }
418    }
419}
420
421impl Default for ApiKeyConfig {
422    fn default() -> Self {
423        Self {
424            config_id: "default".to_string(),
425            references: ApiKeyReferences::default(),
426            key_length: 64,
427            prefix: None,
428            default_permissions: None,
429            custom_key_generator: None,
430            default_permissions_callback: None,
431            api_key_headers: vec!["x-api-key".to_string()],
432            custom_api_key_getter: None,
433            custom_api_key_validator: None,
434            disable_key_hashing: false,
435            starting_characters_length: 6,
436            store_starting_characters: true,
437            max_prefix_length: 32,
438            min_prefix_length: 1,
439            max_name_length: 32,
440            min_name_length: 1,
441            require_name: false,
442            enable_metadata: false,
443            key_expiration: KeyExpirationConfig::default(),
444            rate_limit: RateLimitDefaults::default(),
445            enable_session_for_api_keys: false,
446            storage: ApiKeyStorage::Database,
447            custom_storage: None,
448            fallback_to_database: false,
449            defer_updates: false,
450        }
451    }
452}
453
454// ---------------------------------------------------------------------------
455// Plugin implementation
456// ---------------------------------------------------------------------------
457
458/// Builder for [`ApiKeyPlugin`] powered by the `bon` crate.
459///
460/// Usage:
461/// ```ignore
462/// let plugin = ApiKeyPlugin::builder()
463///     .key_length(48)
464///     .prefix("ba_".to_string())
465///     .enable_metadata(true)
466///     .rate_limit(RateLimitDefaults { enabled: true, time_window: 60_000.0, max_requests: 5.0 })
467///     .build();
468/// ```
469#[bon::bon]
470impl ApiKeyPlugin {
471    #[builder]
472    pub fn new(
473        #[builder(default = "default".to_string())] config_id: String,
474        #[builder(default)] references: ApiKeyReferences,
475        #[builder(default = 64)] key_length: usize,
476        prefix: Option<String>,
477        default_permissions: Option<std::collections::HashMap<String, Vec<String>>>,
478        custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
479        default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
480        #[builder(default = vec!["x-api-key".to_string()])] api_key_headers: Vec<String>,
481        custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
482        custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
483        #[builder(default = false)] disable_key_hashing: bool,
484        #[builder(default = 6)] starting_characters_length: usize,
485        #[builder(default = true)] store_starting_characters: bool,
486        #[builder(default = 32)] max_prefix_length: usize,
487        #[builder(default = 1)] min_prefix_length: usize,
488        #[builder(default = 32)] max_name_length: usize,
489        #[builder(default = 1)] min_name_length: usize,
490        #[builder(default = false)] require_name: bool,
491        #[builder(default = false)] enable_metadata: bool,
492        #[builder(default)] key_expiration: KeyExpirationConfig,
493        #[builder(default)] rate_limit: RateLimitDefaults,
494        #[builder(default = false)] enable_session_for_api_keys: bool,
495        #[builder(default)] storage: ApiKeyStorage,
496        custom_storage: Option<Arc<dyn better_auth_core::store::SecondaryStorage>>,
497        #[builder(default)] fallback_to_database: bool,
498        #[builder(default)] defer_updates: bool,
499    ) -> Self {
500        Self {
501            configurations: vec![
502                ApiKeyConfig {
503                    config_id,
504                    references,
505                    key_length,
506                    prefix,
507                    default_permissions,
508                    custom_key_generator,
509                    default_permissions_callback,
510                    api_key_headers,
511                    custom_api_key_getter,
512                    custom_api_key_validator,
513                    disable_key_hashing,
514                    starting_characters_length,
515                    store_starting_characters,
516                    max_prefix_length,
517                    min_prefix_length,
518                    max_name_length,
519                    min_name_length,
520                    require_name,
521                    enable_metadata,
522                    key_expiration,
523                    rate_limit,
524                    enable_session_for_api_keys,
525                    storage,
526                    custom_storage,
527                    fallback_to_database,
528                    defer_updates,
529                }
530                .normalized(),
531            ],
532            last_expired_check: Arc::new(Mutex::new(None)),
533        }
534    }
535
536    pub fn with_config(config: ApiKeyConfig) -> Self {
537        Self {
538            configurations: vec![config.normalized()],
539            last_expired_check: Arc::new(Mutex::new(None)),
540        }
541    }
542
543    // -- internal helpers --
544
545    pub(super) fn generate_key(
546        config: &ApiKeyConfig,
547        custom_prefix: Option<&str>,
548    ) -> (String, String) {
549        // Match TS: generateRandomString(length, "a-z", "A-Z") — alpha only
550        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
551        let mut rng = rand::thread_rng();
552        let raw: String = (0..config.key_length)
553            .map(|_| {
554                ALPHABET
555                    .choose(&mut rng)
556                    .copied()
557                    .map(char::from)
558                    .unwrap_or('a')
559            })
560            .collect();
561
562        let prefix = custom_prefix.or(config.prefix.as_deref()).unwrap_or("");
563        let full_key = format!("{}{}", prefix, raw);
564
565        let hash = if config.disable_key_hashing {
566            full_key.clone()
567        } else {
568            Self::hash_key(&full_key)
569        };
570
571        (full_key, hash)
572    }
573
574    pub(super) fn hash_key(key: &str) -> String {
575        let mut hasher = Sha256::new();
576        hasher.update(key.as_bytes());
577        let digest = hasher.finalize();
578        URL_SAFE_NO_PAD.encode(digest)
579    }
580
581    /// Throttled cleanup -- at most once per 10 seconds.
582    pub(super) async fn maybe_delete_expired(
583        &self,
584        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
585    ) {
586        let should_run = {
587            let mut last = self
588                .last_expired_check
589                .lock()
590                .unwrap_or_else(|e| e.into_inner());
591            let now = std::time::Instant::now();
592            match *last {
593                Some(prev) if now.duration_since(prev).as_secs() < 10 => false,
594                _ => {
595                    *last = Some(now);
596                    true
597                }
598            }
599        };
600        if should_run && let Err(error) = ctx.database.delete_expired_api_keys().await {
601            better_auth_core::observability::logger::current().error(
602                "Failed to delete expired API keys",
603                &[better_auth_core::observability::LogArgument::Error(&error)],
604            );
605        }
606    }
607
608    // -- Validation helpers --
609
610    pub(super) fn validate_prefix(config: &ApiKeyConfig, prefix: Option<&str>) -> AuthResult<()> {
611        if let Some(p) = prefix.filter(|prefix| !prefix.is_empty()) {
612            let len = p.encode_utf16().count();
613            if len < config.min_prefix_length || len > config.max_prefix_length {
614                return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
615            }
616        }
617        Ok(())
618    }
619
620    /// Validate the `name` field.
621    ///
622    /// When `is_create` is true, `require_name` is enforced (name must be
623    /// present).  On updates `require_name` is **not** enforced -- the
624    /// caller may be updating unrelated fields without resending the name.
625    pub(super) fn validate_name(
626        config: &ApiKeyConfig,
627        name: Option<&str>,
628        is_create: bool,
629    ) -> AuthResult<()> {
630        if is_create && config.require_name && name.is_none_or(str::is_empty) {
631            return Err(api_key_error(ApiKeyErrorCode::NameRequired));
632        }
633        if let Some(n) = name.filter(|name| !is_create || !name.is_empty()) {
634            let len = n.encode_utf16().count();
635            if len < config.min_name_length || len > config.max_name_length {
636                return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
637            }
638        }
639        Ok(())
640    }
641
642    pub(super) fn validate_expires_in(
643        config: &ApiKeyConfig,
644        expires_in: Option<f64>,
645    ) -> AuthResult<Option<f64>> {
646        let cfg = &config.key_expiration;
647        if let Some(secs) = expires_in {
648            if cfg.disable_custom_expires_time {
649                return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
650            }
651            // expiresIn is in seconds; min/max are in days
652            let days = secs / 86_400.0;
653            if days < cfg.min_expires_in {
654                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
655            }
656            if days > cfg.max_expires_in {
657                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
658            }
659            Ok(Some(secs))
660        } else {
661            Ok(cfg.default_expires_in)
662        }
663    }
664
665    pub(super) fn validate_metadata(
666        config: &ApiKeyConfig,
667        metadata: &Option<serde_json::Value>,
668    ) -> AuthResult<()> {
669        if let Some(value) = metadata.as_ref().filter(|value| match value {
670            serde_json::Value::Null => false,
671            serde_json::Value::Bool(value) => *value,
672            serde_json::Value::Number(value) => value.as_f64() != Some(0.0),
673            serde_json::Value::String(value) => !value.is_empty(),
674            _ => true,
675        }) {
676            if !config.enable_metadata {
677                return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
678            }
679            if !value.is_object() && !value.is_array() {
680                return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
681            }
682        }
683        Ok(())
684    }
685
686    pub(super) fn validate_refill(
687        refill_interval: Option<f64>,
688        refill_amount: Option<f64>,
689    ) -> AuthResult<()> {
690        match (
691            refill_interval.filter(|value| *value != 0.0),
692            refill_amount.filter(|value| *value != 0.0),
693        ) {
694            (None, Some(_)) => Err(api_key_error(
695                ApiKeyErrorCode::RefillAmountAndIntervalRequired,
696            )),
697            (Some(_), None) => Err(api_key_error(
698                ApiKeyErrorCode::RefillIntervalAndAmountRequired,
699            )),
700            _ => Ok(()),
701        }
702    }
703
704    // -----------------------------------------------------------------------
705    // Route handlers
706    // -----------------------------------------------------------------------
707
708    async fn handle_create(
709        &self,
710        req: &AuthRequest,
711        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
712    ) -> AuthResult<AuthResponse> {
713        let body: CreateKeyRequest = request::read(req)?;
714        let config = self.resolve_configuration(body.config_id.as_deref())?;
715        let session = ctx
716            .session_manager()
717            .resolve(req, better_auth_core::session::SessionRead::Authoritative)
718            .await?
719            .data;
720        let original = req.original_request().or_else(|| {
721            better_auth_core::hooks::current_request_hook_context()
722                .is_some_and(|context| context.is_http)
723                .then_some(req)
724        });
725        let client = req.endpoint_headers().is_some() || original.is_some();
726        if client {
727            validate_client_create(&body)?;
728        }
729        if original.is_some() && body.user_id.is_some() {
730            return Err(api_key_error(ApiKeyErrorCode::UnauthorizedSession));
731        }
732        let actor = session
733            .as_ref()
734            .and_then(|data| data.user.id.as_str())
735            .or_else(|| {
736                (!client || config.references == ApiKeyReferences::Organization)
737                    .then_some(body.user_id.as_deref())
738                    .flatten()
739            })
740            .filter(|id| !id.is_empty());
741        if config.references == ApiKeyReferences::User
742            && !client
743            && session.is_some()
744            && body
745                .user_id
746                .as_deref()
747                .filter(|id| !id.is_empty())
748                .is_some_and(|id| Some(id) != actor)
749        {
750            return Err(api_key_error(ApiKeyErrorCode::UnauthorizedSession));
751        }
752        // Organization selection precedes the actor check in the upstream handler.
753        if config.references == ApiKeyReferences::Organization
754            && body.organization_id.as_deref().is_none_or(str::is_empty)
755        {
756            return Err(api_key_error(ApiKeyErrorCode::OrganizationIdRequired));
757        }
758        let actor = actor.ok_or_else(|| api_key_error(ApiKeyErrorCode::UnauthorizedSession))?;
759        let response = create_key_for_user(&body, actor, self, ctx, original).await?;
760        Ok(AuthResponse::json(200, &response)?)
761    }
762
763    async fn handle_get(
764        &self,
765        req: &AuthRequest,
766        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
767    ) -> AuthResult<AuthResponse> {
768        let (user, _session) = ctx.require_session(req).await?;
769        let id = req
770            .query_string("id")?
771            .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
772        let config_id = req.query_string("configId")?;
773        let response = get_key_core(id, config_id, user.id().typed()?, self, ctx).await?;
774        Ok(AuthResponse::json(200, &response)?)
775    }
776
777    async fn handle_list(
778        &self,
779        req: &AuthRequest,
780        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
781    ) -> AuthResult<AuthResponse> {
782        let (user, _session) = ctx.require_session(req).await?;
783        let query = ListKeysQuery::from_request(req)?;
784        let response = list_keys_core(user.id().typed()?, &query, self, ctx).await?;
785        Ok(AuthResponse::json(200, &response)?)
786    }
787
788    async fn handle_update(
789        &self,
790        req: &AuthRequest,
791        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
792    ) -> AuthResult<AuthResponse> {
793        let body: UpdateKeyRequest = request::read(req)?;
794        let session = ctx
795            .session_manager()
796            .resolve(req, better_auth_core::session::SessionRead::Authoritative)
797            .await?
798            .data;
799        let client = req.endpoint_headers().is_some()
800            || req.original_request().is_some()
801            || better_auth_core::hooks::current_request_hook_context()
802                .is_some_and(|context| context.is_http);
803        let actor = session
804            .as_ref()
805            .and_then(|data| data.user.id.as_str())
806            .or_else(|| (!client).then_some(body.user_id.as_deref()).flatten())
807            .filter(|id| !id.is_empty())
808            .ok_or_else(|| api_key_error(ApiKeyErrorCode::UnauthorizedSession))?;
809        if session.is_some()
810            && body
811                .user_id
812                .as_deref()
813                .filter(|id| !id.is_empty())
814                .is_some_and(|id| id != actor)
815        {
816            return Err(api_key_error(ApiKeyErrorCode::UnauthorizedSession));
817        }
818        let response = if client {
819            update_key_core(&body, actor, self, ctx).await?
820        } else {
821            update_key_for_user(&body, actor, self, ctx).await?
822        };
823        Ok(AuthResponse::json(200, &response)?)
824    }
825
826    async fn handle_delete(
827        &self,
828        req: &AuthRequest,
829        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
830    ) -> AuthResult<AuthResponse> {
831        let body: DeleteKeyRequest = request::read(req)?;
832        let (user, _session) = ctx.require_session(req).await?;
833        if user.banned() {
834            return Err(AuthError::authentication_failed("User is banned"));
835        }
836        let response = delete_key_core(&body, user.id().typed()?, self, ctx).await?;
837        Ok(AuthResponse::json(200, &response)?)
838    }
839}
840
841// ---------------------------------------------------------------------------
842// AuthPlugin trait implementation
843// ---------------------------------------------------------------------------
844
845better_auth_core::impl_auth_plugin! {
846    ApiKeyPlugin, "api-key";
847    routes {
848        post "/api-key/create"                    => handle_create,             "createApiKey", body = request::validate;
849        get  "/api-key/get"                       => handle_get,                "getApiKey", query = crate::plugins::query_input::api_key_get;
850        post "/api-key/update"                    => handle_update,             "updateApiKey", body = request::validate;
851        post "/api-key/delete"                    => handle_delete,             "deleteApiKey", body = request::validate;
852        get  "/api-key/list"                      => handle_list,               "listApiKeys", query = crate::plugins::query_input::api_key_list;
853    }
854    extra {
855        fn openapi(&self) -> AuthResult<better_auth_core::openapi::OpenApiPluginMetadata> {
856            let mut metadata = better_auth_core::openapi::OpenApiPluginMetadata::from_routes(
857                <Self as better_auth_core::AuthPlugin<S>>::name(self),
858                <Self as better_auth_core::AuthPlugin<S>>::routes(self),
859            )?;
860            if let [config] = self.configurations.as_slice() {
861                metadata = metadata
862                    .model_default("apikey", "rateLimitMax", serde_json::json!(config.rate_limit.max_requests))?
863                    .model_default("apikey", "rateLimitTimeWindow", serde_json::json!(config.rate_limit.time_window))?;
864            }
865            Ok(metadata)
866        }
867
868        async fn on_init(&self, _ctx: &mut better_auth_core::AuthInitContext<S>) -> AuthResult<()> {
869            if self.configurations.len() > 1 {
870                let mut ids = std::collections::HashSet::new();
871                for config in &self.configurations {
872                    if config.config_id.is_empty() {
873                        return Err(AuthError::config("configId is required for each API key configuration in the api-key plugin."));
874                    }
875                    if !ids.insert(&config.config_id) {
876                        return Err(AuthError::config("configId must be unique for each API key configuration in the api-key plugin."));
877                    }
878                }
879            }
880            Ok(())
881        }
882
883        async fn before_request(
884            &self,
885            req: &AuthRequest,
886            ctx: &AuthContext<S>,
887        ) -> AuthResult<Option<BeforeRequestAction>> {
888            self.api_key_session(req, ctx).await
889        }
890    }
891}