Skip to main content

better_auth_api/plugins/
api_key.rs

1use async_trait::async_trait;
2use base64::Engine;
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use governor::clock::DefaultClock;
5use governor::state::{InMemoryState, NotKeyed};
6use governor::{Quota, RateLimiter};
7use rand::RngCore;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::collections::HashMap;
11use std::num::NonZeroU32;
12use std::sync::Mutex;
13use validator::Validate;
14
15use better_auth_core::adapters::DatabaseAdapter;
16use better_auth_core::entity::{AuthApiKey, AuthUser};
17use better_auth_core::{AuthContext, AuthPlugin, AuthRoute, BeforeRequestAction};
18use better_auth_core::{AuthError, AuthResult};
19use better_auth_core::{AuthRequest, AuthResponse, CreateApiKey, HttpMethod, UpdateApiKey};
20
21use super::helpers;
22
23// ---------------------------------------------------------------------------
24// Error codes -- mirrors the TypeScript `API_KEY_ERROR_CODES`
25// ---------------------------------------------------------------------------
26
27/// Dedicated API Key error codes aligned with the TypeScript implementation.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
29pub enum ApiKeyErrorCode {
30    #[serde(rename = "INVALID_API_KEY")]
31    InvalidApiKey,
32    #[serde(rename = "KEY_DISABLED")]
33    KeyDisabled,
34    #[serde(rename = "KEY_EXPIRED")]
35    KeyExpired,
36    #[serde(rename = "USAGE_EXCEEDED")]
37    UsageExceeded,
38    #[serde(rename = "KEY_NOT_FOUND")]
39    KeyNotFound,
40    #[serde(rename = "RATE_LIMITED")]
41    RateLimited,
42    #[serde(rename = "UNAUTHORIZED_SESSION")]
43    UnauthorizedSession,
44    #[serde(rename = "INVALID_PREFIX_LENGTH")]
45    InvalidPrefixLength,
46    #[serde(rename = "INVALID_NAME_LENGTH")]
47    InvalidNameLength,
48    #[serde(rename = "METADATA_DISABLED")]
49    MetadataDisabled,
50    #[serde(rename = "NO_VALUES_TO_UPDATE")]
51    NoValuesToUpdate,
52    #[serde(rename = "KEY_DISABLED_EXPIRATION")]
53    KeyDisabledExpiration,
54    #[serde(rename = "EXPIRES_IN_IS_TOO_SMALL")]
55    ExpiresInTooSmall,
56    #[serde(rename = "EXPIRES_IN_IS_TOO_LARGE")]
57    ExpiresInTooLarge,
58    #[serde(rename = "INVALID_REMAINING")]
59    InvalidRemaining,
60    #[serde(rename = "REFILL_AMOUNT_AND_INTERVAL_REQUIRED")]
61    RefillAmountAndIntervalRequired,
62    #[serde(rename = "NAME_REQUIRED")]
63    NameRequired,
64    #[serde(rename = "INVALID_USER_ID_FROM_API_KEY")]
65    InvalidUserIdFromApiKey,
66    #[serde(rename = "SERVER_ONLY_PROPERTY")]
67    ServerOnlyProperty,
68    #[serde(rename = "FAILED_TO_UPDATE_API_KEY")]
69    FailedToUpdateApiKey,
70    #[serde(rename = "INVALID_METADATA_TYPE")]
71    InvalidMetadataType,
72}
73
74impl ApiKeyErrorCode {
75    /// Return the SCREAMING_SNAKE_CASE string for this error code.
76    /// Used by `handle_verify` to produce the structured JSON error response.
77    pub fn as_str(self) -> &'static str {
78        match self {
79            Self::InvalidApiKey => "INVALID_API_KEY",
80            Self::KeyDisabled => "KEY_DISABLED",
81            Self::KeyExpired => "KEY_EXPIRED",
82            Self::UsageExceeded => "USAGE_EXCEEDED",
83            Self::KeyNotFound => "KEY_NOT_FOUND",
84            Self::RateLimited => "RATE_LIMITED",
85            Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
86            Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
87            Self::InvalidNameLength => "INVALID_NAME_LENGTH",
88            Self::MetadataDisabled => "METADATA_DISABLED",
89            Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
90            Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
91            Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
92            Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
93            Self::InvalidRemaining => "INVALID_REMAINING",
94            Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
95            Self::NameRequired => "NAME_REQUIRED",
96            Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
97            Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
98            Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
99            Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
100        }
101    }
102
103    pub fn message(self) -> &'static str {
104        match self {
105            Self::InvalidApiKey => "Invalid API key.",
106            Self::KeyDisabled => "API Key is disabled",
107            Self::KeyExpired => "API Key has expired",
108            Self::UsageExceeded => "API Key has reached its usage limit",
109            Self::KeyNotFound => "API Key not found",
110            Self::RateLimited => "Rate limit exceeded.",
111            Self::UnauthorizedSession => "Unauthorized or invalid session",
112            Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
113            Self::InvalidNameLength => "The name length is either too large or too small.",
114            Self::MetadataDisabled => "Metadata is disabled.",
115            Self::NoValuesToUpdate => "No values to update.",
116            Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
117            Self::ExpiresInTooSmall => {
118                "The expiresIn is smaller than the predefined minimum value."
119            }
120            Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
121            Self::InvalidRemaining => "The remaining count is either too large or too small.",
122            Self::RefillAmountAndIntervalRequired => {
123                "refillAmount and refillInterval must both be provided together"
124            }
125            Self::NameRequired => "API Key name is required.",
126            Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
127            Self::ServerOnlyProperty => {
128                "The property you're trying to set can only be set from the server auth instance only."
129            }
130            Self::FailedToUpdateApiKey => "Failed to update API key",
131            Self::InvalidMetadataType => "metadata must be an object or undefined",
132        }
133    }
134}
135
136fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
137    AuthError::bad_request(code.message())
138}
139
140/// Structured error returned by `validate_api_key` so that `handle_verify`
141/// can extract the error code without fragile string matching.
142struct ApiKeyValidationError {
143    code: ApiKeyErrorCode,
144    message: String,
145}
146
147impl ApiKeyValidationError {
148    fn new(code: ApiKeyErrorCode) -> Self {
149        Self {
150            message: code.message().to_string(),
151            code,
152        }
153    }
154}
155
156// ---------------------------------------------------------------------------
157// Configuration
158// ---------------------------------------------------------------------------
159
160/// API Key management plugin.
161pub struct ApiKeyPlugin {
162    config: ApiKeyConfig,
163    /// Throttle for `delete_expired_api_keys` -- stores the last check instant.
164    last_expired_check: Mutex<Option<std::time::Instant>>,
165    /// Per-key in-memory rate limiters backed by the `governor` crate.
166    /// Key: API key ID → governor rate limiter.
167    rate_limiters: Mutex<HashMap<String, std::sync::Arc<GovernorLimiter>>>,
168}
169
170/// Type alias for the governor rate limiter we use (not keyed, in-memory, default clock).
171type GovernorLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>;
172
173/// Configuration for the API Key plugin, aligned with the TypeScript `ApiKeyOptions`.
174#[derive(Debug, Clone)]
175pub struct ApiKeyConfig {
176    // -- key generation --
177    pub key_length: usize,
178    pub prefix: Option<String>,
179    pub default_remaining: Option<i64>,
180
181    // -- header --
182    pub api_key_header: String,
183
184    // -- hashing --
185    pub disable_key_hashing: bool,
186
187    // -- starting characters --
188    pub starting_characters_length: usize,
189    pub store_starting_characters: bool,
190
191    // -- prefix length validation --
192    pub max_prefix_length: usize,
193    pub min_prefix_length: usize,
194
195    // -- name validation --
196    pub max_name_length: usize,
197    pub min_name_length: usize,
198    pub require_name: bool,
199
200    // -- metadata --
201    pub enable_metadata: bool,
202
203    // -- key expiration --
204    pub key_expiration: KeyExpirationConfig,
205
206    // -- rate limit defaults --
207    pub rate_limit: RateLimitDefaults,
208
209    // -- session emulation --
210    pub enable_session_for_api_keys: bool,
211}
212
213/// Key expiration constraints.
214#[derive(Debug, Clone)]
215pub struct KeyExpirationConfig {
216    /// Default `expiresIn` (in milliseconds) when none is provided. `None` = no default.
217    pub default_expires_in: Option<i64>,
218    /// If true, clients cannot set a custom `expiresIn`.
219    pub disable_custom_expires_time: bool,
220    /// Maximum `expiresIn` in **days**.
221    pub max_expires_in: i64,
222    /// Minimum `expiresIn` in **days**.
223    pub min_expires_in: i64,
224}
225
226impl Default for KeyExpirationConfig {
227    fn default() -> Self {
228        Self {
229            default_expires_in: None,
230            disable_custom_expires_time: false,
231            max_expires_in: 365,
232            min_expires_in: 0,
233        }
234    }
235}
236
237/// Global rate-limit defaults applied to newly-created keys.
238#[derive(Debug, Clone)]
239pub struct RateLimitDefaults {
240    pub enabled: bool,
241    /// Default time window in milliseconds.
242    pub time_window: i64,
243    /// Default max requests per window.
244    pub max_requests: i64,
245}
246
247impl Default for RateLimitDefaults {
248    fn default() -> Self {
249        Self {
250            enabled: true,
251            time_window: 86_400_000, // 24 hours
252            max_requests: 10,
253        }
254    }
255}
256
257impl Default for ApiKeyConfig {
258    fn default() -> Self {
259        Self {
260            key_length: 32,
261            prefix: None,
262            default_remaining: None,
263            api_key_header: "x-api-key".to_string(),
264            disable_key_hashing: false,
265            starting_characters_length: 6,
266            store_starting_characters: true,
267            max_prefix_length: 32,
268            min_prefix_length: 1,
269            max_name_length: 32,
270            min_name_length: 1,
271            require_name: false,
272            enable_metadata: false,
273            key_expiration: KeyExpirationConfig::default(),
274            rate_limit: RateLimitDefaults::default(),
275            enable_session_for_api_keys: false,
276        }
277    }
278}
279
280// ---------------------------------------------------------------------------
281// Request types
282// ---------------------------------------------------------------------------
283
284#[derive(Debug, Deserialize, Validate)]
285struct CreateKeyRequest {
286    name: Option<String>,
287    prefix: Option<String>,
288    #[serde(rename = "expiresIn")]
289    expires_in: Option<i64>,
290    remaining: Option<i64>,
291    #[serde(rename = "rateLimitEnabled")]
292    rate_limit_enabled: Option<bool>,
293    #[serde(rename = "rateLimitTimeWindow")]
294    rate_limit_time_window: Option<i64>,
295    #[serde(rename = "rateLimitMax")]
296    rate_limit_max: Option<i64>,
297    #[serde(rename = "refillInterval")]
298    refill_interval: Option<i64>,
299    #[serde(rename = "refillAmount")]
300    refill_amount: Option<i64>,
301    permissions: Option<serde_json::Value>,
302    metadata: Option<serde_json::Value>,
303}
304
305#[derive(Debug, Deserialize, Validate)]
306struct UpdateKeyRequest {
307    #[validate(length(min = 1, message = "Key ID is required"))]
308    id: String,
309    name: Option<String>,
310    enabled: Option<bool>,
311    remaining: Option<i64>,
312    #[serde(rename = "rateLimitEnabled")]
313    rate_limit_enabled: Option<bool>,
314    #[serde(rename = "rateLimitTimeWindow")]
315    rate_limit_time_window: Option<i64>,
316    #[serde(rename = "rateLimitMax")]
317    rate_limit_max: Option<i64>,
318    #[serde(rename = "refillInterval")]
319    refill_interval: Option<i64>,
320    #[serde(rename = "refillAmount")]
321    refill_amount: Option<i64>,
322    permissions: Option<serde_json::Value>,
323    metadata: Option<serde_json::Value>,
324    #[serde(rename = "expiresIn")]
325    expires_in: Option<i64>,
326}
327
328#[derive(Debug, Deserialize, Validate)]
329struct DeleteKeyRequest {
330    #[validate(length(min = 1, message = "Key ID is required"))]
331    id: String,
332}
333
334#[derive(Debug, Deserialize)]
335struct VerifyKeyRequest {
336    key: String,
337    permissions: Option<serde_json::Value>,
338}
339
340// ---------------------------------------------------------------------------
341// Response types
342// ---------------------------------------------------------------------------
343
344#[derive(Debug, Serialize)]
345struct ApiKeyView {
346    id: String,
347    name: Option<String>,
348    start: Option<String>,
349    prefix: Option<String>,
350    #[serde(rename = "userId")]
351    user_id: String,
352    #[serde(rename = "refillInterval")]
353    refill_interval: Option<i64>,
354    #[serde(rename = "refillAmount")]
355    refill_amount: Option<i64>,
356    #[serde(rename = "lastRefillAt")]
357    last_refill_at: Option<String>,
358    enabled: bool,
359    #[serde(rename = "rateLimitEnabled")]
360    rate_limit_enabled: bool,
361    #[serde(rename = "rateLimitTimeWindow")]
362    rate_limit_time_window: Option<i64>,
363    #[serde(rename = "rateLimitMax")]
364    rate_limit_max: Option<i64>,
365    #[serde(rename = "requestCount")]
366    request_count: Option<i64>,
367    remaining: Option<i64>,
368    #[serde(rename = "lastRequest")]
369    last_request: Option<String>,
370    #[serde(rename = "expiresAt")]
371    expires_at: Option<String>,
372    #[serde(rename = "createdAt")]
373    created_at: String,
374    #[serde(rename = "updatedAt")]
375    updated_at: String,
376    permissions: Option<serde_json::Value>,
377    metadata: Option<serde_json::Value>,
378}
379
380#[derive(Debug, Serialize)]
381struct CreateKeyResponse {
382    key: String,
383    #[serde(flatten)]
384    api_key: ApiKeyView,
385}
386
387#[derive(Debug, Serialize)]
388struct VerifyKeyResponse {
389    valid: bool,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    error: Option<VerifyErrorBody>,
392    key: Option<ApiKeyView>,
393}
394
395#[derive(Debug, Serialize)]
396struct VerifyErrorBody {
397    message: String,
398    code: String,
399}
400
401impl ApiKeyView {
402    fn from_entity(ak: &impl AuthApiKey) -> Self {
403        Self {
404            id: ak.id().to_string(),
405            name: ak.name().map(|s| s.to_string()),
406            start: ak.start().map(|s| s.to_string()),
407            prefix: ak.prefix().map(|s| s.to_string()),
408            user_id: ak.user_id().to_string(),
409            refill_interval: ak.refill_interval(),
410            refill_amount: ak.refill_amount(),
411            last_refill_at: ak.last_refill_at().map(|s| s.to_string()),
412            enabled: ak.enabled(),
413            rate_limit_enabled: ak.rate_limit_enabled(),
414            rate_limit_time_window: ak.rate_limit_time_window(),
415            rate_limit_max: ak.rate_limit_max(),
416            request_count: ak.request_count(),
417            remaining: ak.remaining(),
418            last_request: ak.last_request().map(|s| s.to_string()),
419            expires_at: ak.expires_at().map(|s| s.to_string()),
420            created_at: ak.created_at().to_string(),
421            updated_at: ak.updated_at().to_string(),
422            permissions: ak.permissions().and_then(|s| serde_json::from_str(s).ok()),
423            metadata: ak.metadata().and_then(|s| serde_json::from_str(s).ok()),
424        }
425    }
426}
427
428// ---------------------------------------------------------------------------
429// Rate limiting
430// ---------------------------------------------------------------------------
431// The legacy hand-written sliding-window rate limiter (and its associated
432// structs) has been removed.
433//
434// Rate limiting is now implemented by the `governor` crate via
435// `ApiKeyPlugin::check_rate_limit_governor()`.
436
437// ---------------------------------------------------------------------------
438// Permissions verification helper (RBAC)
439// ---------------------------------------------------------------------------
440// Custom implementation matching the TypeScript `role().authorize()` pattern
441// from `packages/better-auth/src/plugins/access/access.ts`.
442//
443// The TS logic: for each requested resource (role), check that every requested
444// action exists in the key's allowed actions for that resource. This is a
445// simple subset check with no external dependencies.
446// ---------------------------------------------------------------------------
447
448/// Check whether `key_permissions` (JSON object mapping role->actions) covers
449/// all of the `required_permissions`.
450///
451/// Mirrors the TypeScript `role(apiKeyPermissions).authorize(permissions)`
452/// implementation. Required actions must be a subset of the API key's actions
453/// for each resource/role.
454fn check_permissions(key_permissions_json: &str, required: &serde_json::Value) -> bool {
455    let required_map = match required.as_object() {
456        Some(m) => m,
457        None => return false,
458    };
459
460    let key_map: HashMap<String, Vec<String>> = match serde_json::from_str(key_permissions_json) {
461        Ok(v) => v,
462        Err(_) => return false,
463    };
464
465    for (resource, requested_actions) in required_map {
466        // Look up the allowed actions for this resource
467        let allowed_actions = match key_map.get(resource) {
468            Some(a) => a,
469            // Resource not found in key permissions → fail (matches TS behavior)
470            None => return false,
471        };
472
473        // The request value can be:
474        // 1. An array of action strings → all must be allowed (AND)
475        // 2. An object { actions: [...], connector: "OR"|"AND" }
476        if let Some(actions_array) = requested_actions.as_array() {
477            // Simple array → every requested action must exist in allowed actions
478            for action_val in actions_array {
479                let action = match action_val.as_str() {
480                    Some(s) => s,
481                    None => return false,
482                };
483                if !allowed_actions.iter().any(|a| a == action) {
484                    return false;
485                }
486            }
487        } else if let Some(obj) = requested_actions.as_object() {
488            // Object form: { actions: [...], connector: "OR" | "AND" }
489            let actions = match obj.get("actions").and_then(|v| v.as_array()) {
490                Some(a) => a,
491                None => return false,
492            };
493            let connector = obj
494                .get("connector")
495                .and_then(|v| v.as_str())
496                .unwrap_or("AND");
497
498            if connector == "OR" {
499                // At least one requested action must be allowed
500                let any_allowed = actions.iter().any(|action_val| {
501                    action_val
502                        .as_str()
503                        .is_some_and(|action| allowed_actions.iter().any(|a| a == action))
504                });
505                if !any_allowed {
506                    return false;
507                }
508            } else {
509                // AND (default): every requested action must be allowed
510                for action_val in actions {
511                    let action = match action_val.as_str() {
512                        Some(s) => s,
513                        None => return false,
514                    };
515                    if !allowed_actions.iter().any(|a| a == action) {
516                        return false;
517                    }
518                }
519            }
520        } else {
521            // Invalid format
522            return false;
523        }
524    }
525
526    true
527}
528
529// ---------------------------------------------------------------------------
530// Plugin implementation
531// ---------------------------------------------------------------------------
532
533/// Builder for [`ApiKeyPlugin`] powered by the `bon` crate.
534///
535/// Usage:
536/// ```ignore
537/// let plugin = ApiKeyPlugin::builder()
538///     .key_length(48)
539///     .prefix("ba_".to_string())
540///     .enable_metadata(true)
541///     .rate_limit(RateLimitDefaults { enabled: true, time_window: 60_000, max_requests: 5 })
542///     .build();
543/// ```
544#[bon::bon]
545impl ApiKeyPlugin {
546    #[builder]
547    pub fn new(
548        #[builder(default = 32)] key_length: usize,
549        prefix: Option<String>,
550        default_remaining: Option<i64>,
551        #[builder(default = "x-api-key".to_string())] api_key_header: String,
552        #[builder(default = false)] disable_key_hashing: bool,
553        #[builder(default = 6)] starting_characters_length: usize,
554        #[builder(default = true)] store_starting_characters: bool,
555        #[builder(default = 32)] max_prefix_length: usize,
556        #[builder(default = 1)] min_prefix_length: usize,
557        #[builder(default = 32)] max_name_length: usize,
558        #[builder(default = 1)] min_name_length: usize,
559        #[builder(default = false)] require_name: bool,
560        #[builder(default = false)] enable_metadata: bool,
561        #[builder(default)] key_expiration: KeyExpirationConfig,
562        #[builder(default)] rate_limit: RateLimitDefaults,
563        #[builder(default = false)] enable_session_for_api_keys: bool,
564    ) -> Self {
565        Self {
566            config: ApiKeyConfig {
567                key_length,
568                prefix,
569                default_remaining,
570                api_key_header,
571                disable_key_hashing,
572                starting_characters_length,
573                store_starting_characters,
574                max_prefix_length,
575                min_prefix_length,
576                max_name_length,
577                min_name_length,
578                require_name,
579                enable_metadata,
580                key_expiration,
581                rate_limit,
582                enable_session_for_api_keys,
583            },
584            last_expired_check: Mutex::new(None),
585            rate_limiters: Mutex::new(HashMap::new()),
586        }
587    }
588
589    pub fn with_config(config: ApiKeyConfig) -> Self {
590        Self {
591            config,
592            last_expired_check: Mutex::new(None),
593            rate_limiters: Mutex::new(HashMap::new()),
594        }
595    }
596
597    // -- internal helpers --
598
599    fn generate_key(&self, custom_prefix: Option<&str>) -> (String, String, String) {
600        let mut bytes = vec![0u8; self.config.key_length];
601        rand::rngs::OsRng.fill_bytes(&mut bytes);
602        let raw = URL_SAFE_NO_PAD.encode(&bytes);
603
604        let start_len = self.config.starting_characters_length;
605        let start = raw.chars().take(start_len).collect::<String>();
606
607        let prefix = custom_prefix
608            .or(self.config.prefix.as_deref())
609            .unwrap_or("");
610        let full_key = format!("{}{}", prefix, raw);
611
612        let hash = if self.config.disable_key_hashing {
613            full_key.clone()
614        } else {
615            Self::hash_key(&full_key)
616        };
617
618        (full_key, hash, start)
619    }
620
621    fn hash_key(key: &str) -> String {
622        let mut hasher = Sha256::new();
623        hasher.update(key.as_bytes());
624        let digest = hasher.finalize();
625        URL_SAFE_NO_PAD.encode(digest)
626    }
627
628    /// Throttled cleanup -- at most once per 10 seconds.
629    async fn maybe_delete_expired<DB: DatabaseAdapter>(&self, ctx: &AuthContext<DB>) {
630        let should_run = {
631            let mut last = self.last_expired_check.lock().unwrap();
632            let now = std::time::Instant::now();
633            match *last {
634                Some(prev) if now.duration_since(prev).as_secs() < 10 => false,
635                _ => {
636                    *last = Some(now);
637                    true
638                }
639            }
640        };
641        if should_run {
642            let _ = ctx.database.delete_expired_api_keys().await;
643        }
644    }
645
646    // -- Validation helpers --
647
648    fn validate_prefix(&self, prefix: Option<&str>) -> AuthResult<()> {
649        if let Some(p) = prefix {
650            let len = p.len();
651            if len < self.config.min_prefix_length || len > self.config.max_prefix_length {
652                return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
653            }
654        }
655        Ok(())
656    }
657
658    /// Validate the `name` field.
659    ///
660    /// When `is_create` is true, `require_name` is enforced (name must be
661    /// present).  On updates `require_name` is **not** enforced — the
662    /// caller may be updating unrelated fields without resending the name.
663    fn validate_name(&self, name: Option<&str>, is_create: bool) -> AuthResult<()> {
664        if is_create && self.config.require_name && name.is_none() {
665            return Err(api_key_error(ApiKeyErrorCode::NameRequired));
666        }
667        if let Some(n) = name {
668            let len = n.len();
669            if len < self.config.min_name_length || len > self.config.max_name_length {
670                return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
671            }
672        }
673        Ok(())
674    }
675
676    fn validate_expires_in(&self, expires_in: Option<i64>) -> AuthResult<Option<i64>> {
677        let cfg = &self.config.key_expiration;
678        if let Some(ms) = expires_in {
679            if cfg.disable_custom_expires_time {
680                return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
681            }
682            let days = ms as f64 / 86_400_000.0;
683            if days < cfg.min_expires_in as f64 {
684                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
685            }
686            if days > cfg.max_expires_in as f64 {
687                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
688            }
689            Ok(Some(ms))
690        } else {
691            Ok(cfg.default_expires_in)
692        }
693    }
694
695    fn validate_metadata(&self, metadata: &Option<serde_json::Value>) -> AuthResult<()> {
696        if metadata.is_some() && !self.config.enable_metadata {
697            return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
698        }
699        if let Some(v) = metadata
700            && !v.is_object()
701            && !v.is_null()
702        {
703            return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
704        }
705        Ok(())
706    }
707
708    fn validate_refill(refill_interval: Option<i64>, refill_amount: Option<i64>) -> AuthResult<()> {
709        match (refill_interval, refill_amount) {
710            (Some(_), None) | (None, Some(_)) => Err(api_key_error(
711                ApiKeyErrorCode::RefillAmountAndIntervalRequired,
712            )),
713            _ => Ok(()),
714        }
715    }
716
717    // -----------------------------------------------------------------------
718    // Route handlers
719    // -----------------------------------------------------------------------
720
721    async fn handle_create<DB: DatabaseAdapter>(
722        &self,
723        req: &AuthRequest,
724        ctx: &AuthContext<DB>,
725    ) -> AuthResult<AuthResponse> {
726        let (user, _session) = ctx.require_session(req).await?;
727
728        let create_req: CreateKeyRequest = match better_auth_core::validate_request_body(req) {
729            Ok(v) => v,
730            Err(resp) => return Ok(resp),
731        };
732
733        // Validations
734        self.validate_prefix(create_req.prefix.as_deref())?;
735        self.validate_name(create_req.name.as_deref(), true)?;
736        self.validate_metadata(&create_req.metadata)?;
737        Self::validate_refill(create_req.refill_interval, create_req.refill_amount)?;
738
739        let effective_expires_in = self.validate_expires_in(create_req.expires_in)?;
740
741        let (full_key, hash, start) = self.generate_key(create_req.prefix.as_deref());
742
743        let expires_at = helpers::expires_in_to_at(effective_expires_in)?;
744
745        let remaining = create_req.remaining.or(self.config.default_remaining);
746
747        let store_start = if self.config.store_starting_characters {
748            Some(start)
749        } else {
750            None
751        };
752
753        let input = CreateApiKey {
754            user_id: user.id().to_string(),
755            name: create_req.name,
756            prefix: create_req.prefix.or_else(|| self.config.prefix.clone()),
757            key_hash: hash,
758            start: store_start,
759            expires_at,
760            remaining,
761            rate_limit_enabled: create_req.rate_limit_enabled.unwrap_or(false),
762            rate_limit_time_window: create_req.rate_limit_time_window,
763            rate_limit_max: create_req.rate_limit_max,
764            refill_interval: create_req.refill_interval,
765            refill_amount: create_req.refill_amount,
766            permissions: create_req
767                .permissions
768                .map(|v| serde_json::to_string(&v).unwrap_or_default()),
769            metadata: create_req
770                .metadata
771                .map(|v| serde_json::to_string(&v).unwrap_or_default()),
772            enabled: true,
773        };
774
775        let api_key = ctx.database.create_api_key(input).await?;
776
777        // Throttled cleanup
778        self.maybe_delete_expired(ctx).await;
779
780        let response = CreateKeyResponse {
781            key: full_key,
782            api_key: ApiKeyView::from_entity(&api_key),
783        };
784
785        Ok(AuthResponse::json(200, &response)?)
786    }
787
788    async fn handle_get<DB: DatabaseAdapter>(
789        &self,
790        req: &AuthRequest,
791        ctx: &AuthContext<DB>,
792    ) -> AuthResult<AuthResponse> {
793        let (user, _session) = ctx.require_session(req).await?;
794
795        let id = req
796            .query
797            .get("id")
798            .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
799
800        let api_key = helpers::get_owned_api_key(ctx, id, user.id()).await?;
801
802        self.maybe_delete_expired(ctx).await;
803
804        Ok(AuthResponse::json(200, &ApiKeyView::from_entity(&api_key))?)
805    }
806
807    async fn handle_list<DB: DatabaseAdapter>(
808        &self,
809        req: &AuthRequest,
810        ctx: &AuthContext<DB>,
811    ) -> AuthResult<AuthResponse> {
812        let (user, _session) = ctx.require_session(req).await?;
813
814        let keys = ctx.database.list_api_keys_by_user(user.id()).await?;
815
816        let views: Vec<ApiKeyView> = keys.iter().map(ApiKeyView::from_entity).collect();
817
818        self.maybe_delete_expired(ctx).await;
819
820        Ok(AuthResponse::json(200, &views)?)
821    }
822
823    async fn handle_update<DB: DatabaseAdapter>(
824        &self,
825        req: &AuthRequest,
826        ctx: &AuthContext<DB>,
827    ) -> AuthResult<AuthResponse> {
828        let (user, _session) = ctx.require_session(req).await?;
829
830        let update_req: UpdateKeyRequest = match better_auth_core::validate_request_body(req) {
831            Ok(v) => v,
832            Err(resp) => return Ok(resp),
833        };
834
835        // Validations
836        self.validate_name(update_req.name.as_deref(), false)?;
837        self.validate_metadata(&update_req.metadata)?;
838        Self::validate_refill(update_req.refill_interval, update_req.refill_amount)?;
839
840        // Ownership check via shared helper
841        let _existing = helpers::get_owned_api_key(ctx, &update_req.id, user.id()).await?;
842
843        // Build expires_at if expiresIn is provided
844        let expires_at = if let Some(ms) = update_req.expires_in {
845            let effective_ms = self.validate_expires_in(Some(ms))?;
846            helpers::expires_in_to_at(effective_ms)?.map(Some)
847        } else {
848            None
849        };
850
851        let update = UpdateApiKey {
852            name: update_req.name,
853            enabled: update_req.enabled,
854            remaining: update_req.remaining,
855            rate_limit_enabled: update_req.rate_limit_enabled,
856            rate_limit_time_window: update_req.rate_limit_time_window,
857            rate_limit_max: update_req.rate_limit_max,
858            refill_interval: update_req.refill_interval,
859            refill_amount: update_req.refill_amount,
860            permissions: update_req
861                .permissions
862                .map(|v| serde_json::to_string(&v).unwrap_or_default()),
863            metadata: update_req
864                .metadata
865                .map(|v| serde_json::to_string(&v).unwrap_or_default()),
866            expires_at,
867            last_request: None,
868            request_count: None,
869            last_refill_at: None,
870        };
871
872        let updated = ctx.database.update_api_key(&update_req.id, update).await?;
873
874        // Invalidate cached rate limiter if rate limit settings changed
875        if update_req.rate_limit_time_window.is_some()
876            || update_req.rate_limit_max.is_some()
877            || update_req.rate_limit_enabled.is_some()
878        {
879            self.rate_limiters
880                .lock()
881                .expect("rate_limiters mutex poisoned")
882                .remove(&update_req.id);
883        }
884
885        self.maybe_delete_expired(ctx).await;
886
887        Ok(AuthResponse::json(200, &ApiKeyView::from_entity(&updated))?)
888    }
889
890    async fn handle_delete<DB: DatabaseAdapter>(
891        &self,
892        req: &AuthRequest,
893        ctx: &AuthContext<DB>,
894    ) -> AuthResult<AuthResponse> {
895        let (user, _session) = ctx.require_session(req).await?;
896
897        let delete_req: DeleteKeyRequest = match better_auth_core::validate_request_body(req) {
898            Ok(v) => v,
899            Err(resp) => return Ok(resp),
900        };
901
902        // Ownership check via shared helper
903        let _existing = helpers::get_owned_api_key(ctx, &delete_req.id, user.id()).await?;
904
905        ctx.database.delete_api_key(&delete_req.id).await?;
906
907        // Evict cached rate limiter for the deleted key
908        self.rate_limiters
909            .lock()
910            .expect("rate_limiters mutex poisoned")
911            .remove(&delete_req.id);
912
913        Ok(AuthResponse::json(
914            200,
915            &serde_json::json!({ "status": true }),
916        )?)
917    }
918
919    // -----------------------------------------------------------------------
920    // POST /api-key/verify -- core verification endpoint
921    // -----------------------------------------------------------------------
922
923    async fn handle_verify<DB: DatabaseAdapter>(
924        &self,
925        req: &AuthRequest,
926        ctx: &AuthContext<DB>,
927    ) -> AuthResult<AuthResponse> {
928        let verify_req: VerifyKeyRequest = req
929            .body_as_json()
930            .map_err(|_| AuthError::bad_request("Invalid JSON body"))?;
931
932        let result = self
933            .validate_api_key(ctx, &verify_req.key, verify_req.permissions.as_ref())
934            .await;
935
936        match result {
937            Ok(view) => Ok(AuthResponse::json(
938                200,
939                &VerifyKeyResponse {
940                    valid: true,
941                    error: None,
942                    key: Some(view),
943                },
944            )?),
945            Err(validation_err) => {
946                // Structured error code -- no fragile string matching needed
947                let code_str = validation_err.code.as_str().to_string();
948                let message = validation_err.message;
949                Ok(AuthResponse::json(
950                    200,
951                    &VerifyKeyResponse {
952                        valid: false,
953                        error: Some(VerifyErrorBody {
954                            message,
955                            code: code_str,
956                        }),
957                        key: None,
958                    },
959                )?)
960            }
961        }
962    }
963
964    /// Core validation logic shared by `handle_verify` and `before_request`.
965    ///
966    /// Validation chain: exists -> disabled -> expired -> permissions ->
967    /// remaining/refill -> rate limit.
968    ///
969    /// Returns `Ok(ApiKeyView)` on success, or `Err(ApiKeyValidationError)` with
970    /// a structured error code (no fragile string matching needed).
971    async fn validate_api_key<DB: DatabaseAdapter>(
972        &self,
973        ctx: &AuthContext<DB>,
974        raw_key: &str,
975        required_permissions: Option<&serde_json::Value>,
976    ) -> Result<ApiKeyView, ApiKeyValidationError> {
977        // Hash the key (or use as-is if hashing is disabled)
978        let hashed = if self.config.disable_key_hashing {
979            raw_key.to_string()
980        } else {
981            Self::hash_key(raw_key)
982        };
983
984        // Look up by hash
985        let api_key = ctx
986            .database
987            .get_api_key_by_hash(&hashed)
988            .await
989            .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey))?
990            .ok_or_else(|| ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey))?;
991
992        // 1. Disabled?
993        if !api_key.enabled() {
994            return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyDisabled));
995        }
996
997        // 2. Expired?
998        if let Some(expires_at_str) = api_key.expires_at()
999            && let Ok(expires_at) = chrono::DateTime::parse_from_rfc3339(expires_at_str)
1000            && chrono::Utc::now() > expires_at
1001        {
1002            // Delete expired key and evict its cached rate limiter
1003            let _ = ctx.database.delete_api_key(api_key.id()).await;
1004            self.rate_limiters
1005                .lock()
1006                .expect("rate_limiters mutex poisoned")
1007                .remove(api_key.id());
1008            return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyExpired));
1009        }
1010
1011        // 3. Permissions check
1012        if let Some(required) = required_permissions {
1013            let key_perms_str = api_key.permissions().unwrap_or("");
1014            if key_perms_str.is_empty() {
1015                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyNotFound));
1016            }
1017            if !check_permissions(key_perms_str, required) {
1018                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyNotFound));
1019            }
1020        }
1021
1022        // 4. Remaining / refill
1023        let mut new_remaining = api_key.remaining();
1024        let mut new_last_refill_at: Option<String> =
1025            api_key.last_refill_at().map(|s| s.to_string());
1026
1027        if let Some(0) = api_key.remaining()
1028            && api_key.refill_amount().is_none()
1029        {
1030            // Usage exhausted, no refill configured -- delete key and evict cache
1031            let _ = ctx.database.delete_api_key(api_key.id()).await;
1032            self.rate_limiters
1033                .lock()
1034                .expect("rate_limiters mutex poisoned")
1035                .remove(api_key.id());
1036            return Err(ApiKeyValidationError::new(ApiKeyErrorCode::UsageExceeded));
1037        }
1038
1039        if let Some(remaining) = api_key.remaining() {
1040            let refill_interval = api_key.refill_interval();
1041            let refill_amount = api_key.refill_amount();
1042            let mut current_remaining = remaining;
1043
1044            if let (Some(interval), Some(amount)) = (refill_interval, refill_amount) {
1045                let now = chrono::Utc::now();
1046                let last_time_str = api_key
1047                    .last_refill_at()
1048                    .or_else(|| Some(api_key.created_at()));
1049                if let Some(last_str) = last_time_str
1050                    && let Ok(last_dt) = chrono::DateTime::parse_from_rfc3339(last_str)
1051                {
1052                    let elapsed_ms = (now - last_dt.with_timezone(&chrono::Utc)).num_milliseconds();
1053                    if elapsed_ms > interval {
1054                        current_remaining = amount;
1055                        new_last_refill_at = Some(now.to_rfc3339());
1056                    }
1057                }
1058            }
1059
1060            if current_remaining <= 0 {
1061                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::UsageExceeded));
1062            }
1063
1064            new_remaining = Some(current_remaining - 1);
1065        }
1066
1067        // 5. Rate limiting via `governor` crate
1068        self.check_rate_limit_governor(&api_key)?;
1069
1070        // 6. Build update
1071        let mut update = UpdateApiKey {
1072            remaining: new_remaining,
1073            ..Default::default()
1074        };
1075        if new_last_refill_at != api_key.last_refill_at().map(|s| s.to_string()) {
1076            update.last_refill_at = Some(new_last_refill_at);
1077        }
1078
1079        let updated = ctx
1080            .database
1081            .update_api_key(api_key.id(), update)
1082            .await
1083            .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::FailedToUpdateApiKey))?;
1084
1085        // Throttled cleanup
1086        self.maybe_delete_expired(ctx).await;
1087
1088        Ok(ApiKeyView::from_entity(&updated))
1089    }
1090
1091    /// Check rate limiting for an API key using the `governor` crate.
1092    ///
1093    /// Creates or retrieves a per-key in-memory rate limiter backed by GCRA
1094    /// (Generic Cell Rate Algorithm), which is thread-safe and lock-free on
1095    /// the hot path.
1096    fn check_rate_limit_governor(
1097        &self,
1098        api_key: &impl AuthApiKey,
1099    ) -> Result<(), ApiKeyValidationError> {
1100        // Determine if rate limiting is enabled for this key.
1101        // Per-key `rate_limit_enabled` takes precedence: if the key
1102        // explicitly disables rate limiting, skip even when the global
1103        // default is enabled.
1104        // A key is considered to have "explicit" rate-limit configuration
1105        // when either `rate_limit_time_window` or `rate_limit_max` is set.
1106        // This distinguishes an intentional `enabled=false` (key owner chose
1107        // to disable) from the default `false` (no opinion, defer to global).
1108        //
1109        // Partial configs are valid: if only `time_window` is set, the
1110        // missing `max` falls back to the global default (and vice-versa).
1111        let key_has_explicit_setting =
1112            api_key.rate_limit_time_window().is_some() || api_key.rate_limit_max().is_some();
1113        let key_enabled = api_key.rate_limit_enabled();
1114
1115        if !key_enabled {
1116            // Key explicitly disabled rate limiting — skip.
1117            if key_has_explicit_setting {
1118                return Ok(());
1119            }
1120            // Key has no explicit setting and global is also off — skip.
1121            if !self.config.rate_limit.enabled {
1122                return Ok(());
1123            }
1124        }
1125
1126        let time_window_ms = api_key
1127            .rate_limit_time_window()
1128            .unwrap_or(self.config.rate_limit.time_window);
1129        let max_requests = api_key
1130            .rate_limit_max()
1131            .unwrap_or(self.config.rate_limit.max_requests);
1132
1133        if time_window_ms <= 0 || max_requests <= 0 {
1134            return Ok(());
1135        }
1136
1137        let key_id = api_key.id().to_string();
1138
1139        // Get or create the rate limiter for this key
1140        let limiter = {
1141            let mut limiters = self
1142                .rate_limiters
1143                .lock()
1144                .expect("rate_limiters mutex poisoned");
1145            limiters
1146                .entry(key_id)
1147                .or_insert_with(|| {
1148                    let max = NonZeroU32::new(max_requests as u32).unwrap_or(NonZeroU32::MIN);
1149                    let period_ms = (time_window_ms as u64)
1150                        .checked_div(max_requests as u64)
1151                        .unwrap_or(0);
1152                    // Guard against zero-period panic (e.g. time_window_ms < max_requests)
1153                    let period = std::time::Duration::from_millis(period_ms.max(1));
1154                    let quota = Quota::with_period(period)
1155                        .expect("period >= 1ms is always valid")
1156                        .allow_burst(max);
1157                    std::sync::Arc::new(RateLimiter::direct(quota))
1158                })
1159                .clone()
1160        };
1161
1162        match limiter.check() {
1163            Ok(()) => Ok(()),
1164            Err(_not_until) => Err(ApiKeyValidationError::new(ApiKeyErrorCode::RateLimited)),
1165        }
1166    }
1167
1168    // -----------------------------------------------------------------------
1169    // POST /api-key/delete-all-expired-api-keys
1170    // -----------------------------------------------------------------------
1171
1172    async fn handle_delete_all_expired<DB: DatabaseAdapter>(
1173        &self,
1174        req: &AuthRequest,
1175        ctx: &AuthContext<DB>,
1176    ) -> AuthResult<AuthResponse> {
1177        // Require authentication to prevent unauthenticated mass-deletion
1178        let (_user, _session) = ctx.require_session(req).await?;
1179        let count = ctx.database.delete_expired_api_keys().await?;
1180
1181        // Best-effort eviction: clear all cached limiters when bulk-deleting.
1182        // The `delete_expired_api_keys` adapter method returns only a count,
1183        // not the set of deleted IDs, so we cannot do targeted eviction.
1184        // Clearing the entire cache means active keys temporarily lose their
1185        // rate-limit window state and will have a fresh limiter created on
1186        // their next request.  This is acceptable because:
1187        //   1. Bulk-expire is an infrequent admin operation.
1188        //   2. Recreating a limiter is O(1) and only slightly more
1189        //      permissive during the reset window.
1190        if count > 0 {
1191            self.rate_limiters
1192                .lock()
1193                .expect("rate_limiters mutex poisoned")
1194                .clear();
1195        }
1196
1197        Ok(AuthResponse::json(
1198            200,
1199            &serde_json::json!({ "deleted": count }),
1200        )?)
1201    }
1202}
1203
1204// NOTE: The old `extract_error_info()` function that used fragile string
1205// matching has been removed.  `handle_verify` now uses the structured
1206// `ApiKeyValidationError` directly to get the error code and message.
1207
1208// ---------------------------------------------------------------------------
1209// AuthPlugin trait implementation
1210// ---------------------------------------------------------------------------
1211
1212#[async_trait]
1213impl<DB: DatabaseAdapter> AuthPlugin<DB> for ApiKeyPlugin {
1214    fn name(&self) -> &'static str {
1215        "api-key"
1216    }
1217
1218    fn routes(&self) -> Vec<AuthRoute> {
1219        vec![
1220            AuthRoute::post("/api-key/create", "api_key_create"),
1221            AuthRoute::get("/api-key/get", "api_key_get"),
1222            AuthRoute::post("/api-key/update", "api_key_update"),
1223            AuthRoute::post("/api-key/delete", "api_key_delete"),
1224            AuthRoute::get("/api-key/list", "api_key_list"),
1225            AuthRoute::post("/api-key/verify", "api_key_verify"),
1226            AuthRoute::post(
1227                "/api-key/delete-all-expired-api-keys",
1228                "api_key_delete_all_expired",
1229            ),
1230        ]
1231    }
1232
1233    async fn before_request(
1234        &self,
1235        req: &AuthRequest,
1236        ctx: &AuthContext<DB>,
1237    ) -> AuthResult<Option<BeforeRequestAction>> {
1238        if !self.config.enable_session_for_api_keys {
1239            return Ok(None);
1240        }
1241
1242        // Check for API key in the configured header
1243        let raw_key = match req.headers.get(&self.config.api_key_header) {
1244            Some(k) if !k.is_empty() => k.clone(),
1245            _ => return Ok(None),
1246        };
1247
1248        // Skip session emulation for API-key management routes to avoid
1249        // double-validating the key (before_request + handle_verify both
1250        // call validate_api_key, consuming usage/rate-limit budget twice).
1251        if req.path().starts_with("/api-key/") {
1252            return Ok(None);
1253        }
1254
1255        // Validate the key (reuses the full verify logic)
1256        let view = self
1257            .validate_api_key(ctx, &raw_key, None)
1258            .await
1259            .map_err(|e| AuthError::bad_request(e.message))?;
1260
1261        // Look up the user
1262        let user = ctx
1263            .database
1264            .get_user_by_id(&view.user_id)
1265            .await?
1266            .ok_or_else(|| api_key_error(ApiKeyErrorCode::InvalidUserIdFromApiKey))?;
1267
1268        // Build a virtual session response for `/get-session`
1269        if req.path() == "/get-session" {
1270            let session_json = serde_json::json!({
1271                "user": {
1272                    "id": user.id(),
1273                    "email": user.email(),
1274                    "name": user.name(),
1275                },
1276                "session": {
1277                    "id": view.id,
1278                    "token": raw_key,
1279                    "userId": view.user_id,
1280                }
1281            });
1282            return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
1283                200,
1284                &session_json,
1285            )?)));
1286        }
1287
1288        // For all other routes, inject the session
1289        Ok(Some(BeforeRequestAction::InjectSession {
1290            user_id: view.user_id,
1291            session_token: raw_key,
1292        }))
1293    }
1294
1295    async fn on_request(
1296        &self,
1297        req: &AuthRequest,
1298        ctx: &AuthContext<DB>,
1299    ) -> AuthResult<Option<AuthResponse>> {
1300        match (req.method(), req.path()) {
1301            (HttpMethod::Post, "/api-key/create") => Ok(Some(self.handle_create(req, ctx).await?)),
1302            (HttpMethod::Get, "/api-key/get") => Ok(Some(self.handle_get(req, ctx).await?)),
1303            (HttpMethod::Post, "/api-key/update") => Ok(Some(self.handle_update(req, ctx).await?)),
1304            (HttpMethod::Post, "/api-key/delete") => Ok(Some(self.handle_delete(req, ctx).await?)),
1305            (HttpMethod::Get, "/api-key/list") => Ok(Some(self.handle_list(req, ctx).await?)),
1306            (HttpMethod::Post, "/api-key/verify") => Ok(Some(self.handle_verify(req, ctx).await?)),
1307            (HttpMethod::Post, "/api-key/delete-all-expired-api-keys") => {
1308                Ok(Some(self.handle_delete_all_expired(req, ctx).await?))
1309            }
1310            _ => Ok(None),
1311        }
1312    }
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317    use super::*;
1318    use better_auth_core::adapters::{ApiKeyOps, MemoryDatabaseAdapter, SessionOps, UserOps};
1319    use better_auth_core::{CreateSession, CreateUser, Session, User};
1320    use chrono::{Duration, Utc};
1321    use std::collections::HashMap;
1322    use std::sync::Arc;
1323
1324    async fn create_test_context_with_user() -> (AuthContext<MemoryDatabaseAdapter>, User, Session)
1325    {
1326        let config = Arc::new(better_auth_core::AuthConfig::new(
1327            "test-secret-key-at-least-32-chars-long",
1328        ));
1329        let database = Arc::new(MemoryDatabaseAdapter::new());
1330        let ctx = AuthContext::new(config, database.clone());
1331
1332        let user = database
1333            .create_user(
1334                CreateUser::new()
1335                    .with_email("test@example.com")
1336                    .with_name("Test User"),
1337            )
1338            .await
1339            .unwrap();
1340
1341        let session = database
1342            .create_session(CreateSession {
1343                user_id: user.id.clone(),
1344                expires_at: Utc::now() + Duration::hours(24),
1345                ip_address: Some("127.0.0.1".to_string()),
1346                user_agent: Some("test-agent".to_string()),
1347                impersonated_by: None,
1348                active_organization_id: None,
1349            })
1350            .await
1351            .unwrap();
1352
1353        (ctx, user, session)
1354    }
1355
1356    async fn create_user_with_session(
1357        ctx: &AuthContext<MemoryDatabaseAdapter>,
1358        email: &str,
1359    ) -> (User, Session) {
1360        let user = ctx
1361            .database
1362            .create_user(
1363                CreateUser::new()
1364                    .with_email(email.to_string())
1365                    .with_name("Another User"),
1366            )
1367            .await
1368            .unwrap();
1369
1370        let session = ctx
1371            .database
1372            .create_session(CreateSession {
1373                user_id: user.id.clone(),
1374                expires_at: Utc::now() + Duration::hours(24),
1375                ip_address: None,
1376                user_agent: None,
1377                impersonated_by: None,
1378                active_organization_id: None,
1379            })
1380            .await
1381            .unwrap();
1382
1383        (user, session)
1384    }
1385
1386    fn create_auth_request(
1387        method: HttpMethod,
1388        path: &str,
1389        token: Option<&str>,
1390        body: Option<serde_json::Value>,
1391        query: Option<HashMap<String, String>>,
1392    ) -> AuthRequest {
1393        let mut headers = HashMap::new();
1394        if let Some(token) = token {
1395            headers.insert("authorization".to_string(), format!("Bearer {}", token));
1396        }
1397
1398        AuthRequest::from_parts(
1399            method,
1400            path.to_string(),
1401            headers,
1402            body.map(|b| serde_json::to_vec(&b).unwrap()),
1403            query.unwrap_or_default(),
1404        )
1405    }
1406
1407    fn json_body(response: &AuthResponse) -> serde_json::Value {
1408        serde_json::from_slice(&response.body).unwrap()
1409    }
1410
1411    async fn create_key_and_get_id(
1412        plugin: &ApiKeyPlugin,
1413        ctx: &AuthContext<MemoryDatabaseAdapter>,
1414        token: &str,
1415        name: &str,
1416    ) -> String {
1417        let req = create_auth_request(
1418            HttpMethod::Post,
1419            "/api-key/create",
1420            Some(token),
1421            Some(serde_json::json!({ "name": name })),
1422            None,
1423        );
1424        let response = plugin.handle_create(&req, ctx).await.unwrap();
1425        assert_eq!(response.status, 200);
1426        json_body(&response)["id"].as_str().unwrap().to_string()
1427    }
1428
1429    /// Helper: create a key and return (id, raw_key)
1430    async fn create_key_and_get_raw(
1431        plugin: &ApiKeyPlugin,
1432        ctx: &AuthContext<MemoryDatabaseAdapter>,
1433        token: &str,
1434        body: serde_json::Value,
1435    ) -> (String, String) {
1436        let req = create_auth_request(
1437            HttpMethod::Post,
1438            "/api-key/create",
1439            Some(token),
1440            Some(body),
1441            None,
1442        );
1443        let response = plugin.handle_create(&req, ctx).await.unwrap();
1444        assert_eq!(response.status, 200);
1445        let b = json_body(&response);
1446        (
1447            b["id"].as_str().unwrap().to_string(),
1448            b["key"].as_str().unwrap().to_string(),
1449        )
1450    }
1451
1452    // -----------------------------------------------------------------------
1453    // Existing tests (kept)
1454    // -----------------------------------------------------------------------
1455
1456    #[tokio::test]
1457    async fn test_create_and_get_do_not_expose_hash() {
1458        let plugin = ApiKeyPlugin::builder().prefix("ba_".to_string()).build();
1459        let (ctx, _user, session) = create_test_context_with_user().await;
1460
1461        let create_req = create_auth_request(
1462            HttpMethod::Post,
1463            "/api-key/create",
1464            Some(&session.token),
1465            Some(serde_json::json!({ "name": "primary" })),
1466            None,
1467        );
1468        let create_response = plugin.handle_create(&create_req, &ctx).await.unwrap();
1469        assert_eq!(create_response.status, 200);
1470
1471        let body = json_body(&create_response);
1472        assert!(body.get("key").is_some());
1473        assert!(body.get("key_hash").is_none());
1474        assert!(body.get("hash").is_none());
1475
1476        let id = body["id"].as_str().unwrap();
1477        let mut query = HashMap::new();
1478        query.insert("id".to_string(), id.to_string());
1479
1480        let get_req = create_auth_request(
1481            HttpMethod::Get,
1482            "/api-key/get",
1483            Some(&session.token),
1484            None,
1485            Some(query),
1486        );
1487        let get_response = plugin.handle_get(&get_req, &ctx).await.unwrap();
1488        assert_eq!(get_response.status, 200);
1489
1490        let get_body = json_body(&get_response);
1491        assert!(get_body.get("key").is_none());
1492        assert!(get_body.get("key_hash").is_none());
1493    }
1494
1495    #[tokio::test]
1496    async fn test_create_rejects_invalid_expires_in() {
1497        let plugin = ApiKeyPlugin::builder().build();
1498        let (ctx, _user, session) = create_test_context_with_user().await;
1499
1500        let req = create_auth_request(
1501            HttpMethod::Post,
1502            "/api-key/create",
1503            Some(&session.token),
1504            Some(serde_json::json!({ "expiresIn": -1 })),
1505            None,
1506        );
1507        let response = plugin.handle_create(&req, &ctx).await;
1508        // Should be rejected due to validation (negative expires_in)
1509        assert!(response.is_err() || response.unwrap().status != 200);
1510    }
1511
1512    #[tokio::test]
1513    async fn test_get_update_delete_return_404_for_non_owner() {
1514        let plugin = ApiKeyPlugin::builder().build();
1515        let (ctx, _user1, session1) = create_test_context_with_user().await;
1516        let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1517        let key_id = create_key_and_get_id(&plugin, &ctx, &session1.token, "owner-key").await;
1518
1519        let mut get_query = HashMap::new();
1520        get_query.insert("id".to_string(), key_id.clone());
1521        let get_req = create_auth_request(
1522            HttpMethod::Get,
1523            "/api-key/get",
1524            Some(&session2.token),
1525            None,
1526            Some(get_query),
1527        );
1528        let get_err = plugin.handle_get(&get_req, &ctx).await.unwrap_err();
1529        assert_eq!(get_err.status_code(), 404);
1530
1531        let update_req = create_auth_request(
1532            HttpMethod::Post,
1533            "/api-key/update",
1534            Some(&session2.token),
1535            Some(serde_json::json!({ "id": key_id, "name": "new-name" })),
1536            None,
1537        );
1538        let update_err = plugin.handle_update(&update_req, &ctx).await.unwrap_err();
1539        assert_eq!(update_err.status_code(), 404);
1540
1541        let delete_req = create_auth_request(
1542            HttpMethod::Post,
1543            "/api-key/delete",
1544            Some(&session2.token),
1545            Some(serde_json::json!({ "id": key_id })),
1546            None,
1547        );
1548        let delete_err = plugin.handle_delete(&delete_req, &ctx).await.unwrap_err();
1549        assert_eq!(delete_err.status_code(), 404);
1550    }
1551
1552    #[tokio::test]
1553    async fn test_list_returns_only_user_keys() {
1554        let plugin = ApiKeyPlugin::builder().build();
1555        let (ctx, user1, session1) = create_test_context_with_user().await;
1556        let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1557
1558        let _ = create_key_and_get_id(&plugin, &ctx, &session1.token, "u1-key").await;
1559        let _ = create_key_and_get_id(&plugin, &ctx, &session2.token, "u2-key").await;
1560
1561        let list_req = create_auth_request(
1562            HttpMethod::Get,
1563            "/api-key/list",
1564            Some(&session1.token),
1565            None,
1566            None,
1567        );
1568        let list_response = plugin.handle_list(&list_req, &ctx).await.unwrap();
1569        assert_eq!(list_response.status, 200);
1570
1571        let list_body = json_body(&list_response);
1572        let list = list_body.as_array().unwrap();
1573        assert_eq!(list.len(), 1);
1574        assert_eq!(list[0]["userId"].as_str().unwrap(), user1.id);
1575        assert!(list[0].get("key").is_none());
1576        assert!(list[0].get("key_hash").is_none());
1577    }
1578
1579    #[tokio::test]
1580    async fn test_owner_can_delete_key() {
1581        let plugin = ApiKeyPlugin::builder().build();
1582        let (ctx, _user, session) = create_test_context_with_user().await;
1583        let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "to-delete").await;
1584
1585        let delete_req = create_auth_request(
1586            HttpMethod::Post,
1587            "/api-key/delete",
1588            Some(&session.token),
1589            Some(serde_json::json!({ "id": key_id })),
1590            None,
1591        );
1592        let delete_response = plugin.handle_delete(&delete_req, &ctx).await.unwrap();
1593        assert_eq!(delete_response.status, 200);
1594
1595        let deleted = ctx.database.get_api_key_by_id(&key_id).await.unwrap();
1596        assert!(deleted.is_none());
1597    }
1598
1599    // -----------------------------------------------------------------------
1600    // New tests: verify, rate-limit, remaining/refill, delete expired, config
1601    // -----------------------------------------------------------------------
1602
1603    #[tokio::test]
1604    async fn test_verify_valid_key() {
1605        let plugin = ApiKeyPlugin::builder().build();
1606        let (ctx, _user, session) = create_test_context_with_user().await;
1607
1608        let (_id, raw_key) = create_key_and_get_raw(
1609            &plugin,
1610            &ctx,
1611            &session.token,
1612            serde_json::json!({ "name": "verify-test" }),
1613        )
1614        .await;
1615
1616        let verify_req = create_auth_request(
1617            HttpMethod::Post,
1618            "/api-key/verify",
1619            None,
1620            Some(serde_json::json!({ "key": raw_key })),
1621            None,
1622        );
1623        let resp = plugin.handle_verify(&verify_req, &ctx).await.unwrap();
1624        assert_eq!(resp.status, 200);
1625        let body = json_body(&resp);
1626        assert_eq!(body["valid"], true);
1627        assert!(body["key"].is_object());
1628    }
1629
1630    #[tokio::test]
1631    async fn test_verify_invalid_key() {
1632        let plugin = ApiKeyPlugin::builder().build();
1633        let (ctx, _user, _session) = create_test_context_with_user().await;
1634
1635        let verify_req = create_auth_request(
1636            HttpMethod::Post,
1637            "/api-key/verify",
1638            None,
1639            Some(serde_json::json!({ "key": "definitely-not-a-valid-key" })),
1640            None,
1641        );
1642        let resp = plugin.handle_verify(&verify_req, &ctx).await.unwrap();
1643        assert_eq!(resp.status, 200);
1644        let body = json_body(&resp);
1645        assert_eq!(body["valid"], false);
1646        assert!(body["error"].is_object());
1647    }
1648
1649    #[tokio::test]
1650    async fn test_verify_disabled_key() {
1651        let plugin = ApiKeyPlugin::builder().build();
1652        let (ctx, _user, session) = create_test_context_with_user().await;
1653
1654        let (id, raw_key) = create_key_and_get_raw(
1655            &plugin,
1656            &ctx,
1657            &session.token,
1658            serde_json::json!({ "name": "disable-test" }),
1659        )
1660        .await;
1661
1662        // Disable the key
1663        let update = UpdateApiKey {
1664            enabled: Some(false),
1665            ..Default::default()
1666        };
1667        ctx.database.update_api_key(&id, update).await.unwrap();
1668
1669        let verify_req = create_auth_request(
1670            HttpMethod::Post,
1671            "/api-key/verify",
1672            None,
1673            Some(serde_json::json!({ "key": raw_key })),
1674            None,
1675        );
1676        let resp = plugin.handle_verify(&verify_req, &ctx).await.unwrap();
1677        let body = json_body(&resp);
1678        assert_eq!(body["valid"], false);
1679        assert_eq!(body["error"]["code"], "KEY_DISABLED");
1680    }
1681
1682    #[tokio::test]
1683    async fn test_verify_expired_key() {
1684        let plugin = ApiKeyPlugin::builder().build();
1685        let (ctx, _user, session) = create_test_context_with_user().await;
1686
1687        let (id, raw_key) = create_key_and_get_raw(
1688            &plugin,
1689            &ctx,
1690            &session.token,
1691            serde_json::json!({ "name": "expire-test" }),
1692        )
1693        .await;
1694
1695        // Set expiration to the past
1696        let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1697        let update = UpdateApiKey {
1698            expires_at: Some(Some(past)),
1699            ..Default::default()
1700        };
1701        ctx.database.update_api_key(&id, update).await.unwrap();
1702
1703        let verify_req = create_auth_request(
1704            HttpMethod::Post,
1705            "/api-key/verify",
1706            None,
1707            Some(serde_json::json!({ "key": raw_key })),
1708            None,
1709        );
1710        let resp = plugin.handle_verify(&verify_req, &ctx).await.unwrap();
1711        let body = json_body(&resp);
1712        assert_eq!(body["valid"], false);
1713        assert_eq!(body["error"]["code"], "KEY_EXPIRED");
1714
1715        // The key should have been deleted
1716        let deleted = ctx.database.get_api_key_by_id(&id).await.unwrap();
1717        assert!(deleted.is_none());
1718    }
1719
1720    #[tokio::test]
1721    async fn test_verify_remaining_consumption() {
1722        let plugin = ApiKeyPlugin::builder().build();
1723        let (ctx, _user, session) = create_test_context_with_user().await;
1724
1725        let (_id, raw_key) = create_key_and_get_raw(
1726            &plugin,
1727            &ctx,
1728            &session.token,
1729            serde_json::json!({ "name": "remain-test", "remaining": 2 }),
1730        )
1731        .await;
1732
1733        let make_verify = |key: &str| {
1734            create_auth_request(
1735                HttpMethod::Post,
1736                "/api-key/verify",
1737                None,
1738                Some(serde_json::json!({ "key": key })),
1739                None,
1740            )
1741        };
1742
1743        // First verify - remaining goes from 2 to 1
1744        let resp1 = plugin
1745            .handle_verify(&make_verify(&raw_key), &ctx)
1746            .await
1747            .unwrap();
1748        assert_eq!(json_body(&resp1)["valid"], true);
1749        assert_eq!(json_body(&resp1)["key"]["remaining"], 1);
1750
1751        // Second verify - remaining goes from 1 to 0
1752        let resp2 = plugin
1753            .handle_verify(&make_verify(&raw_key), &ctx)
1754            .await
1755            .unwrap();
1756        assert_eq!(json_body(&resp2)["valid"], true);
1757        assert_eq!(json_body(&resp2)["key"]["remaining"], 0);
1758
1759        // Third verify - should fail (usage exceeded)
1760        let resp3 = plugin
1761            .handle_verify(&make_verify(&raw_key), &ctx)
1762            .await
1763            .unwrap();
1764        let body3 = json_body(&resp3);
1765        assert_eq!(body3["valid"], false);
1766        assert_eq!(body3["error"]["code"], "USAGE_EXCEEDED");
1767    }
1768
1769    #[tokio::test]
1770    async fn test_verify_rate_limiting() {
1771        let plugin = ApiKeyPlugin::builder()
1772            .rate_limit(RateLimitDefaults {
1773                enabled: true,
1774                time_window: 60_000,
1775                max_requests: 2,
1776            })
1777            .build();
1778        let (ctx, _user, session) = create_test_context_with_user().await;
1779
1780        let (_id, raw_key) = create_key_and_get_raw(
1781            &plugin,
1782            &ctx,
1783            &session.token,
1784            serde_json::json!({
1785                "name": "rl-test",
1786                "rateLimitEnabled": true,
1787                "rateLimitTimeWindow": 60000,
1788                "rateLimitMax": 2
1789            }),
1790        )
1791        .await;
1792
1793        let make_verify = |key: &str| {
1794            create_auth_request(
1795                HttpMethod::Post,
1796                "/api-key/verify",
1797                None,
1798                Some(serde_json::json!({ "key": key })),
1799                None,
1800            )
1801        };
1802
1803        // First two should succeed
1804        let r1 = plugin
1805            .handle_verify(&make_verify(&raw_key), &ctx)
1806            .await
1807            .unwrap();
1808        assert_eq!(json_body(&r1)["valid"], true);
1809
1810        let r2 = plugin
1811            .handle_verify(&make_verify(&raw_key), &ctx)
1812            .await
1813            .unwrap();
1814        assert_eq!(json_body(&r2)["valid"], true);
1815
1816        // Third should fail with rate limit
1817        let r3 = plugin
1818            .handle_verify(&make_verify(&raw_key), &ctx)
1819            .await
1820            .unwrap();
1821        let b3 = json_body(&r3);
1822        assert_eq!(b3["valid"], false);
1823        assert_eq!(b3["error"]["code"], "RATE_LIMITED");
1824    }
1825
1826    #[tokio::test]
1827    async fn test_delete_all_expired() {
1828        let plugin = ApiKeyPlugin::builder().build();
1829        let (ctx, _user, session) = create_test_context_with_user().await;
1830
1831        // Create two keys
1832        let (id1, _) = create_key_and_get_raw(
1833            &plugin,
1834            &ctx,
1835            &session.token,
1836            serde_json::json!({ "name": "will-expire" }),
1837        )
1838        .await;
1839        let (_id2, _) = create_key_and_get_raw(
1840            &plugin,
1841            &ctx,
1842            &session.token,
1843            serde_json::json!({ "name": "wont-expire" }),
1844        )
1845        .await;
1846
1847        // Expire the first key
1848        let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1849        ctx.database
1850            .update_api_key(
1851                &id1,
1852                UpdateApiKey {
1853                    expires_at: Some(Some(past)),
1854                    ..Default::default()
1855                },
1856            )
1857            .await
1858            .unwrap();
1859
1860        let delete_req = create_auth_request(
1861            HttpMethod::Post,
1862            "/api-key/delete-all-expired-api-keys",
1863            Some(&session.token),
1864            None,
1865            None,
1866        );
1867        let resp = plugin
1868            .handle_delete_all_expired(&delete_req, &ctx)
1869            .await
1870            .unwrap();
1871        assert_eq!(resp.status, 200);
1872        let body = json_body(&resp);
1873        assert_eq!(body["deleted"], 1);
1874
1875        // Only the non-expired key should remain
1876        let remaining_keys = ctx.database.list_api_keys_by_user(&_user.id).await.unwrap();
1877        assert_eq!(remaining_keys.len(), 1);
1878    }
1879
1880    #[tokio::test]
1881    async fn test_verify_permissions() {
1882        let plugin = ApiKeyPlugin::builder().build();
1883        let (ctx, _user, session) = create_test_context_with_user().await;
1884
1885        let (_id, raw_key) = create_key_and_get_raw(
1886            &plugin,
1887            &ctx,
1888            &session.token,
1889            serde_json::json!({
1890                "name": "perm-test",
1891                "permissions": { "admin": ["read", "write"], "user": ["read"] }
1892            }),
1893        )
1894        .await;
1895
1896        // Verify with matching permissions -> should pass
1897        let verify_ok = create_auth_request(
1898            HttpMethod::Post,
1899            "/api-key/verify",
1900            None,
1901            Some(serde_json::json!({
1902                "key": raw_key,
1903                "permissions": { "admin": ["read"] }
1904            })),
1905            None,
1906        );
1907        let r1 = plugin.handle_verify(&verify_ok, &ctx).await.unwrap();
1908        assert_eq!(json_body(&r1)["valid"], true);
1909
1910        // Verify with non-matching permissions -> should fail
1911        let verify_fail = create_auth_request(
1912            HttpMethod::Post,
1913            "/api-key/verify",
1914            None,
1915            Some(serde_json::json!({
1916                "key": raw_key,
1917                "permissions": { "superadmin": ["delete"] }
1918            })),
1919            None,
1920        );
1921        let r2 = plugin.handle_verify(&verify_fail, &ctx).await.unwrap();
1922        assert_eq!(json_body(&r2)["valid"], false);
1923    }
1924
1925    #[tokio::test]
1926    async fn test_config_validation_prefix_length() {
1927        let plugin = ApiKeyPlugin::builder()
1928            .min_prefix_length(2)
1929            .max_prefix_length(5)
1930            .build();
1931        let (ctx, _user, session) = create_test_context_with_user().await;
1932
1933        // Too short prefix
1934        let req = create_auth_request(
1935            HttpMethod::Post,
1936            "/api-key/create",
1937            Some(&session.token),
1938            Some(serde_json::json!({ "name": "test", "prefix": "a" })),
1939            None,
1940        );
1941        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1942        assert!(err.to_string().contains("prefix length"));
1943
1944        // Too long prefix
1945        let req2 = create_auth_request(
1946            HttpMethod::Post,
1947            "/api-key/create",
1948            Some(&session.token),
1949            Some(serde_json::json!({ "name": "test", "prefix": "toolong" })),
1950            None,
1951        );
1952        let err2 = plugin.handle_create(&req2, &ctx).await.unwrap_err();
1953        assert!(err2.to_string().contains("prefix length"));
1954    }
1955
1956    #[tokio::test]
1957    async fn test_config_require_name() {
1958        let plugin = ApiKeyPlugin::builder().require_name(true).build();
1959        let (ctx, _user, session) = create_test_context_with_user().await;
1960
1961        // No name provided -> should fail
1962        let req = create_auth_request(
1963            HttpMethod::Post,
1964            "/api-key/create",
1965            Some(&session.token),
1966            Some(serde_json::json!({})),
1967            None,
1968        );
1969        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1970        assert!(err.to_string().contains("name is required"));
1971    }
1972
1973    #[tokio::test]
1974    async fn test_config_metadata_disabled() {
1975        let plugin = ApiKeyPlugin::builder().build(); // enable_metadata defaults to false
1976        let (ctx, _user, session) = create_test_context_with_user().await;
1977
1978        let req = create_auth_request(
1979            HttpMethod::Post,
1980            "/api-key/create",
1981            Some(&session.token),
1982            Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1983            None,
1984        );
1985        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1986        assert!(err.to_string().contains("Metadata is disabled"));
1987    }
1988
1989    #[tokio::test]
1990    async fn test_config_metadata_enabled() {
1991        let plugin = ApiKeyPlugin::builder().enable_metadata(true).build();
1992        let (ctx, _user, session) = create_test_context_with_user().await;
1993
1994        let req = create_auth_request(
1995            HttpMethod::Post,
1996            "/api-key/create",
1997            Some(&session.token),
1998            Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1999            None,
2000        );
2001        let resp = plugin.handle_create(&req, &ctx).await.unwrap();
2002        assert_eq!(resp.status, 200);
2003        let body = json_body(&resp);
2004        assert_eq!(body["metadata"]["env"], "prod");
2005    }
2006
2007    #[tokio::test]
2008    async fn test_update_with_expires_in() {
2009        let plugin = ApiKeyPlugin::builder().build();
2010        let (ctx, _user, session) = create_test_context_with_user().await;
2011        let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "update-exp").await;
2012
2013        let update_req = create_auth_request(
2014            HttpMethod::Post,
2015            "/api-key/update",
2016            Some(&session.token),
2017            Some(serde_json::json!({
2018                "id": key_id,
2019                "expiresIn": 86400000
2020            })),
2021            None,
2022        );
2023        let resp = plugin.handle_update(&update_req, &ctx).await.unwrap();
2024        assert_eq!(resp.status, 200);
2025        let body = json_body(&resp);
2026        assert!(body["expiresAt"].is_string());
2027    }
2028
2029    #[tokio::test]
2030    async fn test_on_request_dispatches_verify() {
2031        let plugin = ApiKeyPlugin::builder().build();
2032        let (ctx, _user, session) = create_test_context_with_user().await;
2033
2034        let (_id, raw_key) = create_key_and_get_raw(
2035            &plugin,
2036            &ctx,
2037            &session.token,
2038            serde_json::json!({ "name": "dispatch-test" }),
2039        )
2040        .await;
2041
2042        let verify_req = create_auth_request(
2043            HttpMethod::Post,
2044            "/api-key/verify",
2045            None,
2046            Some(serde_json::json!({ "key": raw_key })),
2047            None,
2048        );
2049        let resp = plugin.on_request(&verify_req, &ctx).await.unwrap();
2050        assert!(resp.is_some());
2051        let body = json_body(&resp.unwrap());
2052        assert_eq!(body["valid"], true);
2053    }
2054
2055    #[tokio::test]
2056    async fn test_on_request_dispatches_delete_all_expired() {
2057        let plugin = ApiKeyPlugin::builder().build();
2058        let (ctx, _user, session) = create_test_context_with_user().await;
2059
2060        let req = create_auth_request(
2061            HttpMethod::Post,
2062            "/api-key/delete-all-expired-api-keys",
2063            Some(&session.token),
2064            None,
2065            None,
2066        );
2067        let resp = plugin.on_request(&req, &ctx).await.unwrap();
2068        assert!(resp.is_some());
2069        let body = json_body(&resp.unwrap());
2070        assert_eq!(body["deleted"], 0);
2071    }
2072
2073    #[tokio::test]
2074    async fn test_refill_logic() {
2075        // Ensure refillInterval + refillAmount require each other
2076        let plugin = ApiKeyPlugin::builder().build();
2077        let (ctx, _user, session) = create_test_context_with_user().await;
2078
2079        let req = create_auth_request(
2080            HttpMethod::Post,
2081            "/api-key/create",
2082            Some(&session.token),
2083            Some(serde_json::json!({
2084                "name": "refill-missing",
2085                "refillInterval": 60000
2086            })),
2087            None,
2088        );
2089        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2090        assert!(err.to_string().contains("refillAmount"));
2091    }
2092
2093    // =======================================================================
2094    // Comprehensive integration tests (9 scenarios from the test plan)
2095    // =======================================================================
2096
2097    // 1. Virtual session: before_request injects session without DB writes
2098    #[tokio::test]
2099    async fn test_virtual_session_creates_no_db_session() {
2100        let plugin = ApiKeyPlugin::builder()
2101            .enable_session_for_api_keys(true)
2102            .build();
2103        let (ctx, _user, session) = create_test_context_with_user().await;
2104
2105        // Create an API key
2106        let (_id, raw_key) = create_key_and_get_raw(
2107            &plugin,
2108            &ctx,
2109            &session.token,
2110            serde_json::json!({ "name": "virtual-session-test" }),
2111        )
2112        .await;
2113
2114        // Count sessions before
2115        let sessions_before = ctx
2116            .database
2117            .get_user_sessions(&_user.id)
2118            .await
2119            .unwrap()
2120            .len();
2121
2122        // Simulate a request to a protected route with only x-api-key header
2123        let mut headers = HashMap::new();
2124        headers.insert("x-api-key".to_string(), raw_key.clone());
2125        let req = AuthRequest::from_parts(
2126            HttpMethod::Post,
2127            "/update-user".to_string(),
2128            headers,
2129            None,
2130            HashMap::new(),
2131        );
2132
2133        // Call before_request — should return InjectSession
2134        let action = plugin.before_request(&req, &ctx).await.unwrap();
2135        assert!(action.is_some(), "before_request should return an action");
2136        match action.unwrap() {
2137            BeforeRequestAction::InjectSession {
2138                user_id,
2139                session_token: _,
2140            } => {
2141                assert_eq!(user_id, _user.id);
2142            }
2143            BeforeRequestAction::Respond(_) => {
2144                panic!("Expected InjectSession, got Respond");
2145            }
2146        }
2147
2148        // Count sessions after — should be unchanged (no DB writes)
2149        let sessions_after = ctx
2150            .database
2151            .get_user_sessions(&_user.id)
2152            .await
2153            .unwrap()
2154            .len();
2155        assert_eq!(
2156            sessions_before, sessions_after,
2157            "No new sessions should be created in the database"
2158        );
2159    }
2160
2161    // 2. Virtual session on /get-session: synthetic response
2162    #[tokio::test]
2163    async fn test_virtual_session_on_get_session() {
2164        let plugin = ApiKeyPlugin::builder()
2165            .enable_session_for_api_keys(true)
2166            .build();
2167        let (ctx, user, session) = create_test_context_with_user().await;
2168
2169        let (_id, raw_key) = create_key_and_get_raw(
2170            &plugin,
2171            &ctx,
2172            &session.token,
2173            serde_json::json!({ "name": "get-session-test" }),
2174        )
2175        .await;
2176
2177        // Send request to /get-session with x-api-key header
2178        let mut headers = HashMap::new();
2179        headers.insert("x-api-key".to_string(), raw_key.clone());
2180        let req = AuthRequest::from_parts(
2181            HttpMethod::Get,
2182            "/get-session".to_string(),
2183            headers,
2184            None,
2185            HashMap::new(),
2186        );
2187
2188        let action = plugin.before_request(&req, &ctx).await.unwrap();
2189        assert!(action.is_some());
2190        match action.unwrap() {
2191            BeforeRequestAction::Respond(resp) => {
2192                assert_eq!(resp.status, 200);
2193                let body: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
2194                // Should contain user data
2195                assert_eq!(body["user"]["id"], user.id);
2196                assert_eq!(body["user"]["email"], "test@example.com");
2197                // Should contain session-like data
2198                assert!(body["session"]["id"].is_string());
2199                assert_eq!(body["session"]["userId"], user.id);
2200            }
2201            BeforeRequestAction::InjectSession { .. } => {
2202                panic!("Expected Respond for /get-session, got InjectSession");
2203            }
2204        }
2205    }
2206
2207    // 3. Rate limiting: create key with rateLimitMax=2, 3rd call fails
2208    #[tokio::test]
2209    async fn test_rate_limiting_third_call_fails() {
2210        let plugin = ApiKeyPlugin::builder()
2211            .rate_limit(RateLimitDefaults {
2212                enabled: true,
2213                time_window: 60_000,
2214                max_requests: 2,
2215            })
2216            .build();
2217        let (ctx, _user, session) = create_test_context_with_user().await;
2218
2219        let (_id, raw_key) = create_key_and_get_raw(
2220            &plugin,
2221            &ctx,
2222            &session.token,
2223            serde_json::json!({
2224                "name": "rl-integration",
2225                "rateLimitEnabled": true,
2226                "rateLimitTimeWindow": 60000,
2227                "rateLimitMax": 2
2228            }),
2229        )
2230        .await;
2231
2232        let make_verify = |key: &str| {
2233            create_auth_request(
2234                HttpMethod::Post,
2235                "/api-key/verify",
2236                None,
2237                Some(serde_json::json!({ "key": key })),
2238                None,
2239            )
2240        };
2241
2242        // First two pass
2243        let r1 = plugin
2244            .handle_verify(&make_verify(&raw_key), &ctx)
2245            .await
2246            .unwrap();
2247        assert_eq!(json_body(&r1)["valid"], true, "1st request should pass");
2248
2249        let r2 = plugin
2250            .handle_verify(&make_verify(&raw_key), &ctx)
2251            .await
2252            .unwrap();
2253        assert_eq!(json_body(&r2)["valid"], true, "2nd request should pass");
2254
2255        // Third should fail
2256        let r3 = plugin
2257            .handle_verify(&make_verify(&raw_key), &ctx)
2258            .await
2259            .unwrap();
2260        let b3 = json_body(&r3);
2261        assert_eq!(b3["valid"], false, "3rd request should be rate-limited");
2262        assert_eq!(b3["error"]["code"], "RATE_LIMITED");
2263    }
2264
2265    // 4. Remaining consumption: remaining=2, no refill, 3rd fails
2266    #[tokio::test]
2267    async fn test_remaining_consumption_no_refill() {
2268        let plugin = ApiKeyPlugin::builder().build();
2269        let (ctx, _user, session) = create_test_context_with_user().await;
2270
2271        let (_id, raw_key) = create_key_and_get_raw(
2272            &plugin,
2273            &ctx,
2274            &session.token,
2275            serde_json::json!({ "name": "remaining-test", "remaining": 2 }),
2276        )
2277        .await;
2278
2279        let make_verify = |key: &str| {
2280            create_auth_request(
2281                HttpMethod::Post,
2282                "/api-key/verify",
2283                None,
2284                Some(serde_json::json!({ "key": key })),
2285                None,
2286            )
2287        };
2288
2289        // 1st: remaining 2→1
2290        let r1 = plugin
2291            .handle_verify(&make_verify(&raw_key), &ctx)
2292            .await
2293            .unwrap();
2294        assert_eq!(json_body(&r1)["valid"], true);
2295        assert_eq!(json_body(&r1)["key"]["remaining"], 1);
2296
2297        // 2nd: remaining 1→0
2298        let r2 = plugin
2299            .handle_verify(&make_verify(&raw_key), &ctx)
2300            .await
2301            .unwrap();
2302        assert_eq!(json_body(&r2)["valid"], true);
2303        assert_eq!(json_body(&r2)["key"]["remaining"], 0);
2304
2305        // 3rd: usage exceeded
2306        let r3 = plugin
2307            .handle_verify(&make_verify(&raw_key), &ctx)
2308            .await
2309            .unwrap();
2310        assert_eq!(json_body(&r3)["valid"], false);
2311        assert_eq!(json_body(&r3)["error"]["code"], "USAGE_EXCEEDED");
2312    }
2313
2314    // 5. Refill logic: remaining=1, refillInterval=100ms, refillAmount=10,
2315    //    verify once → remaining=0, wait 150ms, verify → refill to 10 then
2316    //    decrement to 9.
2317    #[tokio::test]
2318    async fn test_refill_resets_remaining_after_interval() {
2319        let plugin = ApiKeyPlugin::builder().build();
2320        let (ctx, _user, session) = create_test_context_with_user().await;
2321
2322        // Use a very short refill interval for testing (100 ms)
2323        let (_id, raw_key) = create_key_and_get_raw(
2324            &plugin,
2325            &ctx,
2326            &session.token,
2327            serde_json::json!({
2328                "name": "refill-test",
2329                "remaining": 1,
2330                "refillInterval": 100,
2331                "refillAmount": 10
2332            }),
2333        )
2334        .await;
2335
2336        let make_verify = |key: &str| {
2337            create_auth_request(
2338                HttpMethod::Post,
2339                "/api-key/verify",
2340                None,
2341                Some(serde_json::json!({ "key": key })),
2342                None,
2343            )
2344        };
2345
2346        // First verify: remaining 1→0
2347        let r1 = plugin
2348            .handle_verify(&make_verify(&raw_key), &ctx)
2349            .await
2350            .unwrap();
2351        assert_eq!(json_body(&r1)["valid"], true);
2352        assert_eq!(json_body(&r1)["key"]["remaining"], 0);
2353
2354        // Wait for refill interval to elapse
2355        tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2356
2357        // Second verify: should refill to 10 and then decrement → 9
2358        let r2 = plugin
2359            .handle_verify(&make_verify(&raw_key), &ctx)
2360            .await
2361            .unwrap();
2362        let b2 = json_body(&r2);
2363        assert_eq!(b2["valid"], true, "Should succeed after refill");
2364        assert_eq!(b2["key"]["remaining"], 9, "Should be refillAmount - 1 = 9");
2365    }
2366
2367    // 6. Permissions: key with {"admin": ["read"]}, verify with
2368    //    {"admin": ["write"]} should fail
2369    #[tokio::test]
2370    async fn test_permissions_mismatch_fails() {
2371        let plugin = ApiKeyPlugin::builder().build();
2372        let (ctx, _user, session) = create_test_context_with_user().await;
2373
2374        let (_id, raw_key) = create_key_and_get_raw(
2375            &plugin,
2376            &ctx,
2377            &session.token,
2378            serde_json::json!({
2379                "name": "perm-mismatch",
2380                "permissions": { "admin": ["read"] }
2381            }),
2382        )
2383        .await;
2384
2385        // Verify with matching permission → pass
2386        let verify_ok = create_auth_request(
2387            HttpMethod::Post,
2388            "/api-key/verify",
2389            None,
2390            Some(serde_json::json!({
2391                "key": raw_key,
2392                "permissions": { "admin": ["read"] }
2393            })),
2394            None,
2395        );
2396        let r1 = plugin.handle_verify(&verify_ok, &ctx).await.unwrap();
2397        assert_eq!(json_body(&r1)["valid"], true);
2398
2399        // Verify with mismatched permission → fail
2400        let verify_fail = create_auth_request(
2401            HttpMethod::Post,
2402            "/api-key/verify",
2403            None,
2404            Some(serde_json::json!({
2405                "key": raw_key,
2406                "permissions": { "admin": ["write"] }
2407            })),
2408            None,
2409        );
2410        let r2 = plugin.handle_verify(&verify_fail, &ctx).await.unwrap();
2411        assert_eq!(json_body(&r2)["valid"], false);
2412    }
2413
2414    // 7. Concurrent rate limiting: send 5 sequential verify requests with
2415    //    rateLimitMax=2, only first 2 succeed (sequential proves logic is
2416    //    correct; true concurrency race conditions are documented above).
2417    #[tokio::test]
2418    async fn test_concurrent_rate_limiting() {
2419        let plugin = ApiKeyPlugin::builder()
2420            .rate_limit(RateLimitDefaults {
2421                enabled: true,
2422                time_window: 60_000,
2423                max_requests: 2,
2424            })
2425            .build();
2426        let (ctx, _user, session) = create_test_context_with_user().await;
2427
2428        let (_id, raw_key) = create_key_and_get_raw(
2429            &plugin,
2430            &ctx,
2431            &session.token,
2432            serde_json::json!({
2433                "name": "concurrent-rl",
2434                "rateLimitEnabled": true,
2435                "rateLimitTimeWindow": 60000,
2436                "rateLimitMax": 2
2437            }),
2438        )
2439        .await;
2440
2441        let make_verify = |key: &str| {
2442            create_auth_request(
2443                HttpMethod::Post,
2444                "/api-key/verify",
2445                None,
2446                Some(serde_json::json!({ "key": key })),
2447                None,
2448            )
2449        };
2450
2451        let mut success_count = 0;
2452        let mut fail_count = 0;
2453
2454        for _ in 0..5 {
2455            let resp = plugin
2456                .handle_verify(&make_verify(&raw_key), &ctx)
2457                .await
2458                .unwrap();
2459            let body = json_body(&resp);
2460            if body["valid"] == true {
2461                success_count += 1;
2462            } else {
2463                fail_count += 1;
2464                assert_eq!(body["error"]["code"], "RATE_LIMITED");
2465            }
2466        }
2467
2468        assert_eq!(success_count, 2, "Only 2 out of 5 should succeed");
2469        assert_eq!(fail_count, 3, "3 out of 5 should be rate-limited");
2470    }
2471
2472    // 8. Database compatibility: test delete_expired_api_keys on memory
2473    //    adapter (the SQL fix is in the SqlxAdapter; memory adapter tests
2474    //    prove the trait contract works).
2475    #[tokio::test]
2476    async fn test_delete_expired_api_keys_memory_adapter() {
2477        let (ctx, _user, session) = create_test_context_with_user().await;
2478        let plugin = ApiKeyPlugin::builder().build();
2479
2480        // Create two keys
2481        let (id1, _) = create_key_and_get_raw(
2482            &plugin,
2483            &ctx,
2484            &session.token,
2485            serde_json::json!({ "name": "will-expire" }),
2486        )
2487        .await;
2488        let (_id2, _) = create_key_and_get_raw(
2489            &plugin,
2490            &ctx,
2491            &session.token,
2492            serde_json::json!({ "name": "wont-expire" }),
2493        )
2494        .await;
2495
2496        // Expire the first key by setting expires_at to the past
2497        let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2498        ctx.database
2499            .update_api_key(
2500                &id1,
2501                UpdateApiKey {
2502                    expires_at: Some(Some(past)),
2503                    ..Default::default()
2504                },
2505            )
2506            .await
2507            .unwrap();
2508
2509        // Delete expired keys
2510        let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2511        assert_eq!(deleted, 1, "Should delete exactly 1 expired key");
2512
2513        // Verify only the non-expired key remains
2514        let remaining = ctx.database.list_api_keys_by_user(&_user.id).await.unwrap();
2515        assert_eq!(remaining.len(), 1);
2516    }
2517
2518    // 9. Delete expired with auth: unauthenticated call should fail
2519    #[tokio::test]
2520    async fn test_delete_expired_without_auth_returns_error() {
2521        let plugin = ApiKeyPlugin::builder().build();
2522        let (ctx, _user, _session) = create_test_context_with_user().await;
2523
2524        // Call without auth token
2525        let req = create_auth_request(
2526            HttpMethod::Post,
2527            "/api-key/delete-all-expired-api-keys",
2528            None, // no auth token
2529            None,
2530            None,
2531        );
2532        let result = plugin.handle_delete_all_expired(&req, &ctx).await;
2533        assert!(
2534            result.is_err(),
2535            "Should return error when called without authentication"
2536        );
2537    }
2538
2539    // 10. before_request returns None when enableSessionForAPIKeys is false
2540    #[tokio::test]
2541    async fn test_before_request_disabled_returns_none() {
2542        let plugin = ApiKeyPlugin::builder().build(); // enable_session_for_api_keys defaults to false
2543        let (ctx, _user, session) = create_test_context_with_user().await;
2544
2545        let (_id, raw_key) = create_key_and_get_raw(
2546            &plugin,
2547            &ctx,
2548            &session.token,
2549            serde_json::json!({ "name": "disabled-session" }),
2550        )
2551        .await;
2552
2553        let mut headers = HashMap::new();
2554        headers.insert("x-api-key".to_string(), raw_key);
2555        let req = AuthRequest::from_parts(
2556            HttpMethod::Get,
2557            "/get-session".to_string(),
2558            headers,
2559            None,
2560            HashMap::new(),
2561        );
2562
2563        let action = plugin.before_request(&req, &ctx).await.unwrap();
2564        assert!(
2565            action.is_none(),
2566            "before_request should return None when session emulation is disabled"
2567        );
2568    }
2569}