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