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