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#[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 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
140struct 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
156pub struct ApiKeyPlugin {
162 config: ApiKeyConfig,
163 last_expired_check: Mutex<Option<std::time::Instant>>,
165 rate_limiters: Mutex<HashMap<String, std::sync::Arc<GovernorLimiter>>>,
168}
169
170type GovernorLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>;
172
173#[derive(Debug, Clone)]
175pub struct ApiKeyConfig {
176 pub key_length: usize,
178 pub prefix: Option<String>,
179 pub default_remaining: Option<i64>,
180
181 pub api_key_header: String,
183
184 pub disable_key_hashing: bool,
186
187 pub starting_characters_length: usize,
189 pub store_starting_characters: bool,
190
191 pub max_prefix_length: usize,
193 pub min_prefix_length: usize,
194
195 pub max_name_length: usize,
197 pub min_name_length: usize,
198 pub require_name: bool,
199
200 pub enable_metadata: bool,
202
203 pub key_expiration: KeyExpirationConfig,
205
206 pub rate_limit: RateLimitDefaults,
208
209 pub enable_session_for_api_keys: bool,
211}
212
213#[derive(Debug, Clone)]
215pub struct KeyExpirationConfig {
216 pub default_expires_in: Option<i64>,
218 pub disable_custom_expires_time: bool,
220 pub max_expires_in: i64,
222 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#[derive(Debug, Clone)]
239pub struct RateLimitDefaults {
240 pub enabled: bool,
241 pub time_window: i64,
243 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, 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#[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#[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
428fn 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 let allowed_actions = match key_map.get(resource) {
468 Some(a) => a,
469 None => return false,
471 };
472
473 if let Some(actions_array) = requested_actions.as_array() {
477 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 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 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 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 return false;
523 }
524 }
525
526 true
527}
528
529#[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 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 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 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 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 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 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 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 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 let _existing = helpers::get_owned_api_key(ctx, &update_req.id, user.id()).await?;
842
843 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 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 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 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 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 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 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 let hashed = if self.config.disable_key_hashing {
979 raw_key.to_string()
980 } else {
981 Self::hash_key(raw_key)
982 };
983
984 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 if !api_key.enabled() {
994 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyDisabled));
995 }
996
997 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 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 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 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 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 self.check_rate_limit_governor(&api_key)?;
1069
1070 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 self.maybe_delete_expired(ctx).await;
1087
1088 Ok(ApiKeyView::from_entity(&updated))
1089 }
1090
1091 fn check_rate_limit_governor(
1097 &self,
1098 api_key: &impl AuthApiKey,
1099 ) -> Result<(), ApiKeyValidationError> {
1100 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 if key_has_explicit_setting {
1118 return Ok(());
1119 }
1120 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 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 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 async fn handle_delete_all_expired<DB: DatabaseAdapter>(
1173 &self,
1174 req: &AuthRequest,
1175 ctx: &AuthContext<DB>,
1176 ) -> AuthResult<AuthResponse> {
1177 let (_user, _session) = ctx.require_session(req).await?;
1179 let count = ctx.database.delete_expired_api_keys().await?;
1180
1181 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#[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 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 if req.path().starts_with("/api-key/") {
1252 return Ok(None);
1253 }
1254
1255 let view = self
1257 .validate_api_key(ctx, &raw_key, None)
1258 .await
1259 .map_err(|e| AuthError::bad_request(e.message))?;
1260
1261 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 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 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 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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); 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 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 #[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 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 let sessions_before = ctx
2116 .database
2117 .get_user_sessions(&_user.id)
2118 .await
2119 .unwrap()
2120 .len();
2121
2122 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 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 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 #[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 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 assert_eq!(body["user"]["id"], user.id);
2196 assert_eq!(body["user"]["email"], "test@example.com");
2197 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 #[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 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 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 #[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 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 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 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 #[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 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 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 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2356
2357 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 #[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 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 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 #[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 #[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 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 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 let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2511 assert_eq!(deleted, 1, "Should delete exactly 1 expired key");
2512
2513 let remaining = ctx.database.list_api_keys_by_user(&_user.id).await.unwrap();
2515 assert_eq!(remaining.len(), 1);
2516 }
2517
2518 #[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 let req = create_auth_request(
2526 HttpMethod::Post,
2527 "/api-key/delete-all-expired-api-keys",
2528 None, 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 #[tokio::test]
2541 async fn test_before_request_disabled_returns_none() {
2542 let plugin = ApiKeyPlugin::builder().build(); 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}