1mod cleanup;
2mod config;
3
4pub use config::ApiKeyConfig;
5pub use config::ApiKeyReferences;
6pub use config::KeyExpirationConfig;
7pub use config::RateLimitDefaults;
8mod callbacks;
9mod secondary_usage;
10mod storage;
11pub use storage::{ApiKeyStorage, ApiKeyStorageMode};
12mod endpoint;
13pub use endpoint::{ApiKeyVerificationInput, ApiKeyVerificationOutput};
14
15pub(super) mod handlers;
16
17pub(super) mod types;
18
19mod verification;
20
21use alibi_core::entity::AuthUser;
22use alibi_core::{AuthContext, AuthError, AuthResult, BeforeRequestAction};
23use alibi_core::{AuthRequest, AuthResponse};
24use base64::Engine;
25use base64::engine::general_purpose::URL_SAFE_NO_PAD;
26pub use callbacks::{
27 ApiKeyCallbackContext, ApiKeyDefaultPermissions, ApiKeyGenerationOptions, ApiKeyGenerator,
28 ApiKeyGetter, ApiKeyPermissions, ApiKeyValidator,
29};
30use handlers::{create_key_core, delete_key_core, get_key_core, list_keys_core, update_key_core};
31use rand::seq::IndexedRandom;
32use sha2::{Digest, Sha256};
33use std::sync::{Arc, Mutex};
34pub use types::{
35 CreateKeyRequest, CreateKeyResponse, DeleteExpiredApiKeysResponse, UpdateKeyRequest,
36};
37use types::{DeleteKeyRequest, ListKeysQuery, parse_api_key_body};
38pub use verification::{
39 ApiKeyErrorDetails, ApiKeyErrorMessage, ApiKeyValidationError, ApiKeyVerificationError,
40 VerifyApiKey,
41};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ApiKeyErrorCode {
50 InvalidApiKey,
51 KeyDisabled,
52 KeyExpired,
53 UsageExceeded,
54 KeyNotFound,
55 RateLimited,
56 UnauthorizedSession,
57 InvalidPrefixLength,
58 InvalidNameLength,
59 MetadataDisabled,
60 NoValuesToUpdate,
61 KeyDisabledExpiration,
62 ExpiresInTooSmall,
63 ExpiresInTooLarge,
64 InvalidRemaining,
65 RefillAmountAndIntervalRequired,
66 RefillIntervalAndAmountRequired,
67 NameRequired,
68 InvalidUserIdFromApiKey,
69 InvalidReferenceIdFromApiKey,
70 NoDefaultConfiguration,
71 OrganizationIdRequired,
72 OrganizationPluginRequired,
73 UserNotMemberOfOrganization,
74 InsufficientApiKeyPermissions,
75 ServerOnlyProperty,
76 FailedToUpdateApiKey,
77 InvalidMetadataType,
78}
79
80impl ApiKeyErrorCode {
81 #[must_use]
82 pub const fn as_str(self) -> &'static str {
83 match self {
84 Self::InvalidApiKey => "INVALID_API_KEY",
85 Self::KeyDisabled => "KEY_DISABLED",
86 Self::KeyExpired => "KEY_EXPIRED",
87 Self::UsageExceeded => "USAGE_EXCEEDED",
88 Self::KeyNotFound => "KEY_NOT_FOUND",
89 Self::RateLimited => "RATE_LIMITED",
90 Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
91 Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
92 Self::InvalidNameLength => "INVALID_NAME_LENGTH",
93 Self::MetadataDisabled => "METADATA_DISABLED",
94 Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
95 Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
96 Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
97 Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
98 Self::InvalidRemaining => "INVALID_REMAINING",
99 Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
100 Self::RefillIntervalAndAmountRequired => "REFILL_INTERVAL_AND_AMOUNT_REQUIRED",
101 Self::NameRequired => "NAME_REQUIRED",
102 Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
103 Self::InvalidReferenceIdFromApiKey => "INVALID_REFERENCE_ID_FROM_API_KEY",
104 Self::NoDefaultConfiguration => "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND",
105 Self::OrganizationIdRequired => "ORGANIZATION_ID_REQUIRED",
106 Self::OrganizationPluginRequired => "ORGANIZATION_PLUGIN_REQUIRED",
107 Self::UserNotMemberOfOrganization => "USER_NOT_MEMBER_OF_ORGANIZATION",
108 Self::InsufficientApiKeyPermissions => "INSUFFICIENT_API_KEY_PERMISSIONS",
109 Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
110 Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
111 Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
112 }
113 }
114
115 #[must_use]
116 pub const fn message(self) -> &'static str {
117 match self {
118 Self::InvalidApiKey => "Invalid API key.",
119 Self::KeyDisabled => "API Key is disabled",
120 Self::KeyExpired => "API Key has expired",
121 Self::UsageExceeded => "API Key has reached its usage limit",
122 Self::KeyNotFound => "API Key not found",
123 Self::RateLimited => "Rate limit exceeded.",
124 Self::UnauthorizedSession => "Unauthorized or invalid session",
125 Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
126 Self::InvalidNameLength => "The name length is either too large or too small.",
127 Self::MetadataDisabled => "Metadata is disabled.",
128 Self::NoValuesToUpdate => "No values to update.",
129 Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
130 Self::ExpiresInTooSmall => {
131 "The expiresIn is smaller than the predefined minimum value."
132 }
133 Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
134 Self::InvalidRemaining => "The remaining count is either too large or too small.",
135 Self::RefillAmountAndIntervalRequired => {
136 "refillAmount is required when refillInterval is provided"
137 }
138 Self::RefillIntervalAndAmountRequired => {
139 "refillInterval is required when refillAmount is provided"
140 }
141 Self::NameRequired => "API Key name is required.",
142 Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
143 Self::InvalidReferenceIdFromApiKey => "The reference id from the API key is invalid.",
144 Self::NoDefaultConfiguration => "No default api-key configuration found.",
145 Self::OrganizationIdRequired => {
146 "Organization ID is required for organization-owned API keys."
147 }
148 Self::OrganizationPluginRequired => {
149 "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
150 }
151 Self::UserNotMemberOfOrganization => {
152 "You are not a member of the organization that owns this API key."
153 }
154 Self::InsufficientApiKeyPermissions => {
155 "You do not have permission to perform this action on organization API keys."
156 }
157 Self::ServerOnlyProperty => {
158 "The property you're trying to set can only be set from the server auth instance only."
159 }
160 Self::FailedToUpdateApiKey => "Failed to update API key",
161 Self::InvalidMetadataType => "metadata must be an object or undefined",
162 }
163 }
164}
165
166impl serde::Serialize for ApiKeyErrorCode {
167 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
168 serializer.serialize_str(self.as_str())
169 }
170}
171
172static LAST_EXPIRED_CHECK: Mutex<Option<i64>> = Mutex::new(None);
174
175#[derive(Clone)]
177pub struct ApiKeyPlugin {
178 pub(super) configurations: Vec<ApiKeyConfig>,
180}
181
182impl ApiKeyPlugin {
183 #[must_use]
188 pub fn configuration(mut self, config: ApiKeyConfig) -> Self {
189 self.configurations.push(config.normalized());
190 self
191 }
192
193 pub(super) fn resolve_configuration(
200 &self,
201 config_id: Option<&str>,
202 ) -> AuthResult<&ApiKeyConfig> {
203 if let Some(config_id) = config_id
204 && let Some(found) = self
205 .configurations
206 .iter()
207 .find(|config| config.config_id == config_id)
208 {
209 return Ok(found);
210 }
211
212 self.configurations
213 .iter()
214 .find(|config| is_default_config_id(&config.config_id))
215 .ok_or_else(|| api_key_error(ApiKeyErrorCode::NoDefaultConfiguration))
216 }
217}
218
219#[bon::bon]
236impl ApiKeyPlugin {
237 #[builder]
238 #[must_use]
239 pub fn new(
240 #[builder(default = "default".to_owned())] config_id: String,
241 #[builder(default)] references: ApiKeyReferences,
242 #[builder(default)] storage: ApiKeyStorageMode,
243 #[builder(default)] fallback_to_database: bool,
244 custom_storage: Option<Arc<dyn ApiKeyStorage>>,
245 secondary_storage: Option<Arc<dyn alibi_core::store::CacheAdapter>>,
246 #[builder(default = 64.0, into)] key_length: f64,
247 prefix: Option<String>,
248 default_permissions: Option<ApiKeyPermissions>,
249 custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
250 default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
251 #[builder(default = vec!["x-api-key".to_owned()])] api_key_headers: Vec<String>,
252 custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
253 custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
254 #[builder(default = false)] disable_key_hashing: bool,
255 #[builder(default = 6.0, into)] starting_characters_length: f64,
256 #[builder(default = true)] store_starting_characters: bool,
257 #[builder(default = 32.0, into)] max_prefix_length: f64,
258 #[builder(default = 1.0, into)] min_prefix_length: f64,
259 #[builder(default = 32.0, into)] max_name_length: f64,
260 #[builder(default = 1.0, into)] min_name_length: f64,
261 #[builder(default = false)] require_name: bool,
262 #[builder(default = false)] enable_metadata: bool,
263 #[builder(default)] key_expiration: KeyExpirationConfig,
264 #[builder(default)] rate_limit: RateLimitDefaults,
265 #[builder(default = false)] enable_session_for_api_keys: bool,
266 #[builder(default = false)] defer_updates: bool,
267 ) -> Self {
268 Self {
269 configurations: vec![
270 ApiKeyConfig {
271 config_id,
272 references,
273 storage,
274 fallback_to_database,
275 custom_storage,
276 secondary_storage,
277 key_length,
278 prefix,
279 default_permissions,
280 custom_key_generator,
281 default_permissions_callback,
282 api_key_headers,
283 custom_api_key_getter,
284 custom_api_key_validator,
285 disable_key_hashing,
286 starting_characters_length,
287 store_starting_characters,
288 max_prefix_length,
289 min_prefix_length,
290 max_name_length,
291 min_name_length,
292 require_name,
293 enable_metadata,
294 key_expiration,
295 rate_limit,
296 enable_session_for_api_keys,
297 defer_updates,
298 }
299 .normalized(),
300 ],
301 }
302 }
303
304 #[must_use]
305 pub fn with_config(config: ApiKeyConfig) -> Self {
306 Self {
307 configurations: vec![config.normalized()],
308 }
309 }
310
311 pub(super) fn generate_key(
314 config: &ApiKeyConfig,
315 custom_prefix: Option<&str>,
316 ) -> AuthResult<(String, String, alibi_core::ApiKeyStartingCharacters)> {
317 if config.key_length <= 0.0 {
318 return Err(AuthError::internal("Length must be a positive integer."));
319 }
320 if !config.key_length.is_finite() || config.key_length < 0.5 {
323 return Err(AuthError::internal("Unsupported random key length"));
324 }
325 let capacity = config
326 .key_length
327 .ceil()
328 .to_string()
329 .parse::<u32>()
330 .map_err(|error| AuthError::internal(error.to_string()))?;
331 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
333 let mut rng = rand::rng();
334 let mut raw = String::new();
335 raw.try_reserve_exact(
336 usize::try_from(capacity).map_err(|error| AuthError::internal(error.to_string()))?,
337 )
338 .map_err(|error| AuthError::internal(error.to_string()))?;
339 while f64::from(
340 u32::try_from(raw.len()).map_err(|error| AuthError::internal(error.to_string()))?,
341 ) < config.key_length
342 {
343 raw.push(ALPHABET.choose(&mut rng).copied().map_or('a', char::from));
344 }
345
346 let prefix = custom_prefix
347 .filter(|prefix| !prefix.is_empty())
348 .or(config.prefix.as_deref())
349 .unwrap_or("");
350 let full_key = format!("{prefix}{raw}");
351
352 let start = Self::starting_characters(&full_key, config.starting_characters_length);
355
356 let hash = if config.disable_key_hashing {
357 full_key.clone()
358 } else {
359 Self::hash_key(&full_key)
360 };
361
362 Ok((full_key, hash, start))
363 }
364
365 pub(super) fn starting_characters(
366 key: &str,
367 length: f64,
368 ) -> alibi_core::ApiKeyStartingCharacters {
369 let end = if length.is_nan() || length < 0.0 {
372 0.0
373 } else {
374 length.trunc()
375 };
376 let units = key
377 .encode_utf16()
378 .scan(0_u32, |position, unit| {
379 let keep = f64::from(*position) < end;
380 *position = position.saturating_add(1);
381 keep.then_some(unit)
382 })
383 .collect();
384 alibi_core::ApiKeyStartingCharacters::from_utf16(units)
385 }
386
387 pub(super) fn hash_key(key: &str) -> String {
388 let mut hasher = Sha256::new();
389 hasher.update(key.as_bytes());
390 let digest = hasher.finalize();
391 URL_SAFE_NO_PAD.encode(digest)
392 }
393
394 pub(super) fn validate_prefix(config: &ApiKeyConfig, prefix: Option<&str>) -> AuthResult<()> {
400 if let Some(p) = prefix.filter(|prefix| !prefix.is_empty()) {
401 let len = f64::from(
402 u32::try_from(p.encode_utf16().count())
403 .map_err(|error| AuthError::internal(error.to_string()))?,
404 );
405 if len < config.min_prefix_length || len > config.max_prefix_length {
406 return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
407 }
408 }
409 Ok(())
410 }
411
412 pub(super) fn validate_name(
421 config: &ApiKeyConfig,
422 name: Option<&str>,
423 is_create: bool,
424 ) -> AuthResult<()> {
425 if is_create && config.require_name && name.is_none_or(str::is_empty) {
426 return Err(api_key_error(ApiKeyErrorCode::NameRequired));
427 }
428 if let Some(n) = name.filter(|name| !is_create || !name.is_empty()) {
429 let len = f64::from(
430 u32::try_from(n.encode_utf16().count())
431 .map_err(|error| AuthError::internal(error.to_string()))?,
432 );
433 if len < config.min_name_length || len > config.max_name_length {
434 return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
435 }
436 }
437 Ok(())
438 }
439
440 pub(super) fn validate_expires_in(
444 config: &ApiKeyConfig,
445 expires_in: Option<f64>,
446 ) -> AuthResult<Option<f64>> {
447 let cfg = &config.key_expiration;
448 if let Some(secs) = expires_in {
449 if cfg.disable_custom_expires_time {
450 return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
451 }
452 let days = secs / 86_400.0;
454 if days < cfg.min_expires_in {
455 return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
456 }
457 if days > cfg.max_expires_in {
458 return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
459 }
460 Ok(Some(secs))
461 } else {
462 Ok(cfg.default_expires_in)
463 }
464 }
465
466 pub(super) fn validate_metadata(
470 config: &ApiKeyConfig,
471 metadata: Option<&alibi_core::utils::json::JsValue>,
472 ) -> AuthResult<()> {
473 if let Some(value) = metadata.filter(|value| match value {
474 alibi_core::utils::json::JsValue::Null => false,
475 alibi_core::utils::json::JsValue::Bool(value) => *value,
476 alibi_core::utils::json::JsValue::Number(value) => *value != 0.0 && !value.is_nan(),
477 alibi_core::utils::json::JsValue::String(value) => !value.is_empty(),
478 alibi_core::utils::json::JsValue::Array(_)
479 | alibi_core::utils::json::JsValue::Object(_) => true,
480 }) {
481 if !config.enable_metadata {
482 return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
483 }
484 if !value.is_object() && !value.is_array() {
485 return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
486 }
487 }
488 Ok(())
489 }
490
491 pub(super) fn validate_refill(
495 refill_interval: Option<f64>,
496 refill_amount: Option<f64>,
497 ) -> AuthResult<()> {
498 match (
499 refill_interval.filter(|value| *value != 0.0),
500 refill_amount.filter(|value| *value != 0.0),
501 ) {
502 (None, Some(_)) => Err(api_key_error(
503 ApiKeyErrorCode::RefillAmountAndIntervalRequired,
504 )),
505 (Some(_), None) => Err(api_key_error(
506 ApiKeyErrorCode::RefillIntervalAndAmountRequired,
507 )),
508 _ => Ok(()),
509 }
510 }
511
512 async fn handle_create(
517 &self,
518 req: &AuthRequest,
519 ctx: &AuthContext<impl alibi_core::AuthSchema>,
520 ) -> AuthResult<AuthResponse> {
521 let mut resolution = req.clone();
522 drop(
523 resolution
524 .query
525 .insert("disableCookieCache".into(), "true".into()),
526 );
527 let (user, _session) = ctx
528 .require_cached_session(&resolution)
529 .await
530 .map_err(|error| match error {
531 AuthError::Unauthenticated => api_key_error(ApiKeyErrorCode::UnauthorizedSession),
532 error @ (AuthError::Api { .. }
533 | AuthError::Upstream { .. }
534 | AuthError::BadRequest(_)
535 | AuthError::InvalidRequest(_)
536 | AuthError::Validation(_)
537 | AuthError::InvalidCredentials
538 | AuthError::AuthenticationFailed(_)
539 | AuthError::SessionNotFound
540 | AuthError::Forbidden(_)
541 | AuthError::UserCreationCancelled
542 | AuthError::SessionCreationCancelled
543 | AuthError::BannedUser(_)
544 | AuthError::Unauthorized
545 | AuthError::UserNotFound
546 | AuthError::NotFound(_)
547 | AuthError::Conflict(_)
548 | AuthError::MethodNotAllowed(_)
549 | AuthError::PayloadTooLarge(_)
550 | AuthError::UnprocessableEntity(_)
551 | AuthError::RateLimited { .. }
552 | AuthError::NotImplemented(_)
553 | AuthError::Config(_)
554 | AuthError::Database(_)
555 | AuthError::Serialization(_)
556 | AuthError::Plugin { .. }
557 | AuthError::CallbackFailure(_)
558 | AuthError::Internal(_)
559 | AuthError::Encryption(_)
560 | AuthError::PasswordHash(_)
561 | AuthError::Jwt(_)) => error,
562 })?;
563 let body: CreateKeyRequest = match parse_api_key_body(req) {
564 Ok(v) => v,
565 Err(resp) => return Ok(resp),
566 };
567 let response = match create_key_core(&body, user.id(), self, ctx, Some(req)).await {
568 Ok(response) => response,
569 Err(error)
570 if error.status_code() >= 500
571 && !matches!(error, AuthError::Upstream { .. } | AuthError::Api { .. }) =>
572 {
573 return Ok(AuthResponse::new(500));
574 }
575 Err(error) => return Err(error),
576 };
577 Ok(AuthResponse::json(200, &response)?)
578 }
579
580 async fn handle_get(
581 &self,
582 req: &AuthRequest,
583 ctx: &AuthContext<impl alibi_core::AuthSchema>,
584 ) -> AuthResult<AuthResponse> {
585 let mut resolution = req.clone();
586 drop(resolution.query.remove("disableCookieCache"));
587 drop(resolution.query.remove("disableRefresh"));
588 let (user, _session) = super::helpers::ordinary_session(&resolution, ctx).await?;
589 let id = req
590 .query
591 .get("id")
592 .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
593 let config_id = req.query.get("configId").map(String::as_str);
594 key_json_response(get_key_core(id, config_id, user.id(), self, ctx).await)
595 }
596
597 async fn handle_list(
598 &self,
599 req: &AuthRequest,
600 ctx: &AuthContext<impl alibi_core::AuthSchema>,
601 ) -> AuthResult<AuthResponse> {
602 let mut resolution = req.clone();
603 drop(resolution.query.remove("disableCookieCache"));
604 drop(resolution.query.remove("disableRefresh"));
605 let (user, _session) = super::helpers::ordinary_session(&resolution, ctx).await?;
606 let query = match ListKeysQuery::from_request(req) {
607 Ok(query) => query,
608 Err(response) => return Ok(response),
609 };
610 key_json_response(list_keys_core(user.id(), &query, self, ctx).await)
611 }
612
613 async fn handle_update(
614 &self,
615 req: &AuthRequest,
616 ctx: &AuthContext<impl alibi_core::AuthSchema>,
617 ) -> AuthResult<AuthResponse> {
618 let mut resolution = req.clone();
619 drop(
620 resolution
621 .query
622 .insert("disableCookieCache".into(), "true".into()),
623 );
624 let (user, _session) = ctx
625 .require_cached_session(&resolution)
626 .await
627 .map_err(|error| match error {
628 AuthError::Unauthenticated => api_key_error(ApiKeyErrorCode::UnauthorizedSession),
629 error @ (AuthError::Api { .. }
630 | AuthError::Upstream { .. }
631 | AuthError::BadRequest(_)
632 | AuthError::InvalidRequest(_)
633 | AuthError::Validation(_)
634 | AuthError::InvalidCredentials
635 | AuthError::AuthenticationFailed(_)
636 | AuthError::SessionNotFound
637 | AuthError::Forbidden(_)
638 | AuthError::UserCreationCancelled
639 | AuthError::SessionCreationCancelled
640 | AuthError::BannedUser(_)
641 | AuthError::Unauthorized
642 | AuthError::UserNotFound
643 | AuthError::NotFound(_)
644 | AuthError::Conflict(_)
645 | AuthError::MethodNotAllowed(_)
646 | AuthError::PayloadTooLarge(_)
647 | AuthError::UnprocessableEntity(_)
648 | AuthError::RateLimited { .. }
649 | AuthError::NotImplemented(_)
650 | AuthError::Config(_)
651 | AuthError::Database(_)
652 | AuthError::Serialization(_)
653 | AuthError::Plugin { .. }
654 | AuthError::CallbackFailure(_)
655 | AuthError::Internal(_)
656 | AuthError::Encryption(_)
657 | AuthError::PasswordHash(_)
658 | AuthError::Jwt(_)) => error,
659 })?;
660 let body: UpdateKeyRequest = match parse_api_key_body(req) {
661 Ok(v) => v,
662 Err(resp) => return Ok(resp),
663 };
664 let response = update_key_core(&body, user.id(), self, ctx).await?;
665 Ok(AuthResponse::json(200, &response)?)
666 }
667
668 async fn handle_delete(
669 &self,
670 req: &AuthRequest,
671 ctx: &AuthContext<impl alibi_core::AuthSchema>,
672 ) -> AuthResult<AuthResponse> {
673 let (user, _session) = super::helpers::ordinary_session(req, ctx).await?;
674 if user.banned() {
675 return Err(AuthError::authentication_failed("User is banned"));
676 }
677 let body: DeleteKeyRequest = match parse_api_key_body(req) {
678 Ok(v) => v,
679 Err(resp) => return Ok(resp),
680 };
681 let response = delete_key_core(&body, user.id(), self, ctx).await?;
682 Ok(AuthResponse::json(200, &response)?)
683 }
684}
685
686fn key_json_response<T: serde::Serialize>(result: AuthResult<T>) -> AuthResult<AuthResponse> {
687 match result {
688 Ok(response) => Ok(AuthResponse::json(200, &response)?),
689 Err(error)
690 if error.status_code() >= 500
691 && !matches!(error, AuthError::Upstream { .. } | AuthError::Api { .. }) =>
692 {
693 Ok(AuthResponse::new(500))
694 }
695 Err(error) => Err(error),
696 }
697}
698
699alibi_core::impl_auth_plugin! {
704 ApiKeyPlugin, "api-key";
705 routes {
706 post "/api-key/create" => handle_create, "api_key_create";
707 get "/api-key/get" => handle_get, "api_key_get";
708 post "/api-key/update" => handle_update, "api_key_update";
709 post "/api-key/delete" => handle_delete, "api_key_delete";
710 get "/api-key/list" => handle_list, "api_key_list";
711 }
712 extra {
713 fn static_openapi_metadata(&self) -> alibi_core::PluginOpenApiMetadata {
714 crate::metadata::plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self))
715 }
716
717 fn openapi_metadata(&self,ctx:&alibi_core::AuthInitContext<S>)->alibi_core::PluginOpenApiMetadata {
718 let mut metadata=crate::metadata::instance_plugin_metadata("api-key",&<Self as alibi_core::AuthPlugin<S>>::routes(self),ctx);
719 let defaults=self.configurations.first().filter(|_|self.configurations.len()==1).map(|config|config.rate_limit.clone()).unwrap_or_default();
720 for model in &mut metadata.models {
721 if model.name!="Apikey" {continue;}
722 for field in &mut model.fields {
723 let value=match field.name.as_str() {"rateLimitTimeWindow"=>defaults.time_window,"rateLimitMax"=>defaults.max_requests,_=>continue};
724 if let Some(schema)=field.schema.as_object_mut() {drop(schema.insert("default".into(),serde_json::json!(value)));}
725 }
726 }
727 metadata
728 }
729 async fn on_init(&self, _ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
730 if self.configurations.len() > 1 {
731 let mut ids = std::collections::HashSet::new();
732 for config in &self.configurations {
733 if config.config_id.is_empty() {
734 return Err(AuthError::config("configId is required for each API key configuration in the api-key plugin."));
735 }
736 if !ids.insert(&config.config_id) {
737 return Err(AuthError::config("configId must be unique for each API key configuration in the api-key plugin."));
738 }
739 }
740 }
741 Ok(())
742 }
743
744 fn server_endpoints(&self) -> Vec<alibi_core::endpoint::EndpointDefinition> { endpoint::definitions() }
745
746 fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> { vec![self] }
747
748 fn validate_endpoint(&self, call: &alibi_core::endpoint::EndpointCall, _ctx: &AuthContext<S>) -> AuthResult<alibi_core::endpoint::EndpointInput> { endpoint::validate(call) }
749
750 async fn on_endpoint(&self, call: &alibi_core::endpoint::EndpointCall, ctx: &AuthContext<S>) -> AuthResult<alibi_core::endpoint::EndpointResponse> { self.call_endpoint(call, ctx).await }
751
752 async fn before_request(
753 &self,
754 req: &AuthRequest,
755 ctx: &AuthContext<S>,
756 ) -> AuthResult<Option<BeforeRequestAction>> {
757 self.api_key_session(req, ctx).await
758 }
759 }
760}
761
762impl std::fmt::Debug for ApiKeyPlugin {
763 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764 f.debug_struct("ApiKeyPlugin").finish_non_exhaustive()
765 }
766}
767
768pub(super) const fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
769 let status = match code {
770 ApiKeyErrorCode::UnauthorizedSession => 401,
771 ApiKeyErrorCode::OrganizationPluginRequired => 500,
772 ApiKeyErrorCode::UserNotMemberOfOrganization
773 | ApiKeyErrorCode::InsufficientApiKeyPermissions => 403,
774 ApiKeyErrorCode::InvalidApiKey
775 | ApiKeyErrorCode::KeyDisabled
776 | ApiKeyErrorCode::KeyExpired
777 | ApiKeyErrorCode::UsageExceeded
778 | ApiKeyErrorCode::KeyNotFound
779 | ApiKeyErrorCode::RateLimited
780 | ApiKeyErrorCode::InvalidPrefixLength
781 | ApiKeyErrorCode::InvalidNameLength
782 | ApiKeyErrorCode::MetadataDisabled
783 | ApiKeyErrorCode::NoValuesToUpdate
784 | ApiKeyErrorCode::KeyDisabledExpiration
785 | ApiKeyErrorCode::ExpiresInTooSmall
786 | ApiKeyErrorCode::ExpiresInTooLarge
787 | ApiKeyErrorCode::InvalidRemaining
788 | ApiKeyErrorCode::RefillAmountAndIntervalRequired
789 | ApiKeyErrorCode::RefillIntervalAndAmountRequired
790 | ApiKeyErrorCode::NameRequired
791 | ApiKeyErrorCode::InvalidUserIdFromApiKey
792 | ApiKeyErrorCode::InvalidReferenceIdFromApiKey
793 | ApiKeyErrorCode::NoDefaultConfiguration
794 | ApiKeyErrorCode::OrganizationIdRequired
795 | ApiKeyErrorCode::ServerOnlyProperty
796 | ApiKeyErrorCode::FailedToUpdateApiKey
797 | ApiKeyErrorCode::InvalidMetadataType => 400,
798 };
799 AuthError::Upstream {
800 status,
801 code: code.as_str(),
802 message: code.message(),
803 }
804}
805
806fn admit_expired_cleanup(bypass: bool) -> bool {
807 let mut last = LAST_EXPIRED_CHECK
808 .lock()
809 .unwrap_or_else(std::sync::PoisonError::into_inner);
810 let now = chrono::Utc::now().timestamp_millis();
811 if !bypass && last.is_some_and(|previous| now.saturating_sub(previous) < 10_000) {
812 return false;
813 }
814 *last = Some(now);
815 true
816}
817
818pub(super) fn is_default_config_id(config_id: &str) -> bool {
821 config_id.is_empty() || config_id == "default"
822}
823
824pub(super) fn config_id_matches(key_config_id: &str, expected: &str) -> bool {
826 if is_default_config_id(key_config_id) && is_default_config_id(expected) {
827 return true;
828 }
829 key_config_id == expected
830}
831
832#[cfg(test)]
834mod tests {
835
836 use super::*;
837 use alibi_core::wire::{SessionView, UserView};
838 use alibi_core::{
839 AuthContext, AuthPlugin, CreateSession, CreateUser, HttpMethod, UpdateApiKey,
840 };
841 use chrono::{Duration, Utc};
842 use std::collections::HashMap;
843 use std::sync::Arc;
844
845 type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
846
847 async fn create_test_context_with_user() -> (AuthContext<TestSchema>, UserView, SessionView) {
848 let config = Arc::new(alibi_core::AuthConfig::new(
849 "test-secret-key-at-least-32-chars-long",
850 ));
851 let database = crate::plugins::test_helpers::create_test_database().await;
852 let ctx = AuthContext::new(config, Arc::clone(&database));
853
854 let user = database
855 .create_user(
856 CreateUser::new()
857 .with_email("test@example.com")
858 .with_name("Test User"),
859 )
860 .await
861 .unwrap();
862 let wire_user = UserView::from(&user);
863
864 let session = database
865 .create_session(CreateSession {
866 additional_fields: alibi_core::field_policy::FieldValues::default(),
867 token: None,
868 active_team_id: None,
869 user_id: user.id().to_string(),
870 expires_at: Utc::now() + Duration::hours(24),
871 ip_address: Some("127.0.0.1".to_owned()),
872 user_agent: Some("test-agent".to_owned()),
873 impersonated_by: None,
874 active_organization_id: None,
875 })
876 .await
877 .unwrap();
878 let wire_session = SessionView::from(&session);
879
880 (ctx, wire_user, wire_session)
881 }
882
883 async fn create_user_with_session(
884 ctx: &AuthContext<impl alibi_core::AuthSchema>,
885 email: &str,
886 ) -> (UserView, SessionView) {
887 let user = ctx
888 .database
889 .create_user(
890 CreateUser::new()
891 .with_email(email.to_owned())
892 .with_name("Another User"),
893 )
894 .await
895 .unwrap();
896 let wire_user = UserView::from(&user);
897
898 let session = ctx
899 .database
900 .create_session(CreateSession {
901 additional_fields: alibi_core::field_policy::FieldValues::default(),
902 token: None,
903 active_team_id: None,
904 user_id: user.id().to_string(),
905 expires_at: Utc::now() + Duration::hours(24),
906 ip_address: None,
907 user_agent: None,
908 impersonated_by: None,
909 active_organization_id: None,
910 })
911 .await
912 .unwrap();
913 let wire_session = SessionView::from(&session);
914
915 (wire_user, wire_session)
916 }
917
918 fn create_auth_request(
919 method: HttpMethod,
920 path: &str,
921 token: Option<&str>,
922 body: Option<serde_json::Value>,
923 query: Option<HashMap<String, String>>,
924 ) -> AuthRequest {
925 let mut headers = HashMap::new();
926 if let Some(token) = token {
927 headers.insert(
928 "cookie".to_owned(),
929 format!(
930 "better-auth.session_token={}",
931 alibi_core::utils::cookie_utils::sign_cookie_value(
932 token,
933 &crate::plugins::test_helpers::create_test_config().secret
934 )
935 ),
936 );
937 }
938
939 AuthRequest::from_parts(
940 method,
941 path.to_owned(),
942 headers,
943 body.map(|b| serde_json::to_vec(&b).unwrap()),
944 query.unwrap_or_default(),
945 )
946 }
947
948 fn json_body(response: &AuthResponse) -> serde_json::Value {
949 serde_json::from_slice(&response.body).unwrap()
950 }
951
952 async fn verify_key(
955 plugin: &ApiKeyPlugin,
956 ctx: &AuthContext<impl alibi_core::AuthSchema>,
957 raw_key: &str,
958 permissions: Option<&serde_json::Value>,
959 ) -> serde_json::Value {
960 match plugin
961 .verify_api_key(
962 &VerifyApiKey {
963 key: raw_key,
964 config_id: None,
965 permissions,
966 },
967 ctx,
968 )
969 .await
970 {
971 Ok(view) => serde_json::json!({ "valid": true, "error": null, "key": view }),
972 Err(ApiKeyVerificationError::Validation(error)) => serde_json::json!({
973 "valid": false,
974 "error": error,
975 "key": null,
976 }),
977 Err(
978 ApiKeyVerificationError::Internal(error)
979 | ApiKeyVerificationError::ExplicitValidator(error),
980 ) => panic!("Verification failed: {error}"),
981 }
982 }
983
984 async fn create_key_with_server_fields(
990 plugin: &ApiKeyPlugin,
991 ctx: &AuthContext<impl alibi_core::AuthSchema>,
992 token: &str,
993 client_body: serde_json::Value,
994 server_fields: UpdateApiKey,
995 ) -> (String, String) {
996 let (id, raw_key) = create_key_and_get_raw(plugin, ctx, token, client_body).await;
997 ctx.database
998 .update_api_key(&id, server_fields)
999 .await
1000 .unwrap();
1001 (id, raw_key)
1002 }
1003
1004 async fn delete_all_expired(
1006 ctx: &AuthContext<impl alibi_core::AuthSchema>,
1007 ) -> serde_json::Value {
1008 let _ignored_result = ctx.database.delete_expired_api_keys().await.unwrap();
1009 serde_json::json!({ "success": true, "error": null })
1010 }
1011
1012 async fn create_key_and_get_id(
1013 plugin: &ApiKeyPlugin,
1014 ctx: &AuthContext<impl alibi_core::AuthSchema>,
1015 token: &str,
1016 name: &str,
1017 ) -> String {
1018 let req = create_auth_request(
1019 HttpMethod::Post,
1020 "/api-key/create",
1021 Some(token),
1022 Some(serde_json::json!({ "name": name })),
1023 None,
1024 );
1025 let response = plugin.handle_create(&req, ctx).await.unwrap();
1026 assert_eq!(response.status, 200);
1027 (*(json_body(&response))
1028 .get("id")
1029 .unwrap_or(&serde_json::Value::Null))
1030 .as_str()
1031 .unwrap()
1032 .to_owned()
1033 }
1034
1035 async fn create_key_and_get_raw(
1037 plugin: &ApiKeyPlugin,
1038 ctx: &AuthContext<impl alibi_core::AuthSchema>,
1039 token: &str,
1040 body: serde_json::Value,
1041 ) -> (String, String) {
1042 let req = create_auth_request(
1043 HttpMethod::Post,
1044 "/api-key/create",
1045 Some(token),
1046 Some(body),
1047 None,
1048 );
1049 let response = plugin.handle_create(&req, ctx).await.unwrap();
1050 assert_eq!(response.status, 200);
1051 let b = json_body(&response);
1052 (
1053 (*(b).get("id").expect("fixture contains the requested index"))
1054 .as_str()
1055 .unwrap()
1056 .to_owned(),
1057 (*(b)
1058 .get("key")
1059 .expect("fixture contains the requested index"))
1060 .as_str()
1061 .unwrap()
1062 .to_owned(),
1063 )
1064 }
1065
1066 #[tokio::test]
1072 async fn test_create_and_get_do_not_expose_hash() {
1073 let plugin = ApiKeyPlugin::builder().prefix("ba_".to_owned()).build();
1074 let (ctx, _user, session) = create_test_context_with_user().await;
1075
1076 let create_req = create_auth_request(
1077 HttpMethod::Post,
1078 "/api-key/create",
1079 Some(&session.token),
1080 Some(serde_json::json!({ "name": "primary" })),
1081 None,
1082 );
1083 let create_response = plugin.handle_create(&create_req, &ctx).await.unwrap();
1084 assert_eq!(create_response.status, 200);
1085
1086 let body = json_body(&create_response);
1087 assert!(body.get("key").is_some());
1088 assert!(body.get("key_hash").is_none());
1089 assert!(body.get("hash").is_none());
1090
1091 let id = (*(body).get("id").unwrap_or(&serde_json::Value::Null))
1092 .as_str()
1093 .unwrap();
1094 let mut query = HashMap::new();
1095 query.insert("id".to_owned(), id.to_owned());
1096
1097 let get_req = create_auth_request(
1098 HttpMethod::Get,
1099 "/api-key/get",
1100 Some(&session.token),
1101 None,
1102 Some(query),
1103 );
1104 let get_response = plugin.handle_get(&get_req, &ctx).await.unwrap();
1105 assert_eq!(get_response.status, 200);
1106
1107 let get_body = json_body(&get_response);
1108 assert!(get_body.get("key").is_none());
1109 assert!(get_body.get("key_hash").is_none());
1110 }
1111
1112 #[tokio::test]
1114 async fn test_create_rejects_invalid_expires_in() {
1115 let plugin = ApiKeyPlugin::builder().build();
1116 let (ctx, _user, session) = create_test_context_with_user().await;
1117
1118 let req = create_auth_request(
1119 HttpMethod::Post,
1120 "/api-key/create",
1121 Some(&session.token),
1122 Some(serde_json::json!({ "expiresIn": -1 })),
1123 None,
1124 );
1125 let response = plugin.handle_create(&req, &ctx).await;
1126 assert!(response.is_err() || response.unwrap().status != 200);
1128 }
1129
1130 #[tokio::test]
1132 async fn test_get_update_delete_return_404_for_non_owner() {
1133 let plugin = ApiKeyPlugin::builder().build();
1134 let (ctx, _user1, session1) = create_test_context_with_user().await;
1135 let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1136 let key_id = create_key_and_get_id(&plugin, &ctx, &session1.token, "owner-key").await;
1137
1138 let mut get_query = HashMap::new();
1139 get_query.insert("id".to_owned(), key_id.clone());
1140 let get_req = create_auth_request(
1141 HttpMethod::Get,
1142 "/api-key/get",
1143 Some(&session2.token),
1144 None,
1145 Some(get_query),
1146 );
1147 let get_err = plugin.handle_get(&get_req, &ctx).await.unwrap_err();
1148 assert_eq!(get_err.status_code(), 404);
1149
1150 let update_req = create_auth_request(
1151 HttpMethod::Post,
1152 "/api-key/update",
1153 Some(&session2.token),
1154 Some(serde_json::json!({ "keyId": key_id, "name": "new-name" })),
1155 None,
1156 );
1157 let update_err = plugin.handle_update(&update_req, &ctx).await.unwrap_err();
1158 assert_eq!(update_err.status_code(), 404);
1159
1160 let delete_req = create_auth_request(
1161 HttpMethod::Post,
1162 "/api-key/delete",
1163 Some(&session2.token),
1164 Some(serde_json::json!({ "keyId": key_id })),
1165 None,
1166 );
1167 let delete_err = plugin.handle_delete(&delete_req, &ctx).await.unwrap_err();
1168 assert_eq!(delete_err.status_code(), 404);
1169 }
1170
1171 #[tokio::test]
1173 async fn test_list_returns_only_user_keys() {
1174 let plugin = ApiKeyPlugin::builder().build();
1175 let (ctx, user1, session1) = create_test_context_with_user().await;
1176 let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1177
1178 drop(create_key_and_get_id(&plugin, &ctx, &session1.token, "u1-key").await);
1179 drop(create_key_and_get_id(&plugin, &ctx, &session2.token, "u2-key").await);
1180
1181 let list_req = create_auth_request(
1182 HttpMethod::Get,
1183 "/api-key/list",
1184 Some(&session1.token),
1185 None,
1186 None,
1187 );
1188 let list_response = plugin.handle_list(&list_req, &ctx).await.unwrap();
1189 assert_eq!(list_response.status, 200);
1190
1191 let list_body = json_body(&list_response);
1192 let list = (*(list_body)
1194 .get("apiKeys")
1195 .unwrap_or(&serde_json::Value::Null))
1196 .as_array()
1197 .unwrap();
1198 assert_eq!(list.len(), 1);
1199 assert_eq!(
1200 (*(list_body).get("total").unwrap_or(&serde_json::Value::Null)),
1201 1
1202 );
1203 assert_eq!(
1206 (*(*(list)
1207 .first()
1208 .expect("fixture contains the requested index"))
1209 .get("referenceId")
1210 .expect("fixture contains the requested index"))
1211 .as_str()
1212 .unwrap(),
1213 user1.id
1214 );
1215 assert_eq!(
1216 (*(*(list)
1217 .first()
1218 .expect("fixture contains the requested index"))
1219 .get("configId")
1220 .expect("fixture contains the requested index"))
1221 .as_str()
1222 .unwrap(),
1223 "default"
1224 );
1225 assert!(
1226 (*(list)
1227 .first()
1228 .expect("fixture contains the requested index"))
1229 .get("userId")
1230 .is_none()
1231 );
1232 assert!(
1233 (*(list)
1234 .first()
1235 .expect("fixture contains the requested index"))
1236 .get("key")
1237 .is_none()
1238 );
1239 assert!(
1240 (*(list)
1241 .first()
1242 .expect("fixture contains the requested index"))
1243 .get("key_hash")
1244 .is_none()
1245 );
1246 }
1247
1248 #[tokio::test]
1250 async fn test_owner_can_delete_key() {
1251 let plugin = ApiKeyPlugin::builder().build();
1252 let (ctx, _user, session) = create_test_context_with_user().await;
1253 let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "to-delete").await;
1254
1255 let delete_req = create_auth_request(
1256 HttpMethod::Post,
1257 "/api-key/delete",
1258 Some(&session.token),
1259 Some(serde_json::json!({ "keyId": key_id })),
1260 None,
1261 );
1262 let delete_response = plugin.handle_delete(&delete_req, &ctx).await.unwrap();
1263 assert_eq!(delete_response.status, 200);
1264
1265 let deleted = ctx.database.get_api_key_by_id(&key_id).await.unwrap();
1266 assert!(deleted.is_none());
1267 }
1268
1269 #[tokio::test]
1275 async fn test_verify_valid_key() {
1276 let plugin = ApiKeyPlugin::builder().build();
1277 let (ctx, _user, session) = create_test_context_with_user().await;
1278
1279 let (_id, raw_key) = create_key_and_get_raw(
1280 &plugin,
1281 &ctx,
1282 &session.token,
1283 serde_json::json!({ "name": "verify-test" }),
1284 )
1285 .await;
1286
1287 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1288 assert_eq!(
1289 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1290 true
1291 );
1292 assert!((*(body).get("key").unwrap_or(&serde_json::Value::Null)).is_object());
1293 }
1294
1295 #[tokio::test]
1296 async fn test_verify_invalid_key() {
1297 let plugin = ApiKeyPlugin::builder().build();
1298 let (ctx, _user, _session) = create_test_context_with_user().await;
1299
1300 let body = verify_key(&plugin, &ctx, "definitely-not-a-valid-key", None).await;
1301 assert_eq!(
1302 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1303 false
1304 );
1305 assert!((*(body).get("error").unwrap_or(&serde_json::Value::Null)).is_object());
1306 }
1307
1308 #[tokio::test]
1309 async fn test_verify_disabled_key() {
1310 let plugin = ApiKeyPlugin::builder().build();
1311 let (ctx, _user, session) = create_test_context_with_user().await;
1312
1313 let (id, raw_key) = create_key_and_get_raw(
1314 &plugin,
1315 &ctx,
1316 &session.token,
1317 serde_json::json!({ "name": "disable-test" }),
1318 )
1319 .await;
1320
1321 let update = UpdateApiKey {
1322 enabled: Some(false),
1323 ..Default::default()
1324 };
1325 ctx.database.update_api_key(&id, update).await.unwrap();
1326
1327 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1328 assert_eq!(
1329 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1330 false
1331 );
1332 assert_eq!(
1333 (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
1334 .get("code")
1335 .unwrap_or(&serde_json::Value::Null)),
1336 "KEY_DISABLED"
1337 );
1338 }
1339
1340 #[tokio::test]
1341 async fn test_verify_expired_key() {
1342 let plugin = ApiKeyPlugin::builder().build();
1343 let (ctx, _user, session) = create_test_context_with_user().await;
1344
1345 let (id, raw_key) = create_key_and_get_raw(
1346 &plugin,
1347 &ctx,
1348 &session.token,
1349 serde_json::json!({ "name": "expire-test" }),
1350 )
1351 .await;
1352
1353 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1354 let update = UpdateApiKey {
1355 expires_at: Some(Some(past)),
1356 ..Default::default()
1357 };
1358 ctx.database.update_api_key(&id, update).await.unwrap();
1359
1360 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1361 assert_eq!(
1362 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1363 false
1364 );
1365 assert_eq!(
1366 (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
1367 .get("code")
1368 .unwrap_or(&serde_json::Value::Null)),
1369 "KEY_EXPIRED"
1370 );
1371
1372 let deleted = ctx.database.get_api_key_by_id(&id).await.unwrap();
1374 assert!(deleted.is_none());
1375 }
1376
1377 #[tokio::test]
1379 async fn test_verify_remaining_consumption() {
1380 let plugin = ApiKeyPlugin::builder().build();
1381 let (ctx, _user, session) = create_test_context_with_user().await;
1382
1383 let (_id, raw_key) = create_key_with_server_fields(
1384 &plugin,
1385 &ctx,
1386 &session.token,
1387 serde_json::json!({ "name": "remain-test" }),
1388 UpdateApiKey {
1389 remaining: Some(2.0),
1390 ..Default::default()
1391 },
1392 )
1393 .await;
1394
1395 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1397 assert_eq!(
1398 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1399 true
1400 );
1401 assert_eq!(
1402 (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1403 .get("remaining")
1404 .unwrap_or(&serde_json::Value::Null)),
1405 1
1406 );
1407
1408 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1410 assert_eq!(
1411 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1412 true
1413 );
1414 assert_eq!(
1415 (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
1416 .get("remaining")
1417 .unwrap_or(&serde_json::Value::Null)),
1418 0
1419 );
1420
1421 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1423 assert_eq!(
1424 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1425 false
1426 );
1427 assert_eq!(
1428 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1429 .get("code")
1430 .unwrap_or(&serde_json::Value::Null)),
1431 "USAGE_EXCEEDED"
1432 );
1433 }
1434
1435 #[tokio::test]
1437 async fn test_verify_rate_limiting() {
1438 let plugin = ApiKeyPlugin::builder()
1439 .rate_limit(RateLimitDefaults {
1440 enabled: true,
1441 time_window: 60_000.0,
1442 max_requests: 2.0,
1443 })
1444 .build();
1445 let (ctx, _user, session) = create_test_context_with_user().await;
1446
1447 let (_id, raw_key) = create_key_with_server_fields(
1448 &plugin,
1449 &ctx,
1450 &session.token,
1451 serde_json::json!({ "name": "rl-test" }),
1452 UpdateApiKey {
1453 rate_limit_enabled: Some(true),
1454 rate_limit_time_window: Some(60_000.0),
1455 rate_limit_max: Some(2.0),
1456 ..Default::default()
1457 },
1458 )
1459 .await;
1460
1461 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1463 assert_eq!(
1464 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1465 true
1466 );
1467
1468 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1469 assert_eq!(
1470 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1471 true
1472 );
1473
1474 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1476 assert_eq!(
1477 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1478 false
1479 );
1480 assert_eq!(
1481 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1482 .get("code")
1483 .unwrap_or(&serde_json::Value::Null)),
1484 "RATE_LIMITED"
1485 );
1486 }
1487
1488 #[tokio::test]
1489 async fn test_delete_all_expired() {
1490 let plugin = ApiKeyPlugin::builder().build();
1491 let (ctx, fixture_user, session) = create_test_context_with_user().await;
1492
1493 let (id1, _) = create_key_and_get_raw(
1495 &plugin,
1496 &ctx,
1497 &session.token,
1498 serde_json::json!({ "name": "will-expire" }),
1499 )
1500 .await;
1501 let (_id2, _) = create_key_and_get_raw(
1502 &plugin,
1503 &ctx,
1504 &session.token,
1505 serde_json::json!({ "name": "wont-expire" }),
1506 )
1507 .await;
1508
1509 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1511 ctx.database
1512 .update_api_key(
1513 &id1,
1514 UpdateApiKey {
1515 expires_at: Some(Some(past)),
1516 ..Default::default()
1517 },
1518 )
1519 .await
1520 .unwrap();
1521
1522 let body = delete_all_expired(&ctx).await;
1523 assert_eq!(
1524 (*(body).get("success").unwrap_or(&serde_json::Value::Null)),
1525 true
1526 );
1527
1528 let remaining_keys = ctx
1530 .database
1531 .list_api_keys_by_reference(&fixture_user.id)
1532 .await
1533 .unwrap();
1534 assert_eq!(remaining_keys.len(), 1);
1535 }
1536
1537 #[tokio::test]
1539 async fn test_verify_permissions() {
1540 let plugin = ApiKeyPlugin::builder().build();
1541 let (ctx, _user, session) = create_test_context_with_user().await;
1542
1543 let (_id, raw_key) = create_key_with_server_fields(
1544 &plugin,
1545 &ctx,
1546 &session.token,
1547 serde_json::json!({ "name": "perm-test" }),
1548 UpdateApiKey {
1549 permissions: Some(
1550 serde_json::to_string(&serde_json::json!({
1551 "admin": ["read", "write"],
1552 "user": ["read"]
1553 }))
1554 .unwrap(),
1555 ),
1556 ..Default::default()
1557 },
1558 )
1559 .await;
1560
1561 let perms_ok = serde_json::json!({ "admin": ["read"] });
1563 let r1 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_ok)).await;
1564 assert_eq!(
1565 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1566 true
1567 );
1568
1569 let perms_fail = serde_json::json!({ "superadmin": ["delete"] });
1571 let r2 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_fail)).await;
1572 assert_eq!(
1573 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1574 false
1575 );
1576 }
1577
1578 #[tokio::test]
1580 async fn test_config_validation_prefix_length() {
1581 let plugin = ApiKeyPlugin::builder()
1582 .min_prefix_length(2)
1583 .max_prefix_length(5)
1584 .build();
1585 let (ctx, _user, session) = create_test_context_with_user().await;
1586
1587 let req = create_auth_request(
1589 HttpMethod::Post,
1590 "/api-key/create",
1591 Some(&session.token),
1592 Some(serde_json::json!({ "name": "test", "prefix": "a" })),
1593 None,
1594 );
1595 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1596 assert!(err.to_string().contains("prefix length"));
1597
1598 let req2 = create_auth_request(
1600 HttpMethod::Post,
1601 "/api-key/create",
1602 Some(&session.token),
1603 Some(serde_json::json!({ "name": "test", "prefix": "toolong" })),
1604 None,
1605 );
1606 let err2 = plugin.handle_create(&req2, &ctx).await.unwrap_err();
1607 assert!(err2.to_string().contains("prefix length"));
1608 }
1609
1610 #[tokio::test]
1612 async fn test_config_require_name() {
1613 let plugin = ApiKeyPlugin::builder().require_name(true).build();
1614 let (ctx, _user, session) = create_test_context_with_user().await;
1615
1616 let req = create_auth_request(
1618 HttpMethod::Post,
1619 "/api-key/create",
1620 Some(&session.token),
1621 Some(serde_json::json!({})),
1622 None,
1623 );
1624 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1625 assert!(err.to_string().contains("name is required"));
1626 }
1627
1628 #[tokio::test]
1630 async fn test_config_metadata_disabled() {
1631 let plugin = ApiKeyPlugin::builder().build(); let (ctx, _user, session) = create_test_context_with_user().await;
1633
1634 let req = create_auth_request(
1635 HttpMethod::Post,
1636 "/api-key/create",
1637 Some(&session.token),
1638 Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1639 None,
1640 );
1641 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1642 assert!(err.to_string().contains("Metadata is disabled"));
1643 }
1644
1645 #[tokio::test]
1647 async fn test_config_metadata_enabled() {
1648 let plugin = ApiKeyPlugin::builder().enable_metadata(true).build();
1649 let (ctx, _user, session) = create_test_context_with_user().await;
1650
1651 let req = create_auth_request(
1652 HttpMethod::Post,
1653 "/api-key/create",
1654 Some(&session.token),
1655 Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1656 None,
1657 );
1658 let resp = plugin.handle_create(&req, &ctx).await.unwrap();
1659 assert_eq!(resp.status, 200);
1660 let body = json_body(&resp);
1661 assert_eq!(
1662 (*(*(body).get("metadata").unwrap_or(&serde_json::Value::Null))
1663 .get("env")
1664 .unwrap_or(&serde_json::Value::Null)),
1665 "prod"
1666 );
1667 }
1668
1669 #[tokio::test]
1671 async fn test_update_with_expires_in() {
1672 let plugin = ApiKeyPlugin::builder().build();
1673 let (ctx, _user, session) = create_test_context_with_user().await;
1674 let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "update-exp").await;
1675
1676 let update_req = create_auth_request(
1677 HttpMethod::Post,
1678 "/api-key/update",
1679 Some(&session.token),
1680 Some(serde_json::json!({
1681 "keyId": key_id,
1682 "expiresIn": 86400
1683 })),
1684 None,
1685 );
1686 let resp = plugin.handle_update(&update_req, &ctx).await.unwrap();
1687 assert_eq!(resp.status, 200);
1688 let body = json_body(&resp);
1689 assert!((*(body).get("expiresAt").unwrap_or(&serde_json::Value::Null)).is_string());
1690 }
1691
1692 #[tokio::test]
1698 async fn test_refill_logic() {
1699 let result = ApiKeyPlugin::validate_refill(Some(60_000.0), None);
1700 assert!(result.is_err());
1701 assert!(result.unwrap_err().to_string().contains("refillAmount"));
1702
1703 let result_2 = ApiKeyPlugin::validate_refill(None, Some(10.0));
1704 assert!(result_2.is_err());
1705
1706 let result_3 = ApiKeyPlugin::validate_refill(Some(60_000.0), Some(10.0));
1707 assert!(result_3.is_ok());
1708 }
1709
1710 mod session_tests {
1711 use super::*;
1712
1713 #[tokio::test]
1718 async fn test_virtual_session_answers_get_and_post_get_session() {
1719 use alibi_core::AuthPlugin;
1720
1721 let plugin = ApiKeyPlugin::builder()
1722 .enable_session_for_api_keys(true)
1723 .build();
1724 let (ctx, user, session) = create_test_context_with_user().await;
1725 let (id, raw_key) = create_key_and_get_raw(
1726 &plugin,
1727 &ctx,
1728 &session.token,
1729 serde_json::json!({ "name": "get-session" }),
1730 )
1731 .await;
1732
1733 for method in [HttpMethod::Get, HttpMethod::Post] {
1734 let mut request = create_auth_request(method, "/get-session", None, None, None);
1735 drop(
1736 request
1737 .headers
1738 .insert("x-api-key".to_owned(), raw_key.clone()),
1739 );
1740 let action = AuthPlugin::<TestSchema>::before_request(&plugin, &request, &ctx)
1741 .await
1742 .unwrap();
1743 let Some(BeforeRequestAction::Respond(response)) = action else {
1744 panic!("API key sessions must answer GET and POST before route dispatch")
1745 };
1746 assert_eq!(response.status, 200);
1747 let body = json_body(&response);
1748 assert_eq!(
1749 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1750 .get("id")
1751 .unwrap_or(&serde_json::Value::Null)),
1752 id
1753 );
1754 assert_eq!(
1755 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1756 .get("token")
1757 .unwrap_or(&serde_json::Value::Null)),
1758 raw_key
1759 );
1760 assert_eq!(
1761 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1762 .get("userId")
1763 .unwrap_or(&serde_json::Value::Null)),
1764 user.id
1765 );
1766 }
1767 }
1768
1769 #[tokio::test]
1772 async fn test_virtual_session_creates_no_db_session() {
1773 let plugin = ApiKeyPlugin::builder()
1774 .enable_session_for_api_keys(true)
1775 .build();
1776 let (ctx, fixture_user, session) = create_test_context_with_user().await;
1777
1778 let (_id, raw_key) = create_key_and_get_raw(
1780 &plugin,
1781 &ctx,
1782 &session.token,
1783 serde_json::json!({ "name": "virtual-session-test" }),
1784 )
1785 .await;
1786
1787 let sessions_before = ctx
1789 .database
1790 .get_user_sessions(&fixture_user.id)
1791 .await
1792 .unwrap()
1793 .len();
1794
1795 let mut headers = HashMap::new();
1797 headers.insert("x-api-key".to_owned(), raw_key.clone());
1798 let req = AuthRequest::from_parts(
1799 HttpMethod::Post,
1800 "/update-user".to_owned(),
1801 headers,
1802 None,
1803 HashMap::new(),
1804 );
1805
1806 let action = plugin.before_request(&req, &ctx).await.unwrap();
1808 assert!(action.is_some(), "before_request should return an action");
1809 match action.unwrap() {
1810 BeforeRequestAction::InjectSession { session: session_2 } => {
1811 assert_eq!(session_2.user_id, fixture_user.id);
1812 }
1813 BeforeRequestAction::Respond(_) | BeforeRequestAction::ReplaceHeaders { .. } => {
1814 panic!("Expected InjectSession, got Respond");
1815 }
1816 }
1817
1818 let sessions_after = ctx
1820 .database
1821 .get_user_sessions(&fixture_user.id)
1822 .await
1823 .unwrap()
1824 .len();
1825 assert_eq!(
1826 sessions_before, sessions_after,
1827 "No new sessions should be created in the database"
1828 );
1829 }
1830
1831 #[tokio::test]
1834 async fn test_virtual_session_on_get_session() {
1835 let plugin = ApiKeyPlugin::builder()
1836 .enable_session_for_api_keys(true)
1837 .build();
1838 let (ctx, user, session) = create_test_context_with_user().await;
1839
1840 let (_id, raw_key) = create_key_and_get_raw(
1841 &plugin,
1842 &ctx,
1843 &session.token,
1844 serde_json::json!({ "name": "get-session-test" }),
1845 )
1846 .await;
1847
1848 let mut headers = HashMap::new();
1850 headers.insert("x-api-key".to_owned(), raw_key.clone());
1851 let req = AuthRequest::from_parts(
1852 HttpMethod::Get,
1853 "/get-session".to_owned(),
1854 headers,
1855 None,
1856 HashMap::new(),
1857 );
1858
1859 let action = plugin.before_request(&req, &ctx).await.unwrap();
1860 assert!(action.is_some());
1861 match action.unwrap() {
1862 BeforeRequestAction::Respond(resp) => {
1863 assert_eq!(resp.status, 200);
1864 let body: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
1865 assert_eq!(
1867 (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
1868 .get("id")
1869 .unwrap_or(&serde_json::Value::Null)),
1870 user.id
1871 );
1872 assert_eq!(
1873 (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
1874 .get("email")
1875 .unwrap_or(&serde_json::Value::Null)),
1876 "test@example.com"
1877 );
1878 assert!(
1880 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1881 .get("id")
1882 .unwrap_or(&serde_json::Value::Null))
1883 .is_string()
1884 );
1885 assert_eq!(
1886 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1887 .get("userId")
1888 .unwrap_or(&serde_json::Value::Null)),
1889 user.id
1890 );
1891 }
1892 BeforeRequestAction::InjectSession { .. }
1893 | BeforeRequestAction::ReplaceHeaders { .. } => {
1894 panic!("Expected Respond for /get-session, got InjectSession");
1895 }
1896 }
1897 }
1898
1899 #[tokio::test]
1902 async fn test_rate_limiting_third_call_fails() {
1903 let plugin = ApiKeyPlugin::builder()
1904 .rate_limit(RateLimitDefaults {
1905 enabled: true,
1906 time_window: 60_000.0,
1907 max_requests: 2.0,
1908 })
1909 .build();
1910 let (ctx, _user, session) = create_test_context_with_user().await;
1911
1912 let (_id, raw_key) = create_key_with_server_fields(
1913 &plugin,
1914 &ctx,
1915 &session.token,
1916 serde_json::json!({ "name": "rl-integration" }),
1917 UpdateApiKey {
1918 rate_limit_enabled: Some(true),
1919 rate_limit_time_window: Some(60_000.0),
1920 rate_limit_max: Some(2.0),
1921 ..Default::default()
1922 },
1923 )
1924 .await;
1925
1926 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1928 assert_eq!(
1929 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1930 true,
1931 "1st request should pass"
1932 );
1933
1934 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1935 assert_eq!(
1936 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1937 true,
1938 "2nd request should pass"
1939 );
1940
1941 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1943 assert_eq!(
1944 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1945 false,
1946 "3rd request should be rate-limited"
1947 );
1948 assert_eq!(
1949 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1950 .get("code")
1951 .unwrap_or(&serde_json::Value::Null)),
1952 "RATE_LIMITED"
1953 );
1954 }
1955
1956 #[tokio::test]
1959 async fn test_remaining_consumption_no_refill() {
1960 let plugin = ApiKeyPlugin::builder().build();
1961 let (ctx, _user, session) = create_test_context_with_user().await;
1962
1963 let (_id, raw_key) = create_key_with_server_fields(
1964 &plugin,
1965 &ctx,
1966 &session.token,
1967 serde_json::json!({ "name": "remaining-test" }),
1968 UpdateApiKey {
1969 remaining: Some(2.0),
1970 ..Default::default()
1971 },
1972 )
1973 .await;
1974
1975 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1977 assert_eq!(
1978 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1979 true
1980 );
1981 assert_eq!(
1982 (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1983 .get("remaining")
1984 .unwrap_or(&serde_json::Value::Null)),
1985 1
1986 );
1987
1988 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1990 assert_eq!(
1991 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1992 true
1993 );
1994 assert_eq!(
1995 (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
1996 .get("remaining")
1997 .unwrap_or(&serde_json::Value::Null)),
1998 0
1999 );
2000
2001 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
2003 assert_eq!(
2004 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
2005 false
2006 );
2007 assert_eq!(
2008 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
2009 .get("code")
2010 .unwrap_or(&serde_json::Value::Null)),
2011 "USAGE_EXCEEDED"
2012 );
2013 }
2014
2015 #[tokio::test]
2020 async fn test_refill_resets_remaining_after_interval() {
2021 let plugin = ApiKeyPlugin::builder().build();
2022 let (ctx, _user, session) = create_test_context_with_user().await;
2023
2024 let (_id, raw_key) = create_key_with_server_fields(
2026 &plugin,
2027 &ctx,
2028 &session.token,
2029 serde_json::json!({ "name": "refill-test" }),
2030 UpdateApiKey {
2031 remaining: Some(1.0),
2032 refill_interval: Some(100.0),
2033 refill_amount: Some(10.0),
2034 ..Default::default()
2035 },
2036 )
2037 .await;
2038
2039 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
2041 assert_eq!(
2042 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
2043 true
2044 );
2045 assert_eq!(
2046 (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
2047 .get("remaining")
2048 .unwrap_or(&serde_json::Value::Null)),
2049 0
2050 );
2051
2052 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2054
2055 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
2057 assert_eq!(
2058 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
2059 true,
2060 "Should succeed after refill"
2061 );
2062 assert_eq!(
2063 (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
2064 .get("remaining")
2065 .unwrap_or(&serde_json::Value::Null)),
2066 9,
2067 "Should be refillAmount - 1 = 9"
2068 );
2069 }
2070
2071 #[tokio::test]
2075 async fn test_permissions_mismatch_fails() {
2076 let plugin = ApiKeyPlugin::builder().build();
2077 let (ctx, _user, session) = create_test_context_with_user().await;
2078
2079 let (_id, raw_key) = create_key_with_server_fields(
2080 &plugin,
2081 &ctx,
2082 &session.token,
2083 serde_json::json!({ "name": "perm-mismatch" }),
2084 UpdateApiKey {
2085 permissions: Some(
2086 serde_json::to_string(&serde_json::json!({ "admin": ["read"] })).unwrap(),
2087 ),
2088 ..Default::default()
2089 },
2090 )
2091 .await;
2092
2093 let perms_ok = serde_json::json!({ "admin": ["read"] });
2095 let r1 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_ok)).await;
2096 assert_eq!(
2097 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
2098 true
2099 );
2100
2101 let perms_fail = serde_json::json!({ "admin": ["write"] });
2103 let r2 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_fail)).await;
2104 assert_eq!(
2105 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
2106 false
2107 );
2108 }
2109
2110 #[tokio::test]
2115 async fn test_concurrent_rate_limiting() {
2116 let plugin = ApiKeyPlugin::builder()
2117 .rate_limit(RateLimitDefaults {
2118 enabled: true,
2119 time_window: 60_000.0,
2120 max_requests: 2.0,
2121 })
2122 .build();
2123 let (ctx, _user, session) = create_test_context_with_user().await;
2124
2125 let (_id, raw_key) = create_key_with_server_fields(
2126 &plugin,
2127 &ctx,
2128 &session.token,
2129 serde_json::json!({ "name": "concurrent-rl" }),
2130 UpdateApiKey {
2131 rate_limit_enabled: Some(true),
2132 rate_limit_time_window: Some(60_000.0),
2133 rate_limit_max: Some(2.0),
2134 ..Default::default()
2135 },
2136 )
2137 .await;
2138
2139 let mut success_count = 0;
2140 let mut fail_count = 0;
2141
2142 for _ in 0..5 {
2143 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
2144 if (*(body).get("valid").unwrap_or(&serde_json::Value::Null)) == true {
2145 success_count += 1;
2146 } else {
2147 fail_count += 1;
2148 assert_eq!(
2149 (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
2150 .get("code")
2151 .unwrap_or(&serde_json::Value::Null)),
2152 "RATE_LIMITED"
2153 );
2154 }
2155 }
2156
2157 assert_eq!(success_count, 2, "Only 2 out of 5 should succeed");
2158 assert_eq!(fail_count, 3, "3 out of 5 should be rate-limited");
2159 }
2160
2161 #[tokio::test]
2165 async fn test_delete_expired_api_keys_memory_adapter() {
2166 let (ctx, fixture_user, session) = create_test_context_with_user().await;
2167 let plugin = ApiKeyPlugin::builder().build();
2168
2169 let (id1, _) = create_key_and_get_raw(
2171 &plugin,
2172 &ctx,
2173 &session.token,
2174 serde_json::json!({ "name": "will-expire" }),
2175 )
2176 .await;
2177 let (_id2, _) = create_key_and_get_raw(
2178 &plugin,
2179 &ctx,
2180 &session.token,
2181 serde_json::json!({ "name": "wont-expire" }),
2182 )
2183 .await;
2184
2185 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2187 ctx.database
2188 .update_api_key(
2189 &id1,
2190 UpdateApiKey {
2191 expires_at: Some(Some(past)),
2192 ..Default::default()
2193 },
2194 )
2195 .await
2196 .unwrap();
2197
2198 let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2200 assert_eq!(deleted, 1, "Should delete exactly 1 expired key");
2201
2202 let remaining = ctx
2204 .database
2205 .list_api_keys_by_reference(&fixture_user.id)
2206 .await
2207 .unwrap();
2208 assert_eq!(remaining.len(), 1);
2209 }
2210
2211 #[tokio::test]
2213 async fn test_delete_expired_removes_only_expired() {
2214 let plugin = ApiKeyPlugin::builder().build();
2215 let (ctx, fixture_user, session) = create_test_context_with_user().await;
2216
2217 let (id1, _) = create_key_and_get_raw(
2219 &plugin,
2220 &ctx,
2221 &session.token,
2222 serde_json::json!({ "name": "expired" }),
2223 )
2224 .await;
2225 let (_id2, _) = create_key_and_get_raw(
2226 &plugin,
2227 &ctx,
2228 &session.token,
2229 serde_json::json!({ "name": "active" }),
2230 )
2231 .await;
2232
2233 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2234 ctx.database
2235 .update_api_key(
2236 &id1,
2237 UpdateApiKey {
2238 expires_at: Some(Some(past)),
2239 ..Default::default()
2240 },
2241 )
2242 .await
2243 .unwrap();
2244
2245 let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2246 assert_eq!(deleted, 1);
2247
2248 let remaining = ctx
2249 .database
2250 .list_api_keys_by_reference(&fixture_user.id)
2251 .await
2252 .unwrap();
2253 assert_eq!(remaining.len(), 1);
2254 }
2255
2256 #[tokio::test]
2259 async fn test_before_request_disabled_returns_none() {
2260 let plugin = ApiKeyPlugin::builder().build(); let (ctx, _user, session) = create_test_context_with_user().await;
2262
2263 let (_id, raw_key) = create_key_and_get_raw(
2264 &plugin,
2265 &ctx,
2266 &session.token,
2267 serde_json::json!({ "name": "disabled-session" }),
2268 )
2269 .await;
2270
2271 let mut headers = HashMap::new();
2272 headers.insert("x-api-key".to_owned(), raw_key);
2273 let req = AuthRequest::from_parts(
2274 HttpMethod::Get,
2275 "/get-session".to_owned(),
2276 headers,
2277 None,
2278 HashMap::new(),
2279 );
2280
2281 let action = plugin.before_request(&req, &ctx).await.unwrap();
2282 assert!(
2283 action.is_none(),
2284 "before_request should return None when session emulation is disabled"
2285 );
2286 }
2287
2288 #[tokio::test]
2291 async fn test_resolve_configuration_falls_back_to_default() {
2292 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2293 config_id: "billing".to_owned(),
2294 ..ApiKeyConfig::default()
2295 });
2296
2297 assert_eq!(
2298 plugin.resolve_configuration(None).unwrap().config_id,
2299 "default"
2300 );
2301 assert_eq!(
2302 plugin
2303 .resolve_configuration(Some("billing"))
2304 .unwrap()
2305 .config_id,
2306 "billing"
2307 );
2308 assert_eq!(
2310 plugin
2311 .resolve_configuration(Some("nope"))
2312 .unwrap()
2313 .config_id,
2314 "default"
2315 );
2316 }
2317
2318 #[tokio::test]
2321 async fn test_resolve_configuration_without_default_is_an_error() {
2322 let plugin = ApiKeyPlugin::builder()
2323 .config_id("billing".to_owned())
2324 .build();
2325
2326 let err = plugin.resolve_configuration(None).unwrap_err();
2327 assert_eq!(err.status_code(), 400);
2328 assert_eq!(err.to_string(), "No default api-key configuration found.");
2329 }
2330
2331 #[tokio::test]
2334 async fn test_config_id_matches_treats_missing_as_default() {
2335 assert!(config_id_matches("", "default"));
2336 assert!(config_id_matches("default", ""));
2337 assert!(config_id_matches("billing", "billing"));
2338 assert!(!config_id_matches("billing", "default"));
2339 }
2340
2341 #[tokio::test]
2344 async fn test_create_for_organization_requires_organization_id() {
2345 let plugin = ApiKeyPlugin::builder()
2346 .references(ApiKeyReferences::Organization)
2347 .build();
2348 let (ctx, _user, session) = create_test_context_with_user().await;
2349
2350 let req = create_auth_request(
2351 HttpMethod::Post,
2352 "/api-key/create",
2353 Some(&session.token),
2354 Some(serde_json::json!({ "name": "org-key" })),
2355 None,
2356 );
2357 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2358
2359 assert_eq!(err.status_code(), 400);
2360 assert_eq!(
2361 err.to_string(),
2362 "Organization ID is required for organization-owned API keys."
2363 );
2364 }
2365
2366 #[tokio::test]
2369 async fn test_create_for_organization_requires_the_organization_plugin() {
2370 let plugin = ApiKeyPlugin::builder()
2371 .references(ApiKeyReferences::Organization)
2372 .build();
2373 let (ctx, _user, session) = create_test_context_with_user().await;
2374
2375 let req = create_auth_request(
2376 HttpMethod::Post,
2377 "/api-key/create",
2378 Some(&session.token),
2379 Some(serde_json::json!({ "name": "org-key", "organizationId": "org-1" })),
2380 None,
2381 );
2382 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2383
2384 assert_eq!(
2385 err.to_string(),
2386 "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
2387 );
2388 }
2389
2390 #[tokio::test]
2393 async fn test_create_for_organization_rejects_non_member() {
2394 let plugin = ApiKeyPlugin::builder()
2395 .references(ApiKeyReferences::Organization)
2396 .build();
2397 let (ctx, _user, session) = create_test_context_with_user().await;
2398
2399 let mut metadata: HashMap<String, serde_json::Value> = HashMap::new();
2401 drop(metadata.insert(
2402 crate::plugins::organization::METADATA_ENABLED.to_owned(),
2403 serde_json::Value::Bool(true),
2404 ));
2405 let ctx = AuthContext::with_metadata(
2406 Arc::clone(&ctx.config),
2407 Arc::clone(&ctx.database),
2408 metadata,
2409 );
2410
2411 let req = create_auth_request(
2412 HttpMethod::Post,
2413 "/api-key/create",
2414 Some(&session.token),
2415 Some(
2416 serde_json::json!({ "name": "org-key", "organizationId": "org-the-user-is-not-in" }),
2417 ),
2418 None,
2419 );
2420 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2421
2422 assert_eq!(
2423 err.to_string(),
2424 "You are not a member of the organization that owns this API key."
2425 );
2426 }
2427 }
2428
2429 mod verification_tests {
2430 use super::*;
2431
2432 #[tokio::test]
2433 async fn scoped_verification_uses_the_configuration_hashing_and_rejects_other_configs() {
2434 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2435 config_id: "machines".to_owned(),
2436 disable_key_hashing: true,
2437 ..Default::default()
2438 });
2439 let (ctx, _, session) = create_test_context_with_user().await;
2440 let (id, key) = create_key_and_get_raw(
2441 &plugin,
2442 &ctx,
2443 &session.token,
2444 serde_json::json!({ "configId": "machines", "name": "machine" }),
2445 )
2446 .await;
2447 let scoped = VerifyApiKey {
2448 key: &key,
2449 config_id: Some("machines"),
2450 permissions: None,
2451 };
2452 let verified = plugin.verify_api_key(&scoped, &ctx).await.unwrap();
2453 assert_eq!(verified.id, id);
2454 assert_eq!(verified.config_id, "machines");
2455 let serialized = serde_json::to_value(verified).unwrap();
2456 assert!(serialized.get("key").is_none());
2457 assert!(serialized.get("keyHash").is_none());
2458
2459 for config_id in [None, Some("default"), Some("unknown")] {
2460 let error = plugin
2461 .verify_api_key(
2462 &VerifyApiKey {
2463 config_id,
2464 ..scoped
2465 },
2466 &ctx,
2467 )
2468 .await
2469 .unwrap_err();
2470 assert!(matches!(
2471 error,
2472 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2473 code: ApiKeyErrorCode::InvalidApiKey,
2474 ..
2475 })
2476 ));
2477 }
2478 assert_eq!(
2479 ctx.database
2480 .get_api_key_by_id(&id)
2481 .await
2482 .unwrap()
2483 .unwrap()
2484 .request_count,
2485 Some(1.0)
2486 );
2487 }
2488
2489 #[tokio::test]
2490 async fn unscoped_verification_uses_the_issuing_configuration_limits() {
2491 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2492 config_id: "machines".to_owned(),
2493 rate_limit: RateLimitDefaults {
2494 enabled: false,
2495 ..Default::default()
2496 },
2497 ..Default::default()
2498 });
2499 let (ctx, _, session) = create_test_context_with_user().await;
2500 let (id, key) = create_key_and_get_raw(
2501 &plugin,
2502 &ctx,
2503 &session.token,
2504 serde_json::json!({ "configId": "machines" }),
2505 )
2506 .await;
2507 ctx.database
2508 .update_api_key(
2509 &id,
2510 UpdateApiKey {
2511 rate_limit_enabled: Some(true),
2512 rate_limit_max: Some(1.0),
2513 ..Default::default()
2514 },
2515 )
2516 .await
2517 .unwrap();
2518
2519 let input = VerifyApiKey {
2520 key: &key,
2521 config_id: None,
2522 permissions: None,
2523 };
2524 for _ in 0..3 {
2525 assert_eq!(plugin.verify_api_key(&input, &ctx).await.unwrap().id, id);
2526 }
2527 let error = plugin
2528 .verify_api_key(
2529 &VerifyApiKey {
2530 config_id: Some("default"),
2531 ..input
2532 },
2533 &ctx,
2534 )
2535 .await
2536 .unwrap_err();
2537 assert!(matches!(
2538 error,
2539 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2540 code: ApiKeyErrorCode::InvalidApiKey,
2541 ..
2542 })
2543 ));
2544 assert_eq!(
2545 ctx.database
2546 .get_api_key_by_id(&id)
2547 .await
2548 .unwrap()
2549 .unwrap()
2550 .request_count,
2551 Some(0.0)
2552 );
2553 }
2554
2555 #[tokio::test]
2556 async fn permissions_failure_preserves_usage_and_rate_limit_reports_retry_time() {
2557 let plugin = ApiKeyPlugin::builder().build();
2558 let (ctx, _, session) = create_test_context_with_user().await;
2559 let (id, key) = create_key_with_server_fields(
2560 &plugin,
2561 &ctx,
2562 &session.token,
2563 serde_json::json!({}),
2564 UpdateApiKey {
2565 remaining: Some(3.0),
2566 permissions: Some(serde_json::json!({ "nodes": ["read"] }).to_string()),
2567 rate_limit_max: Some(1.0),
2568 rate_limit_time_window: Some(60_000.0),
2569 ..Default::default()
2570 },
2571 )
2572 .await;
2573 let denied = serde_json::json!({ "nodes": ["delete"] });
2574 let allowed = serde_json::json!({ "nodes": ["read"] });
2575 let input = VerifyApiKey {
2576 key: &key,
2577 config_id: None,
2578 permissions: Some(&denied),
2579 };
2580 let error = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2581 assert!(matches!(
2582 error,
2583 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2584 code: ApiKeyErrorCode::KeyNotFound,
2585 ..
2586 })
2587 ));
2588 assert_eq!(
2589 ctx.database
2590 .get_api_key_by_id(&id)
2591 .await
2592 .unwrap()
2593 .unwrap()
2594 .remaining,
2595 Some(3.0)
2596 );
2597 let input = VerifyApiKey {
2598 permissions: Some(&allowed),
2599 ..input
2600 };
2601 assert_eq!(
2602 plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
2603 Some(2.0)
2604 );
2605 let error_2 = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2606 let ApiKeyVerificationError::Validation(error_2_3) = error_2 else {
2607 panic!("Expected rate limit rejection")
2608 };
2609 assert_eq!(error_2_3.code, ApiKeyErrorCode::RateLimited);
2610 let retry = error_2_3.details.unwrap().try_again_in;
2611 assert!(retry > 0.0 && retry <= 60_000.0);
2612 assert_eq!(
2613 ctx.database
2614 .get_api_key_by_id(&id)
2615 .await
2616 .unwrap()
2617 .unwrap()
2618 .remaining,
2619 Some(1.0)
2620 );
2621 }
2622
2623 #[tokio::test]
2624 async fn session_header_selects_a_named_config_without_a_default() {
2625 let plugin = ApiKeyPlugin::builder()
2626 .config_id("machines".to_owned())
2627 .api_key_headers(vec![
2628 "x-machine-key".to_owned(),
2629 "x-alternate-key".to_owned(),
2630 ])
2631 .enable_session_for_api_keys(true)
2632 .build();
2633 let (ctx, user, session) = create_test_context_with_user().await;
2634 let (_, key) = create_key_and_get_raw(
2635 &plugin,
2636 &ctx,
2637 &session.token,
2638 serde_json::json!({ "configId": "machines" }),
2639 )
2640 .await;
2641 let mut request =
2642 create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2643 request
2644 .headers
2645 .insert("x-alternate-key".to_owned(), key.clone());
2646 let result = plugin
2647 .before_request(&request, &ctx)
2648 .await
2649 .unwrap()
2650 .unwrap();
2651 let BeforeRequestAction::Respond(response) = result else {
2652 panic!("Expected virtual session")
2653 };
2654 assert_eq!(response.status, 200);
2655 let body = json_body(&response);
2656 assert_eq!(
2657 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2658 .get("token")
2659 .unwrap_or(&serde_json::Value::Null)),
2660 key
2661 );
2662 assert_eq!(
2663 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2664 .get("userId")
2665 .unwrap_or(&serde_json::Value::Null)),
2666 user.id
2667 );
2668 assert_eq!(
2669 (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
2670 .get("emailVerified")
2671 .unwrap_or(&serde_json::Value::Null)),
2672 false
2673 );
2674 assert!(
2675 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2676 .get("createdAt")
2677 .unwrap_or(&serde_json::Value::Null))
2678 .is_string()
2679 );
2680 assert!(
2681 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2682 .get("expiresAt")
2683 .unwrap_or(&serde_json::Value::Null))
2684 .is_string()
2685 );
2686 }
2687
2688 #[tokio::test]
2689 async fn session_header_cannot_authenticate_a_different_configuration() {
2690 let plugin = ApiKeyPlugin::builder()
2691 .enable_session_for_api_keys(true)
2692 .build()
2693 .configuration(ApiKeyConfig {
2694 config_id: "machines".to_owned(),
2695 api_key_headers: vec!["x-machine-key".to_owned()],
2696 enable_session_for_api_keys: true,
2697 ..Default::default()
2698 });
2699 let (ctx, _, session) = create_test_context_with_user().await;
2700 let (id, key) = create_key_and_get_raw(
2701 &plugin,
2702 &ctx,
2703 &session.token,
2704 serde_json::json!({ "configId": "machines" }),
2705 )
2706 .await;
2707 let mut request =
2708 create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2709 request.headers.insert("x-api-key".to_owned(), key);
2710 let result = plugin
2711 .before_request(&request, &ctx)
2712 .await
2713 .unwrap()
2714 .unwrap();
2715 let BeforeRequestAction::Respond(response) = result else {
2716 panic!("Expected credential rejection")
2717 };
2718 assert_eq!(response.status, 401);
2719 assert_eq!(
2720 (*(json_body(&response))
2721 .get("code")
2722 .unwrap_or(&serde_json::Value::Null)),
2723 "INVALID_API_KEY"
2724 );
2725 assert_eq!(
2726 ctx.database
2727 .get_api_key_by_id(&id)
2728 .await
2729 .unwrap()
2730 .unwrap()
2731 .request_count,
2732 Some(0.0)
2733 );
2734 }
2735
2736 #[tokio::test]
2737 async fn organization_key_verifies_without_emulating_a_user_session() {
2738 let plugin = ApiKeyPlugin::builder()
2739 .references(ApiKeyReferences::Organization)
2740 .enable_session_for_api_keys(true)
2741 .build();
2742 let (ctx, user, _) = create_test_context_with_user().await;
2743 let (key, key_hash, start) =
2744 ApiKeyPlugin::generate_key(&ApiKeyConfig::default(), None).unwrap();
2745 ctx.database
2746 .create_api_key(alibi_core::CreateApiKey {
2747 reference_id: user.id,
2749 config_id: "default".to_owned(),
2750 key_hash,
2751 start: Some(start),
2752 enabled: true,
2753 name: None,
2754 prefix: None,
2755 expires_at: None,
2756 remaining: None,
2757 rate_limit_enabled: false,
2758 rate_limit_time_window: None,
2759 rate_limit_max: None,
2760 refill_interval: None,
2761 refill_amount: None,
2762 permissions: None,
2763 metadata: None,
2764 })
2765 .await
2766 .unwrap();
2767 plugin
2768 .verify_api_key(
2769 &VerifyApiKey {
2770 key: &key,
2771 config_id: None,
2772 permissions: None,
2773 },
2774 &ctx,
2775 )
2776 .await
2777 .unwrap();
2778 let mut request =
2779 create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2780 request.headers.insert("x-api-key".to_owned(), key);
2781 let result = plugin
2782 .before_request(&request, &ctx)
2783 .await
2784 .unwrap()
2785 .unwrap();
2786 let BeforeRequestAction::Respond(response) = result else {
2787 panic!("Expected organization rejection")
2788 };
2789 assert_eq!(response.status, 401);
2790 assert_eq!(
2791 (*(json_body(&response))
2792 .get("code")
2793 .unwrap_or(&serde_json::Value::Null)),
2794 "INVALID_REFERENCE_ID_FROM_API_KEY"
2795 );
2796 }
2797
2798 #[tokio::test]
2799 async fn api_key_initialization_rejects_ambiguous_configurations() {
2800 let (ctx, _, _) = create_test_context_with_user().await;
2801 for config_id in ["", "default"] {
2802 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2803 config_id: config_id.to_owned(),
2804 ..Default::default()
2805 });
2806 let mut init = alibi_core::AuthInitContext::new(
2807 Arc::clone(&ctx.config),
2808 Arc::clone(&ctx.database),
2809 );
2810 assert!(matches!(
2811 plugin.on_init(&mut init).await,
2812 Err(AuthError::Config(_))
2813 ));
2814 }
2815 }
2816
2817 #[tokio::test]
2818 async fn verification_preserves_database_failures() {
2819 let connection = alibi_seaorm::Database::connect("sqlite::memory:")
2820 .await
2821 .unwrap();
2822 connection.clone().close().await.unwrap();
2823 let config = Arc::new(crate::plugins::test_helpers::create_test_config());
2824 let database = Arc::new(alibi_seaorm::SeaOrmStore::<TestSchema>::new(
2825 Arc::clone(&config),
2826 connection,
2827 ));
2828 let ctx = AuthContext::new(config, database);
2829 let plugin = ApiKeyPlugin::builder().build();
2830 let error = plugin
2831 .verify_api_key(
2832 &VerifyApiKey {
2833 key: "not-looked-up",
2834 config_id: None,
2835 permissions: None,
2836 },
2837 &ctx,
2838 )
2839 .await
2840 .unwrap_err();
2841 assert!(matches!(
2842 error,
2843 ApiKeyVerificationError::Internal(AuthError::Database(_))
2844 ));
2845 }
2846
2847 #[tokio::test]
2848 async fn verified_session_authenticates_a_protected_plugin_route_without_a_database_session()
2849 {
2850 let plugin = ApiKeyPlugin::builder()
2851 .enable_session_for_api_keys(true)
2852 .build();
2853 let (ctx, user, session) = create_test_context_with_user().await;
2854 let (id, key) =
2855 create_key_and_get_raw(&plugin, &ctx, &session.token, serde_json::json!({})).await;
2856 let mut request =
2857 create_auth_request(HttpMethod::Get, "/api-key/list", None, None, None);
2858 request.headers.insert("x-api-key".to_owned(), key.clone());
2859 let action = plugin
2860 .before_request(&request, &ctx)
2861 .await
2862 .unwrap()
2863 .unwrap();
2864 let BeforeRequestAction::InjectSession { session: session_2 } = action else {
2865 panic!("Expected virtual session")
2866 };
2867 assert_eq!(session_2.token, key);
2868 request.set_virtual_session(session_2);
2869 let response = plugin.on_request(&request, &ctx).await.unwrap().unwrap();
2870 assert_eq!(response.status, 200);
2871 assert_eq!(
2872 (*(*(*(json_body(&response))
2873 .get("apiKeys")
2874 .unwrap_or(&serde_json::Value::Null))
2875 .get(0)
2876 .unwrap_or(&serde_json::Value::Null))
2877 .get("id")
2878 .unwrap_or(&serde_json::Value::Null)),
2879 id
2880 );
2881 assert_eq!(
2882 ctx.database
2883 .get_user_sessions(&user.id)
2884 .await
2885 .unwrap()
2886 .len(),
2887 1
2888 );
2889 }
2890
2891 #[tokio::test]
2895 async fn programmatic_validator_uses_typed_policy_without_a_request() {
2896 use std::sync::atomic::{AtomicBool, Ordering};
2897 struct Policy(AtomicBool);
2898 struct Predicate {
2899 private_policy_secret: String,
2900 }
2901 #[async_trait::async_trait]
2902 impl ApiKeyValidator for Predicate {
2903 async fn validate(
2904 &self,
2905 context: &ApiKeyCallbackContext<'_>,
2906 _key: &str,
2907 ) -> AuthResult<bool> {
2908 Ok(context.request.is_none()
2909 && context.configuration_id == "programmatic"
2910 && context.auth_config.secret == "test-secret-key-at-least-32-chars-long"
2911 && self.private_policy_secret == "application-private-policy-secret"
2912 && context
2913 .extensions
2914 .get::<Policy>()
2915 .is_some_and(|policy| policy.0.load(Ordering::SeqCst)))
2916 }
2917 }
2918 let (mut ctx, user, _) = create_test_context_with_user().await;
2919 ctx.extensions.insert(Policy(AtomicBool::new(false)));
2920 let configuration = ApiKeyConfig {
2921 config_id: "programmatic".to_owned(),
2922 custom_api_key_validator: Some(Arc::new(Predicate {
2923 private_policy_secret: "application-private-policy-secret".to_owned(),
2924 })),
2925 rate_limit: RateLimitDefaults {
2926 enabled: false,
2927 ..Default::default()
2928 },
2929 ..Default::default()
2930 };
2931 assert!(!format!("{configuration:?}").contains("application-private-policy-secret"));
2932 let plugin = ApiKeyPlugin::with_config(configuration);
2933 let created = plugin
2934 .create_key(
2935 &ctx,
2936 &CreateKeyRequest {
2937 config_id: Some("programmatic".to_owned()),
2938 user_id: Some(user.id.clone()),
2939 remaining: Some(2.0),
2940 ..Default::default()
2941 },
2942 )
2943 .await
2944 .unwrap();
2945 let input = VerifyApiKey {
2946 key: &created.key,
2947 config_id: Some("programmatic"),
2948 permissions: None,
2949 };
2950 let rejected = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2951 assert!(matches!(
2952 rejected,
2953 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2954 code: ApiKeyErrorCode::KeyNotFound,
2955 ..
2956 })
2957 ));
2958 assert_eq!(
2959 ctx.database
2960 .get_api_key_by_id(&created.api_key.id)
2961 .await
2962 .unwrap()
2963 .unwrap()
2964 .remaining,
2965 Some(2.0)
2966 );
2967 ctx.extensions
2968 .get::<Policy>()
2969 .unwrap()
2970 .0
2971 .store(true, Ordering::SeqCst);
2972 let accepted = plugin.verify_api_key(&input, &ctx).await.unwrap();
2973 assert_eq!(accepted.reference_id, user.id);
2974 assert_eq!(accepted.id, created.api_key.id);
2975 assert_eq!(accepted.remaining, Some(1.0));
2976 assert_eq!(
2977 ctx.database
2978 .get_api_key_by_id(&created.api_key.id)
2979 .await
2980 .unwrap()
2981 .unwrap()
2982 .remaining,
2983 Some(1.0)
2984 );
2985 }
2986 }
2987}
2988#[cfg(test)]
2992mod crud_tests {
2993 use super::*;
2994 use alibi_core::{AuthConfig, CreateSession, CreateUser, HttpMethod};
2995 use chrono::{Duration, Utc};
2996 use serde_json::json;
2997 use std::collections::HashMap;
2998 use std::sync::Arc;
2999
3000 type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
3001
3002 async fn context() -> (AuthContext<TestSchema>, String, String) {
3003 let database = crate::plugins::test_helpers::create_test_database().await;
3004 let user = database
3005 .create_user(
3006 CreateUser::new()
3007 .with_email("crud@example.com")
3008 .with_name("CRUD user"),
3009 )
3010 .await
3011 .unwrap();
3012 let user_id = user.id().to_string();
3013 let session = database
3014 .create_session(CreateSession {
3015 additional_fields: alibi_core::field_policy::FieldValues::default(),
3016 token: None,
3017 active_team_id: None,
3018 user_id: user_id.clone(),
3019 expires_at: Utc::now() + Duration::hours(1),
3020 ip_address: None,
3021 user_agent: None,
3022 impersonated_by: None,
3023 active_organization_id: None,
3024 })
3025 .await
3026 .unwrap();
3027 let token = alibi_core::entity::AuthSession::token(&session).to_owned();
3028 (
3029 AuthContext::new(
3030 Arc::new(AuthConfig::new("a-secret-that-is-at-least-32-characters")),
3031 database,
3032 ),
3033 user_id,
3034 token,
3035 )
3036 }
3037
3038 fn request(token: &str, path: &str, body: &serde_json::Value) -> AuthRequest {
3039 AuthRequest::from_parts(
3040 HttpMethod::Post,
3041 path.to_owned(),
3042 HashMap::from([(
3043 "cookie".to_owned(),
3044 format!(
3045 "better-auth.session_token={}",
3046 alibi_core::utils::cookie_utils::sign_cookie_value(
3047 token,
3048 "a-secret-that-is-at-least-32-characters"
3049 )
3050 ),
3051 )]),
3052 Some(serde_json::to_vec(&body).unwrap()),
3053 HashMap::new(),
3054 )
3055 }
3056
3057 async fn server_key(
3058 plugin: &ApiKeyPlugin,
3059 ctx: &AuthContext<TestSchema>,
3060 user_id: &str,
3061 config_id: &str,
3062 ) -> CreateKeyResponse {
3063 plugin
3064 .create_key(
3065 ctx,
3066 &CreateKeyRequest {
3067 user_id: Some(user_id.to_owned()),
3068 config_id: Some(config_id.to_owned()),
3069 ..Default::default()
3070 },
3071 )
3072 .await
3073 .unwrap()
3074 }
3075
3076 #[tokio::test]
3077 async fn list_without_configuration_includes_all_user_configurations() {
3078 let (ctx, user_id, _) = context().await;
3079 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
3080 config_id: "secondary".to_owned(),
3081 ..Default::default()
3082 });
3083 server_key(&plugin, &ctx, &user_id, "default").await;
3084 server_key(&plugin, &ctx, &user_id, "secondary").await;
3085 let list = list_keys_core(&user_id, &ListKeysQuery::default(), &plugin, &ctx)
3086 .await
3087 .unwrap();
3088 assert_eq!(list.total, 2);
3089 let list_2 = list_keys_core(
3090 &user_id,
3091 &ListKeysQuery {
3092 config_id: Some("secondary".to_owned()),
3093 ..Default::default()
3094 },
3095 &plugin,
3096 &ctx,
3097 )
3098 .await
3099 .unwrap();
3100 assert_eq!(list_2.total, 1);
3101 assert_eq!(
3102 (list_2.api_keys)
3103 .first()
3104 .expect("fixture contains the requested index")
3105 .config_id,
3106 "secondary"
3107 );
3108 let list_3 = list_keys_core(
3109 &user_id,
3110 &ListKeysQuery {
3111 config_id: Some("missing".to_owned()),
3112 ..Default::default()
3113 },
3114 &plugin,
3115 &ctx,
3116 )
3117 .await
3118 .unwrap();
3119 assert_eq!(list_3.total, 0);
3120
3121 let no_default = ApiKeyPlugin::builder()
3122 .config_id("secondary".to_owned())
3123 .build();
3124 assert_eq!(
3125 list_keys_core(&user_id, &ListKeysQuery::default(), &no_default, &ctx)
3126 .await
3127 .unwrap()
3128 .total,
3129 2
3130 );
3131 }
3132
3133 #[tokio::test]
3134 async fn http_requests_cannot_impersonate_users_or_change_server_permissions() {
3135 let (ctx, user_id, token) = context().await;
3136 let plugin = ApiKeyPlugin::builder().build();
3137 let create = request(&token, "/api-key/create", &(json!({"userId":user_id})));
3138 assert_eq!(
3139 plugin
3140 .handle_create(&create, &ctx)
3141 .await
3142 .unwrap_err()
3143 .status_code(),
3144 401
3145 );
3146 let key = server_key(&plugin, &ctx, &user_id, "default").await;
3147 let update = request(
3148 &token,
3149 "/api-key/update",
3150 &(json!({"keyId":key.api_key.id,"userId":"someone-else","enabled":false})),
3151 );
3152 assert_eq!(
3153 plugin
3154 .handle_update(&update, &ctx)
3155 .await
3156 .unwrap_err()
3157 .status_code(),
3158 401
3159 );
3160 let update_2 = request(
3161 &token,
3162 "/api-key/update",
3163 &(json!({"keyId":key.api_key.id,"permissions":null})),
3164 );
3165 assert_eq!(
3166 plugin
3167 .handle_update(&update_2, &ctx)
3168 .await
3169 .unwrap_err()
3170 .to_string(),
3171 ApiKeyErrorCode::ServerOnlyProperty.message()
3172 );
3173 let update_3 = request(&token, "/api-key/update", &(json!({"keyId":"missing"})));
3174 assert_eq!(
3175 plugin
3176 .handle_update(&update_3, &ctx)
3177 .await
3178 .unwrap_err()
3179 .status_code(),
3180 404
3181 );
3182 }
3183
3184 #[tokio::test]
3185 async fn metadata_can_be_cleared_and_disabled_metadata_is_ignored_on_update() {
3186 let (ctx, user_id, token) = context().await;
3187 let enabled = ApiKeyPlugin::builder().enable_metadata(true).build();
3188 let key = enabled
3189 .create_key(
3190 &ctx,
3191 &CreateKeyRequest {
3192 user_id: Some(user_id.clone()),
3193 metadata: Some(json!(["initial"]).into()),
3194 ..Default::default()
3195 },
3196 )
3197 .await
3198 .unwrap();
3199 assert_eq!(key.api_key.metadata, Some(json!(["initial"])));
3200 let update = request(
3201 &token,
3202 "/api-key/update",
3203 &(json!({"keyId":key.api_key.id,"metadata":null})),
3204 );
3205 let response = enabled.handle_update(&update, &ctx).await.unwrap();
3206 assert!(
3207 (*(serde_json::from_slice::<serde_json::Value>(&response.body).unwrap())
3208 .get("metadata")
3209 .unwrap_or(&serde_json::Value::Null))
3210 .is_null()
3211 );
3212 let disabled = ApiKeyPlugin::builder().build();
3213 let update_2 = request(
3214 &token,
3215 "/api-key/update",
3216 &(json!({"keyId":key.api_key.id,"metadata":{"ignored":true},"enabled":false})),
3217 );
3218 let response_2 = disabled.handle_update(&update_2, &ctx).await.unwrap();
3219 let result: serde_json::Value = serde_json::from_slice(&response_2.body).unwrap();
3220 assert!((*(result).get("metadata").unwrap_or(&serde_json::Value::Null)).is_null());
3221 assert_eq!(
3222 (*(result).get("enabled").unwrap_or(&serde_json::Value::Null)),
3223 false
3224 );
3225 let update_3 = request(
3226 &token,
3227 "/api-key/update",
3228 &(json!({"keyId":key.api_key.id,"metadata":null})),
3229 );
3230 assert_eq!(
3231 disabled
3232 .handle_update(&update_3, &ctx)
3233 .await
3234 .unwrap_err()
3235 .to_string(),
3236 ApiKeyErrorCode::NoValuesToUpdate.message()
3237 );
3238 }
3239
3240 #[tokio::test]
3241 async fn trusted_creation_and_update_preserve_permissions_and_fractional_expiration() {
3242 let (ctx, user_id, _) = context().await;
3243 let plugin = ApiKeyPlugin::builder()
3244 .key_expiration(KeyExpirationConfig {
3245 min_expires_in: 0.0,
3246 ..Default::default()
3247 })
3248 .build();
3249 let before = Utc::now();
3250 let key = plugin
3251 .create_key(
3252 &ctx,
3253 &CreateKeyRequest {
3254 user_id: Some(user_id.clone()),
3255 expires_in: Some(86400.25),
3256 remaining: Some(3.0),
3257 permissions: Some(ApiKeyPermissions::from([(
3258 "device".to_owned(),
3259 vec!["read".to_owned()],
3260 )])),
3261 ..Default::default()
3262 },
3263 )
3264 .await
3265 .unwrap();
3266 assert_eq!(key.api_key.remaining, Some(3.0));
3267 assert_eq!(key.api_key.permissions, Some(json!({"device":["read"]})));
3268 let expires_at =
3269 chrono::DateTime::parse_from_rfc3339(key.api_key.expires_at.as_deref().unwrap())
3270 .unwrap()
3271 .with_timezone(&Utc);
3272 assert!((expires_at - before).num_milliseconds() >= 86_400_249);
3273 assert!((expires_at - Utc::now()).num_milliseconds() <= 86_400_250);
3274 let updated = plugin
3275 .update_key(
3276 &ctx,
3277 &UpdateKeyRequest {
3278 key_id: key.api_key.id.clone(),
3279 user_id: Some(user_id),
3280 remaining: Some(5.0),
3281 permissions: Some(None),
3282 expires_in: Some(None),
3283 ..Default::default()
3284 },
3285 )
3286 .await
3287 .unwrap();
3288 assert_eq!(updated.remaining, Some(5.0));
3289 assert_eq!(updated.permissions, Some(serde_json::Value::Null));
3290 assert_eq!(updated.expires_at, None);
3291 let stored = ctx
3292 .database
3293 .get_api_key_by_id(&key.api_key.id)
3294 .await
3295 .unwrap()
3296 .unwrap();
3297 assert_ne!(stored.key_hash, key.key);
3298 }
3299
3300 #[tokio::test]
3301 async fn list_rejects_invalid_pagination_instead_of_ignoring_it() {
3302 let (ctx, _, token) = context().await;
3303 let plugin = ApiKeyPlugin::builder().build();
3304 for (field, value) in [
3305 ("limit", "-1"),
3306 ("offset", "1.5"),
3307 ("limit", "bad"),
3308 ("sortDirection", "sideways"),
3309 ] {
3310 let mut req = request(&token, "/api-key/list", &(json!({})));
3311 req.query.insert(field.to_owned(), value.to_owned());
3312 let response = plugin.handle_list(&req, &ctx).await.unwrap();
3313 assert_eq!(response.status, 400);
3314 assert_eq!(
3315 (*(serde_json::from_slice::<serde_json::Value>(&response.body).unwrap())
3316 .get("code")
3317 .unwrap_or(&serde_json::Value::Null)),
3318 "VALIDATION_ERROR"
3319 );
3320 }
3321 }
3322
3323 #[tokio::test]
3324 async fn forced_cleanup_preserves_rows_on_store_failure_and_retries_without_throttle() {
3325 use alibi_seaorm::sea_orm::{ConnectionTrait, DbBackend, Statement};
3326 let config = Arc::new(AuthConfig::new("forced-cleanup-application-secret32"));
3327 let connection = alibi_seaorm::Database::connect("sqlite::memory:")
3328 .await
3329 .unwrap();
3330 alibi_seaorm::store::__private_test_support::migrator::run_migrations(&connection)
3331 .await
3332 .unwrap();
3333 let database = Arc::new(alibi_seaorm::SeaOrmStore::<TestSchema>::new(
3334 std::sync::Arc::clone(&config),
3335 connection.clone(),
3336 ));
3337 let ctx = AuthContext::new(config, database);
3338 let owner = ctx
3339 .database
3340 .create_user(CreateUser::new().with_email("cleanup@native.local"))
3341 .await
3342 .unwrap();
3343 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3344 key_expiration: KeyExpirationConfig {
3345 min_expires_in: 0.0,
3346 ..Default::default()
3347 },
3348 ..Default::default()
3349 });
3350 let key = plugin
3351 .create_key(
3352 &ctx,
3353 &CreateKeyRequest {
3354 user_id: Some(owner.id().to_string()),
3355 name: Some("expired".into()),
3356 ..Default::default()
3357 },
3358 )
3359 .await
3360 .unwrap();
3361 let expired = ctx
3362 .database
3363 .update_api_key(
3364 &key.api_key.id,
3365 alibi_core::UpdateApiKey {
3366 expires_at: Some(Some("1970-01-01T00:00:00.000Z".into())),
3367 ..Default::default()
3368 },
3369 )
3370 .await
3371 .unwrap();
3372 connection.execute_raw(Statement::from_string(DbBackend::Sqlite,"CREATE TRIGGER reject_api_key_cleanup BEFORE DELETE ON api_keys BEGIN SELECT RAISE(ABORT,'application cleanup rejected'); END")).await.unwrap();
3373 let failed = plugin.delete_all_expired_api_keys(&ctx).await;
3374 assert!(failed.success);
3375 assert!(failed.error.is_none());
3376 let retained = ctx
3377 .database
3378 .get_api_key_by_id(&key.api_key.id)
3379 .await
3380 .unwrap()
3381 .unwrap();
3382 assert_eq!(
3383 alibi_core::utils::json::to_value(&retained).unwrap(),
3384 alibi_core::utils::json::to_value(&expired).unwrap()
3385 );
3386 connection
3387 .execute_raw(Statement::from_string(
3388 DbBackend::Sqlite,
3389 "DROP TRIGGER reject_api_key_cleanup",
3390 ))
3391 .await
3392 .unwrap();
3393 let retry = plugin.delete_all_expired_api_keys(&ctx).await;
3394 assert!(retry.success);
3395 assert!(retry.error.is_none());
3396 assert!(
3397 ctx.database
3398 .get_api_key_by_id(&key.api_key.id)
3399 .await
3400 .unwrap()
3401 .is_none()
3402 );
3403 assert_eq!(
3404 ctx.database
3405 .get_user_by_id(owner.id().as_ref())
3406 .await
3407 .unwrap()
3408 .unwrap(),
3409 owner
3410 );
3411 }
3412 struct ApplicationKeyStorage {
3413 cache: Arc<alibi_core::store::MemoryCacheAdapter>,
3414 fail_writes: std::sync::atomic::AtomicBool,
3415 }
3416 #[async_trait::async_trait]
3417 impl ApiKeyStorage for ApplicationKeyStorage {
3418 async fn get(&self, key: &str) -> AuthResult<Option<String>> {
3419 use alibi_core::store::CacheAdapter as _;
3420 self.cache.get(key).await
3421 }
3422 async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> AuthResult<()> {
3423 use alibi_core::store::CacheAdapter as _;
3424 if self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
3425 return Err(AuthError::internal("application storage unavailable"));
3426 }
3427 match ttl {
3428 Some(ttl) => self.cache.set(key, value, ttl).await,
3429 None => self.cache.set_without_expiry(key, value).await,
3430 }
3431 }
3432 async fn delete(&self, key: &str) -> AuthResult<()> {
3433 use alibi_core::store::CacheAdapter as _;
3434 self.cache.delete(key).await
3435 }
3436 }
3437 #[derive(Default)]
3438 struct ApplicationCompletions(std::sync::Mutex<Vec<alibi_core::BackgroundTaskCompletion>>);
3439 impl alibi_core::BackgroundTaskHandler for ApplicationCompletions {
3440 fn handle(&self, completion: alibi_core::BackgroundTaskCompletion) -> AuthResult<()> {
3441 self.0.lock().unwrap().push(completion);
3442 Ok(())
3443 }
3444 }
3445 impl ApplicationCompletions {
3446 async fn drain(&self) {
3447 let tasks = std::mem::take(&mut *self.0.lock().unwrap());
3448 for task in tasks {
3449 task.await.unwrap();
3450 }
3451 }
3452 }
3453
3454 #[tokio::test]
3455 async fn source_boxed_metadata_rows_list_successfully_without_mutating_storage() {
3456 use alibi_core::store::{CacheAdapter, MemoryCacheAdapter};
3457 let (ctx, owner, token) = context().await;
3458 let cache = Arc::new(MemoryCacheAdapter::new());
3459 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3460 storage: ApiKeyStorageMode::SecondaryStorage,
3461 secondary_storage: Some(cache.clone()),
3462 ..Default::default()
3463 });
3464 let mut rows = Vec::new();
3465 let mut expected = std::collections::BTreeMap::new();
3466 for index in 0..64 {
3471 let created = server_key(&plugin, &ctx, &owner, "default").await;
3472 let id = format!("api-key:by-id:{}", created.api_key.id);
3473 let hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3474 let mut row: serde_json::Value =
3475 serde_json::from_str(&cache.get(&id).await.unwrap().unwrap()).unwrap();
3476 *row.get_mut("metadata").unwrap() = match index % 6 {
3477 0 => json!("10"),
3478 1 => json!("2"),
3479 2 => json!(3),
3480 3 => json!(null),
3481 4 => json!("0"),
3482 _ => json!({"literal":"object"}),
3483 };
3484 let value = serde_json::to_string(&row).unwrap();
3485 cache.set_without_expiry(&id, &value).await.unwrap();
3486 cache.set_without_expiry(&hash, &value).await.unwrap();
3487 rows.push((id, value.clone()));
3488 rows.push((hash, value));
3489 expected.insert(
3490 created.api_key.id,
3491 [
3492 json!(10),
3493 json!(2),
3494 json!(3),
3495 json!(null),
3496 json!(0),
3497 json!({"literal":"object"}),
3498 ]
3499 .get(index % 6)
3500 .unwrap()
3501 .clone(),
3502 );
3503 }
3504 let reference = format!("api-key:by-ref:{owner}");
3505 let reference_before = cache.get(&reference).await.unwrap();
3506 for direction in ["asc", "desc"] {
3507 let mut req = request(&token, "/api-key/list", &json!(null));
3508 req.method = HttpMethod::Get;
3509 req.body = None;
3510 req.query = HashMap::from([
3511 ("sortBy".to_owned(), "metadata".to_owned()),
3512 ("sortDirection".to_owned(), direction.to_owned()),
3513 ]);
3514 let response = plugin.handle_list(&req, &ctx).await.unwrap();
3515 assert_eq!(response.status, 200);
3516 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3517 assert_eq!(body.get("total").unwrap(), 64);
3518 let actual: std::collections::BTreeMap<_, _> = body
3519 .get("apiKeys")
3520 .unwrap()
3521 .as_array()
3522 .unwrap()
3523 .iter()
3524 .map(|row| {
3525 (
3526 row.get("id").unwrap().as_str().unwrap().to_owned(),
3527 row.get("metadata").unwrap().clone(),
3528 )
3529 })
3530 .collect();
3531 assert_eq!(actual, expected);
3532 for (index, value) in &rows {
3533 assert_eq!(
3534 cache.get(index).await.unwrap().as_deref(),
3535 Some(value.as_str())
3536 );
3537 }
3538 assert_eq!(cache.get(&reference).await.unwrap(), reference_before);
3539 assert!(
3540 ctx.database
3541 .list_api_keys_by_reference(&owner)
3542 .await
3543 .unwrap()
3544 .is_empty()
3545 );
3546 }
3547 for _ in 64..96 {
3550 let created = server_key(&plugin, &ctx, &owner, "default").await;
3551 let id = format!("api-key:by-id:{}", created.api_key.id);
3552 let hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3553 let value = cache.get(&id).await.unwrap().unwrap();
3554 rows.push((id, value.clone()));
3555 rows.push((hash, value));
3556 }
3557 let mut ordered = std::collections::BTreeMap::new();
3558 for (index, pair) in rows.as_chunks_mut::<2>().0.iter_mut().enumerate() {
3559 let rank = (index * 37) % 96;
3560 let mut row: serde_json::Value =
3561 serde_json::from_str(&pair.first().unwrap().1).unwrap();
3562 drop(ordered.insert(rank, row.get("id").unwrap().clone()));
3563 *row.get_mut("metadata").unwrap() = json!(rank);
3564 let serialized = serde_json::to_string(&row).unwrap();
3565 for (key, value) in pair {
3566 cache.set_without_expiry(key, &serialized).await.unwrap();
3567 *value = serialized.clone();
3568 }
3569 }
3570 let reference_before = cache.get(&reference).await.unwrap();
3571 for direction in ["asc", "desc"] {
3572 let mut req = request(&token, "/api-key/list", &json!(null));
3573 req.method = HttpMethod::Get;
3574 req.body = None;
3575 req.query = HashMap::from([
3576 ("sortBy".into(), "metadata".into()),
3577 ("sortDirection".into(), direction.into()),
3578 ("offset".into(), "11".into()),
3579 ("limit".into(), "9".into()),
3580 ]);
3581 let response = plugin.handle_list(&req, &ctx).await.unwrap();
3582 assert_eq!(response.status, 200);
3583 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3584 assert_eq!(body.get("total").unwrap(), 96);
3585 let page = body.get("apiKeys").unwrap().as_array().unwrap();
3586 assert_eq!(page.len(), 9);
3587 for (offset, row) in page.iter().enumerate() {
3588 let rank = if direction == "asc" {
3589 11 + offset
3590 } else {
3591 84 - offset
3592 };
3593 assert_eq!(row.get("id").unwrap(), ordered.get(&rank).unwrap());
3594 assert_eq!(row.get("metadata").unwrap(), &json!(rank));
3595 }
3596 for (key, value) in &rows {
3597 assert_eq!(
3598 cache.get(key).await.unwrap().as_deref(),
3599 Some(value.as_str())
3600 );
3601 }
3602 assert_eq!(cache.get(&reference).await.unwrap(), reference_before);
3603 }
3604 }
3605
3606 #[tokio::test]
3607 async fn application_storage_preserves_authority_indexes_and_usage() {
3608 use alibi_core::store::{CacheAdapter, MemoryCacheAdapter};
3609 for (fallback, custom, deferred) in [
3612 (false, false, false),
3613 (true, false, false),
3614 (false, true, false),
3615 (true, true, false),
3616 (false, true, true),
3617 (true, true, true),
3618 ] {
3619 let (mut ctx, owner, token) = context().await;
3620 let completions = Arc::new(ApplicationCompletions::default());
3621 Arc::make_mut(&mut ctx.config).background_tasks = Some(completions.clone());
3622 let outsider = ctx
3623 .database
3624 .create_user(
3625 CreateUser::new()
3626 .with_email("outsider@example.com")
3627 .with_name("outsider"),
3628 )
3629 .await
3630 .unwrap();
3631 let cache = Arc::new(MemoryCacheAdapter::new());
3632 let decoy = Arc::new(MemoryCacheAdapter::new());
3633 let application = Arc::new(ApplicationKeyStorage {
3634 cache: cache.clone(),
3635 fail_writes: std::sync::atomic::AtomicBool::new(false),
3636 });
3637 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3638 storage: ApiKeyStorageMode::SecondaryStorage,
3639 secondary_storage: Some(
3640 if custom { decoy.clone() } else { cache.clone() } as Arc<dyn CacheAdapter>
3641 ),
3642 custom_storage: custom.then(|| application.clone() as Arc<dyn ApiKeyStorage>),
3643 fallback_to_database: fallback,
3644 defer_updates: deferred,
3645 rate_limit: RateLimitDefaults {
3646 enabled: false,
3647 ..Default::default()
3648 },
3649 ..Default::default()
3650 });
3651 let response = plugin
3652 .handle_create(
3653 &request(&token, "/api-key/create", &json!({"name":"stored"})),
3654 &ctx,
3655 )
3656 .await
3657 .unwrap();
3658 let created: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3659 let id = created.get("id").unwrap().as_str().unwrap();
3660 let raw = created.get("key").unwrap().as_str().unwrap();
3661 plugin
3662 .update_key(
3663 &ctx,
3664 &UpdateKeyRequest {
3665 key_id: id.to_owned(),
3666 user_id: Some(owner.clone()),
3667 remaining: Some(2.0),
3668 ..Default::default()
3669 },
3670 )
3671 .await
3672 .unwrap();
3673 let hash = ApiKeyPlugin::hash_key(raw);
3674 let by_id = format!("api-key:by-id:{id}");
3675 let by_hash = format!("api-key:{hash}");
3676 let by_ref = format!("api-key:by-ref:{owner}");
3677 let initial = cache.get(&by_id).await.unwrap().unwrap();
3678 assert_eq!(cache.get(&by_hash).await.unwrap(), Some(initial.clone()));
3679 assert_eq!(
3680 ctx.database.get_api_key_by_id(id).await.unwrap().is_some(),
3681 fallback
3682 );
3683 assert_eq!(cache.get(&by_ref).await.unwrap().is_none(), fallback);
3684 assert!(
3685 get_key_core(id, None, &outsider.id().to_string(), &plugin, &ctx)
3686 .await
3687 .is_err()
3688 );
3689 assert_eq!(cache.get(&by_id).await.unwrap(), Some(initial.clone()));
3690 assert!(
3691 plugin
3692 .verify_api_key(
3693 &VerifyApiKey {
3694 key: raw,
3695 config_id: Some("wrong"),
3696 permissions: None
3697 },
3698 &ctx
3699 )
3700 .await
3701 .is_err()
3702 );
3703 assert_eq!(cache.get(&by_id).await.unwrap(), Some(initial));
3704 let list = list_keys_core(&owner, &ListKeysQuery::default(), &plugin, &ctx)
3705 .await
3706 .unwrap();
3707 assert_eq!(list.total, 1);
3708 let input = VerifyApiKey {
3709 key: raw,
3710 config_id: None,
3711 permissions: None,
3712 };
3713 assert_eq!(
3714 plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
3715 Some(1.0)
3716 );
3717 completions.drain().await;
3718 assert!(decoy.get(&by_id).await.unwrap().is_none());
3719 let row: alibi_core::ApiKey =
3720 serde_json::from_str(&cache.get(&by_id).await.unwrap().unwrap()).unwrap();
3721 assert_eq!(row.remaining, Some(1.0));
3722 assert_eq!(row.reference_id, owner);
3723 assert_eq!(
3724 cache.get(&by_hash).await.unwrap(),
3725 cache.get(&by_id).await.unwrap()
3726 );
3727 if fallback {
3728 let persisted = ctx.database.get_api_key_by_id(id).await.unwrap().unwrap();
3729 let mut persisted_json = serde_json::to_value(&persisted).unwrap();
3730 *persisted_json.get_mut("metadata").unwrap() =
3733 serde_json::from_str(persisted.metadata.as_deref().unwrap_or("null")).unwrap();
3734 assert_eq!(persisted_json, serde_json::to_value(&row).unwrap());
3735 cache.delete(&by_id).await.unwrap();
3736 cache.delete(&by_hash).await.unwrap();
3737 assert_eq!(
3738 get_key_core(id, None, &owner, &plugin, &ctx)
3739 .await
3740 .unwrap()
3741 .remaining,
3742 Some(1.0)
3743 );
3744 assert_eq!(
3745 cache.get(&by_hash).await.unwrap(),
3746 cache.get(&by_id).await.unwrap()
3747 );
3748 }
3749 assert_eq!(
3750 plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
3751 Some(0.0)
3752 );
3753 completions.drain().await;
3754 assert!(plugin.verify_api_key(&input, &ctx).await.is_err());
3755 completions.drain().await;
3756 assert!(cache.get(&by_id).await.unwrap().is_none());
3757 assert!(cache.get(&by_hash).await.unwrap().is_none());
3758 assert!(cache.get(&by_ref).await.unwrap().is_none());
3759 assert!(ctx.database.get_api_key_by_id(id).await.unwrap().is_none());
3760 if custom {
3761 let target = plugin
3762 .create_key(
3763 &ctx,
3764 &CreateKeyRequest {
3765 user_id: Some(owner.clone()),
3766 remaining: Some(4.0),
3767 ..Default::default()
3768 },
3769 )
3770 .await
3771 .unwrap();
3772 let raw = target.key.as_str();
3773 let key_id = &target.api_key.id;
3774 let id_index = format!("api-key:by-id:{key_id}");
3775 let hash_index = format!("api-key:{}", ApiKeyPlugin::hash_key(raw));
3776 let before = cache.get(&id_index).await.unwrap();
3777 application
3778 .fail_writes
3779 .store(true, std::sync::atomic::Ordering::SeqCst);
3780 let input = VerifyApiKey {
3781 key: raw,
3782 config_id: None,
3783 permissions: None,
3784 };
3785 let failed = plugin.verify_api_key(&input, &ctx).await;
3786 assert_eq!(failed.is_ok(), deferred && !fallback);
3787 completions.drain().await;
3788 assert_eq!(cache.get(&id_index).await.unwrap(), before);
3789 assert_eq!(cache.get(&hash_index).await.unwrap(), before);
3790 if fallback {
3791 assert_eq!(
3792 ctx.database
3793 .get_api_key_by_id(key_id)
3794 .await
3795 .unwrap()
3796 .unwrap()
3797 .remaining,
3798 Some(3.0)
3799 );
3800 } else {
3801 assert!(
3802 ctx.database
3803 .get_api_key_by_id(key_id)
3804 .await
3805 .unwrap()
3806 .is_none()
3807 );
3808 }
3809 application
3810 .fail_writes
3811 .store(false, std::sync::atomic::Ordering::SeqCst);
3812 let retried = plugin.verify_api_key(&input, &ctx).await.unwrap();
3813 assert_eq!(retried.remaining, Some(if fallback { 2.0 } else { 3.0 }));
3814 completions.drain().await;
3815 assert_eq!(
3816 cache.get(&hash_index).await.unwrap(),
3817 cache.get(&id_index).await.unwrap()
3818 );
3819 let row: alibi_core::ApiKey =
3820 serde_json::from_str(&cache.get(&id_index).await.unwrap().unwrap()).unwrap();
3821 assert_eq!(row.remaining, retried.remaining);
3822 }
3823 }
3824 let (ctx, owner, _) = context().await;
3827 let cache = Arc::new(MemoryCacheAdapter::new());
3828 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3829 storage: ApiKeyStorageMode::SecondaryStorage,
3830 secondary_storage: Some(cache.clone()),
3831 ..Default::default()
3832 });
3833 let created = plugin
3834 .create_key(
3835 &ctx,
3836 &CreateKeyRequest {
3837 user_id: Some(owner.clone()),
3838 remaining: Some(2.0),
3839 rate_limit_enabled: Some(true),
3840 rate_limit_time_window: Some(60_000.0),
3841 rate_limit_max: Some(1.0),
3842 refill_interval: Some(60_000.0),
3843 refill_amount: Some(5.0),
3844 ..Default::default()
3845 },
3846 )
3847 .await
3848 .unwrap();
3849 let by_id = format!("api-key:by-id:{}", created.api_key.id);
3850 let by_hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3851 let input = VerifyApiKey {
3852 key: &created.key,
3853 config_id: None,
3854 permissions: None,
3855 };
3856 let first = plugin.verify_api_key(&input, &ctx).await.unwrap();
3857 assert_eq!(first.remaining, Some(1.0));
3858 assert_eq!(first.request_count, Some(1.0));
3859 let before = cache.get(&by_id).await.unwrap();
3860 assert_eq!(cache.get(&by_hash).await.unwrap(), before);
3861 let denied = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
3862 let ApiKeyVerificationError::Validation(denied) = denied else {
3863 panic!("Expected rate-limit validation");
3864 };
3865 assert_eq!(denied.code, ApiKeyErrorCode::RateLimited);
3866 assert!(denied.details.unwrap().try_again_in > 0.0);
3867 assert_eq!(cache.get(&by_id).await.unwrap(), before);
3868 assert_eq!(cache.get(&by_hash).await.unwrap(), before);
3869 let mut elapsed: alibi_core::ApiKey =
3870 serde_json::from_str(before.as_deref().unwrap()).unwrap();
3871 let previous = (chrono::Utc::now() - chrono::Duration::minutes(2)).to_rfc3339();
3872 elapsed.last_request = Some(previous.clone());
3873 elapsed.last_refill_at = Some(previous.clone());
3874 let seeded = serde_json::to_string(&elapsed).unwrap();
3875 cache.set_without_expiry(&by_id, &seeded).await.unwrap();
3876 cache.set_without_expiry(&by_hash, &seeded).await.unwrap();
3877 let refilled = plugin.verify_api_key(&input, &ctx).await.unwrap();
3878 assert_eq!(refilled.remaining, Some(4.0));
3879 assert_eq!(refilled.request_count, Some(1.0));
3880 assert_ne!(refilled.last_refill_at.as_deref(), Some(previous.as_str()));
3881 let observed: alibi_core::ApiKey =
3882 serde_json::from_str(&cache.get(&by_id).await.unwrap().unwrap()).unwrap();
3883 assert_eq!(observed.remaining, Some(4.0));
3884 assert_eq!(observed.request_count, Some(1.0));
3885 assert_eq!(
3886 cache.get(&by_id).await.unwrap(),
3887 cache.get(&by_hash).await.unwrap()
3888 );
3889 assert!(
3890 ctx.database
3891 .list_api_keys_by_reference(&owner)
3892 .await
3893 .unwrap()
3894 .is_empty()
3895 );
3896 }
3897}
3898