use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rand::seq::SliceRandom;
use sha2::{Digest, Sha256};
use std::sync::{Arc, Mutex};
use better_auth_core::{AuthContext, AuthError, AuthResult, BeforeRequestAction};
use better_auth_core::{AuthRecordFields, AuthRequest, AuthResponse, FieldValue};
pub(super) mod handlers;
pub(crate) mod storage;
pub use storage::ApiKeyStorage;
mod callbacks;
mod metadata;
mod native;
mod permissions;
mod request;
pub(super) mod types;
mod verification;
pub use callbacks::{
ApiKeyDefaultPermissions, ApiKeyEndpoint, ApiKeyGenerator, ApiKeyGetter, ApiKeyPermissions,
ApiKeyValidator,
};
pub use verification::{
ApiKeyErrorDetails, ApiKeyErrorMessage, ApiKeyValidationError, ApiKeyVerificationError,
VerifyApiKey,
};
#[cfg(test)]
mod tests;
#[cfg(test)]
mod crud_tests;
#[cfg(test)]
mod expiration_tests;
#[cfg(test)]
mod background_tests;
#[cfg(test)]
mod cleanup_clock_tests;
use handlers::*;
use types::*;
pub use types::{CreateKeyRequest, CreateKeyResponse, UpdateKeyRequest};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiKeyErrorCode {
InvalidApiKey,
KeyDisabled,
KeyExpired,
UsageExceeded,
KeyNotFound,
RateLimited,
UnauthorizedSession,
UserBanned,
InvalidPrefixLength,
InvalidNameLength,
MetadataDisabled,
NoValuesToUpdate,
KeyDisabledExpiration,
ExpiresInTooSmall,
ExpiresInTooLarge,
InvalidRemaining,
RefillAmountAndIntervalRequired,
RefillIntervalAndAmountRequired,
NameRequired,
InvalidUserIdFromApiKey,
InvalidReferenceIdFromApiKey,
NoDefaultConfiguration,
OrganizationIdRequired,
OrganizationPluginRequired,
UserNotMemberOfOrganization,
InsufficientApiKeyPermissions,
ServerOnlyProperty,
FailedToUpdateApiKey,
InvalidMetadataType,
}
impl ApiKeyErrorCode {
pub fn as_str(self) -> &'static str {
match self {
Self::InvalidApiKey => "INVALID_API_KEY",
Self::KeyDisabled => "KEY_DISABLED",
Self::KeyExpired => "KEY_EXPIRED",
Self::UsageExceeded => "USAGE_EXCEEDED",
Self::KeyNotFound => "KEY_NOT_FOUND",
Self::RateLimited => "RATE_LIMITED",
Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
Self::UserBanned => "USER_BANNED",
Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
Self::InvalidNameLength => "INVALID_NAME_LENGTH",
Self::MetadataDisabled => "METADATA_DISABLED",
Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
Self::InvalidRemaining => "INVALID_REMAINING",
Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
Self::RefillIntervalAndAmountRequired => "REFILL_INTERVAL_AND_AMOUNT_REQUIRED",
Self::NameRequired => "NAME_REQUIRED",
Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
Self::InvalidReferenceIdFromApiKey => "INVALID_REFERENCE_ID_FROM_API_KEY",
Self::NoDefaultConfiguration => "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND",
Self::OrganizationIdRequired => "ORGANIZATION_ID_REQUIRED",
Self::OrganizationPluginRequired => "ORGANIZATION_PLUGIN_REQUIRED",
Self::UserNotMemberOfOrganization => "USER_NOT_MEMBER_OF_ORGANIZATION",
Self::InsufficientApiKeyPermissions => "INSUFFICIENT_API_KEY_PERMISSIONS",
Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
}
}
pub fn message(self) -> &'static str {
match self {
Self::InvalidApiKey => "Invalid API key.",
Self::KeyDisabled => "API Key is disabled",
Self::KeyExpired => "API Key has expired",
Self::UsageExceeded => "API Key has reached its usage limit",
Self::KeyNotFound => "API Key not found",
Self::RateLimited => "Rate limit exceeded.",
Self::UnauthorizedSession => "Unauthorized or invalid session",
Self::UserBanned => "User is banned",
Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
Self::InvalidNameLength => "The name length is either too large or too small.",
Self::MetadataDisabled => "Metadata is disabled.",
Self::NoValuesToUpdate => "No values to update.",
Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
Self::ExpiresInTooSmall => {
"The expiresIn is smaller than the predefined minimum value."
}
Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
Self::InvalidRemaining => "The remaining count is either too large or too small.",
Self::RefillAmountAndIntervalRequired => {
"refillAmount is required when refillInterval is provided"
}
Self::RefillIntervalAndAmountRequired => {
"refillInterval is required when refillAmount is provided"
}
Self::NameRequired => "API Key name is required.",
Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
Self::InvalidReferenceIdFromApiKey => "The reference id from the API key is invalid.",
Self::NoDefaultConfiguration => "No default api-key configuration found.",
Self::OrganizationIdRequired => {
"Organization ID is required for organization-owned API keys."
}
Self::OrganizationPluginRequired => {
"Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
}
Self::UserNotMemberOfOrganization => {
"You are not a member of the organization that owns this API key."
}
Self::InsufficientApiKeyPermissions => {
"You do not have permission to perform this action on organization API keys."
}
Self::ServerOnlyProperty => {
"The property you're trying to set can only be set from the server auth instance only."
}
Self::FailedToUpdateApiKey => "Failed to update API key",
Self::InvalidMetadataType => "metadata must be an object or undefined",
}
}
}
impl serde::Serialize for ApiKeyErrorCode {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
pub(super) fn api_key_error(status: &'static str, code: ApiKeyErrorCode) -> AuthError {
AuthError::from_code(status, code.as_str(), code.message())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ApiKeyReferences {
#[default]
User,
Organization,
}
#[derive(Clone)]
pub struct ApiKeyPlugin {
pub(super) configurations: Vec<ApiKeyConfig>,
}
impl ApiKeyPlugin {
pub fn configuration(mut self, config: ApiKeyConfig) -> Self {
self.configurations.push(config.normalized());
self
}
pub(super) fn resolve_configuration(
&self,
config_id: Option<&str>,
) -> AuthResult<&ApiKeyConfig> {
if let Some(config_id) = config_id
&& let Some(found) = self
.configurations
.iter()
.find(|config| config.config_id == config_id)
{
return Ok(found);
}
self.configurations
.iter()
.find(|config| is_default_config_id(&config.config_id))
.ok_or_else(|| api_key_error("BAD_REQUEST", ApiKeyErrorCode::NoDefaultConfiguration))
}
}
pub(super) fn is_default_config_id(config_id: &str) -> bool {
config_id.is_empty() || config_id == "default"
}
pub(super) fn config_id_matches(
key_config_id: &better_auth_core::SchemaValue<String>,
expected: &str,
) -> bool {
let value = key_config_id.field_value();
if (!value.is_truthy() || key_config_id == "default") && is_default_config_id(expected) {
return true;
}
key_config_id.field_value().strict_equals(&expected.into())
}
#[derive(Clone)]
pub struct ApiKeyConfig {
pub config_id: String,
pub references: ApiKeyReferences,
pub key_length: usize,
pub prefix: Option<String>,
pub default_permissions: Option<std::collections::HashMap<String, Vec<String>>>,
pub custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
pub default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
pub api_key_headers: Vec<String>,
pub custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
pub custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
pub disable_key_hashing: bool,
pub starting_characters_length: usize,
pub store_starting_characters: bool,
pub max_prefix_length: usize,
pub min_prefix_length: usize,
pub max_name_length: usize,
pub min_name_length: usize,
pub require_name: bool,
pub enable_metadata: bool,
pub key_expiration: KeyExpirationConfig,
pub rate_limit: RateLimitDefaults,
pub enable_session_for_api_keys: bool,
pub storage: ApiKeyStorage,
pub custom_storage: Option<Arc<dyn better_auth_core::store::SecondaryStorage>>,
pub fallback_to_database: bool,
pub defer_updates: bool,
}
impl ApiKeyConfig {
fn normalized(mut self) -> Self {
if self.key_length == 0 {
self.key_length = 64;
}
self
}
}
impl std::fmt::Debug for ApiKeyConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiKeyConfig")
.field("config_id", &self.config_id)
.field("references", &self.references)
.field("key_length", &self.key_length)
.field("prefix", &self.prefix)
.field("default_permissions", &self.default_permissions)
.field("custom_key_generator", &self.custom_key_generator.is_some())
.field(
"default_permissions_callback",
&self.default_permissions_callback.is_some(),
)
.field(
"custom_api_key_getter",
&self.custom_api_key_getter.is_some(),
)
.field(
"custom_api_key_validator",
&self.custom_api_key_validator.is_some(),
)
.field("api_key_headers", &self.api_key_headers)
.field("disable_key_hashing", &self.disable_key_hashing)
.field(
"starting_characters_length",
&self.starting_characters_length,
)
.field("store_starting_characters", &self.store_starting_characters)
.field("max_prefix_length", &self.max_prefix_length)
.field("min_prefix_length", &self.min_prefix_length)
.field("max_name_length", &self.max_name_length)
.field("min_name_length", &self.min_name_length)
.field("require_name", &self.require_name)
.field("enable_metadata", &self.enable_metadata)
.field("key_expiration", &self.key_expiration)
.field("rate_limit", &self.rate_limit)
.field(
"enable_session_for_api_keys",
&self.enable_session_for_api_keys,
)
.field("storage", &self.storage)
.field("custom_storage", &self.custom_storage.is_some())
.field("fallback_to_database", &self.fallback_to_database)
.field("defer_updates", &self.defer_updates)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct KeyExpirationConfig {
pub default_expires_in: Option<f64>,
pub disable_custom_expires_time: bool,
pub max_expires_in: f64,
pub min_expires_in: f64,
}
impl Default for KeyExpirationConfig {
fn default() -> Self {
Self {
default_expires_in: None,
disable_custom_expires_time: false,
max_expires_in: 365.0,
min_expires_in: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct RateLimitDefaults {
pub enabled: bool,
pub time_window: f64,
pub max_requests: f64,
}
impl Default for RateLimitDefaults {
fn default() -> Self {
Self {
enabled: true,
time_window: 86_400_000.0, max_requests: 10.0,
}
}
}
impl Default for ApiKeyConfig {
fn default() -> Self {
Self {
config_id: "default".to_string(),
references: ApiKeyReferences::default(),
key_length: 64,
prefix: None,
default_permissions: None,
custom_key_generator: None,
default_permissions_callback: None,
api_key_headers: vec!["x-api-key".to_string()],
custom_api_key_getter: None,
custom_api_key_validator: None,
disable_key_hashing: false,
starting_characters_length: 6,
store_starting_characters: true,
max_prefix_length: 32,
min_prefix_length: 1,
max_name_length: 32,
min_name_length: 1,
require_name: false,
enable_metadata: false,
key_expiration: KeyExpirationConfig::default(),
rate_limit: RateLimitDefaults::default(),
enable_session_for_api_keys: false,
storage: ApiKeyStorage::Database,
custom_storage: None,
fallback_to_database: false,
defer_updates: false,
}
}
}
#[bon::bon]
impl ApiKeyPlugin {
#[builder]
pub fn new(
#[builder(default = "default".to_string())] config_id: String,
#[builder(default)] references: ApiKeyReferences,
#[builder(default = 64)] key_length: usize,
prefix: Option<String>,
default_permissions: Option<std::collections::HashMap<String, Vec<String>>>,
custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
#[builder(default = vec!["x-api-key".to_string()])] api_key_headers: Vec<String>,
custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
#[builder(default = false)] disable_key_hashing: bool,
#[builder(default = 6)] starting_characters_length: usize,
#[builder(default = true)] store_starting_characters: bool,
#[builder(default = 32)] max_prefix_length: usize,
#[builder(default = 1)] min_prefix_length: usize,
#[builder(default = 32)] max_name_length: usize,
#[builder(default = 1)] min_name_length: usize,
#[builder(default = false)] require_name: bool,
#[builder(default = false)] enable_metadata: bool,
#[builder(default)] key_expiration: KeyExpirationConfig,
#[builder(default)] rate_limit: RateLimitDefaults,
#[builder(default = false)] enable_session_for_api_keys: bool,
#[builder(default)] storage: ApiKeyStorage,
custom_storage: Option<Arc<dyn better_auth_core::store::SecondaryStorage>>,
#[builder(default)] fallback_to_database: bool,
#[builder(default)] defer_updates: bool,
) -> Self {
Self {
configurations: vec![
ApiKeyConfig {
config_id,
references,
key_length,
prefix,
default_permissions,
custom_key_generator,
default_permissions_callback,
api_key_headers,
custom_api_key_getter,
custom_api_key_validator,
disable_key_hashing,
starting_characters_length,
store_starting_characters,
max_prefix_length,
min_prefix_length,
max_name_length,
min_name_length,
require_name,
enable_metadata,
key_expiration,
rate_limit,
enable_session_for_api_keys,
storage,
custom_storage,
fallback_to_database,
defer_updates,
}
.normalized(),
],
}
}
pub fn with_config(config: ApiKeyConfig) -> Self {
Self {
configurations: vec![config.normalized()],
}
}
pub(super) fn generate_key(
config: &ApiKeyConfig,
custom_prefix: Option<&str>,
) -> (String, String) {
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let mut rng = rand::thread_rng();
let raw: String = (0..config.key_length)
.map(|_| {
ALPHABET
.choose(&mut rng)
.copied()
.map(char::from)
.unwrap_or('a')
})
.collect();
let prefix = custom_prefix.or(config.prefix.as_deref()).unwrap_or("");
let full_key = format!("{}{}", prefix, raw);
let hash = if config.disable_key_hashing {
full_key.clone()
} else {
Self::hash_key(&full_key)
};
(full_key, hash)
}
pub(super) fn hash_key(key: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(key.as_bytes());
let digest = hasher.finalize();
URL_SAFE_NO_PAD.encode(digest)
}
async fn maybe_delete_expired(
&self,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
bypass_last_check: bool,
) -> AuthResult<()> {
static LAST_CHECKED: Mutex<Option<i64>> = Mutex::new(None);
let cutoff = {
let mut last = LAST_CHECKED
.lock()
.map_err(|_| AuthError::internal("API Key cleanup clock lock poisoned"))?;
if let Some(previous) = *last
&& !bypass_last_check
&& ctx.now().timestamp_millis() - previous < 10_000
{
return Ok(());
}
*last = Some(ctx.now().timestamp_millis());
better_auth_core::FieldDate::from(ctx.now())
};
let _ = ctx.database.delete_expired_api_keys_before(cutoff).await?;
Ok(())
}
pub(super) async fn start_expired_cleanup(
&self,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
defer: bool,
outer_catch: bool,
) -> AuthResult<()> {
use better_auth_core::background::{
BackgroundFuture, run_in_background_with_error_messages,
start_unowned_with_error_messages,
};
const INNER: &[&str] = &["Failed to delete expired API keys:"];
const NESTED: &[&str] = &[
"Failed to delete expired API keys:",
"Failed to delete expired API keys:",
];
let plugin = self.clone();
let context = ctx.clone();
let task =
BackgroundFuture::new(
async move { plugin.maybe_delete_expired(&context, false).await },
);
let messages = if outer_catch { NESTED } else { INNER };
if defer {
run_in_background_with_error_messages(
task,
ctx.config.advanced.background_tasks.as_ref(),
ctx.logger(),
messages,
)
.await
} else {
start_unowned_with_error_messages(task, ctx.logger(), messages, "API key cleanup")
.await;
Ok(())
}
}
pub(super) fn validate_prefix(config: &ApiKeyConfig, prefix: Option<&str>) -> AuthResult<()> {
if let Some(p) = prefix.filter(|prefix| !prefix.is_empty()) {
let len = p.encode_utf16().count();
if len < config.min_prefix_length || len > config.max_prefix_length {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::InvalidPrefixLength,
));
}
}
Ok(())
}
pub(super) fn validate_name(
config: &ApiKeyConfig,
name: Option<&str>,
is_create: bool,
) -> AuthResult<()> {
if is_create && config.require_name && name.is_none_or(str::is_empty) {
return Err(api_key_error("BAD_REQUEST", ApiKeyErrorCode::NameRequired));
}
if let Some(n) = name.filter(|name| !is_create || !name.is_empty()) {
let len = n.encode_utf16().count();
if len < config.min_name_length || len > config.max_name_length {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::InvalidNameLength,
));
}
}
Ok(())
}
pub(super) fn validate_expires_in(
config: &ApiKeyConfig,
expires_in: Option<f64>,
) -> AuthResult<Option<f64>> {
let cfg = &config.key_expiration;
if let Some(secs) = expires_in {
if cfg.disable_custom_expires_time {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::KeyDisabledExpiration,
));
}
let days = secs / 86_400.0;
if days < cfg.min_expires_in {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::ExpiresInTooSmall,
));
}
if days > cfg.max_expires_in {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::ExpiresInTooLarge,
));
}
Ok(Some(secs))
} else {
Ok(cfg.default_expires_in)
}
}
pub(super) fn validate_metadata(
config: &ApiKeyConfig,
metadata: &better_auth_core::FieldValue,
) -> AuthResult<()> {
if metadata.is_truthy() {
if !config.enable_metadata {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::MetadataDisabled,
));
}
if !metadata.is_object() && !metadata.is_array() && metadata.as_date().is_none() {
return Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::InvalidMetadataType,
));
}
}
Ok(())
}
pub(super) fn validate_refill(
refill_interval: Option<f64>,
refill_amount: Option<f64>,
) -> AuthResult<()> {
match (
refill_interval.filter(|value| *value != 0.0),
refill_amount.filter(|value| *value != 0.0),
) {
(None, Some(_)) => Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::RefillAmountAndIntervalRequired,
)),
(Some(_), None) => Err(api_key_error(
"BAD_REQUEST",
ApiKeyErrorCode::RefillIntervalAndAmountRequired,
)),
_ => Ok(()),
}
}
async fn handle_create(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let (body, callback_body): (CreateKeyRequest, _) = request::read_with_body(req)?;
let input = request::CapturedInput::new(&body, &callback_body)?;
let endpoint = ApiKeyEndpoint::from_request(ctx, req, &callback_body);
let config = self.resolve_configuration(body.config_id.as_deref())?;
let session = ctx
.native_session(req, better_auth_core::session::SessionRead::CookieBypass)
.await?;
let original = endpoint.request;
let client = req.endpoint_headers().is_some() || original.is_some();
if client {
validate_client_create(&body)?;
}
if original.is_some() && body.user_id.is_some() {
return Err(api_key_error(
"UNAUTHORIZED",
ApiKeyErrorCode::UnauthorizedSession,
));
}
let actor = create_key_actor(
&body,
config.references,
session
.as_ref()
.map(|data| data.user_field("id"))
.transpose()?,
client,
)?;
let response = create_key_for_user(&input, &actor, self, ctx, endpoint).await?;
Ok(AuthResponse::native(None, response.into_field_value()?))
}
async fn handle_get(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let session = ctx.require_native_session(req).await?;
let id = req
.query_string("id")?
.ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
let config_id = req.query_string("configId")?;
let response = get_key_core(
&id,
config_id.as_deref(),
session.user_field("id")?,
self,
ctx,
)
.await?;
Ok(AuthResponse::native(None, response.field_values()?.into()))
}
async fn handle_list(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let session = ctx.require_native_session(req).await?;
let query = ListKeysQuery::from_request(req)?;
let response = list_keys_core(session.user_field("id")?, &query, self, ctx).await?;
Ok(AuthResponse::native(None, response.into_field_value()?))
}
async fn handle_update(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let (body, callback_body): (UpdateKeyRequest, _) = request::read_with_body(req)?;
let input = request::CapturedInput::new(&body, &callback_body)?;
let session = ctx
.native_session(req, better_auth_core::session::SessionRead::CookieBypass)
.await?;
let client = req.endpoint_headers().is_some()
|| req.original_request().is_some()
|| better_auth_core::hooks::current_request_hook_context()
.is_some_and(|context| context.is_http);
let actor = session
.as_ref()
.map(|data| data.user_field("id"))
.transpose()?
.or_else(|| {
(!client)
.then_some(body.user_id.as_deref())
.flatten()
.map(FieldValue::from)
})
.filter(FieldValue::is_truthy)
.ok_or_else(|| api_key_error("UNAUTHORIZED", ApiKeyErrorCode::UnauthorizedSession))?;
if session.is_some()
&& body
.user_id
.as_deref()
.filter(|id| !id.is_empty())
.is_some_and(|id| !actor.strict_equals(&id.into()))
{
return Err(api_key_error(
"UNAUTHORIZED",
ApiKeyErrorCode::UnauthorizedSession,
));
}
let response = if client {
update_key_core(&input, actor, self, ctx).await?
} else {
update_key_for_user(&input, &actor, self, ctx).await?
};
Ok(AuthResponse::native(None, response.field_values()?.into()))
}
async fn handle_delete(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let body: DeleteKeyRequest = request::read(req)?;
let session = ctx.require_native_session(req).await?;
if session.user_field("banned")?.as_bool() == Some(true) {
return Err(api_key_error("UNAUTHORIZED", ApiKeyErrorCode::UserBanned));
}
let response = delete_key_core(&body, session.user_field("id")?, self, ctx).await?;
AuthResponse::json(None, &response)
}
}
better_auth_core::impl_auth_plugin! {
ApiKeyPlugin, "api-key";
routes {
post "/api-key/create" => handle_create, "createApiKey", body = request::validate;
get "/api-key/get" => handle_get, "getApiKey", query = crate::plugins::query_input::api_key_get;
post "/api-key/update" => handle_update, "updateApiKey", body = request::validate;
post "/api-key/delete" => handle_delete, "deleteApiKey", body = request::validate;
get "/api-key/list" => handle_list, "listApiKeys", query = crate::plugins::query_input::api_key_list;
}
extra {
fn schema(&self, fields: &mut better_auth_core::plugin_runtime::ModelFields) -> AuthResult<()> {
let mut declaration = better_auth_core::plugin_runtime::ModelFields::plugin_native_fields(
better_auth_core::store::schema::EntityRole::ApiKey,
);
if let [config] = self.configurations.as_slice() {
for (name, value) in [
("rateLimitMax", config.rate_limit.max_requests),
("rateLimitTimeWindow", config.rate_limit.time_window),
] {
if let Some(field) = declaration.fields_mut().get_mut(name) {
field.default_value = Some(value.into());
}
}
}
fields.register_model_fields(better_auth_core::store::schema::EntityRole::ApiKey, declaration)?;
Ok(())
}
async fn on_init(&self, _ctx: &mut better_auth_core::AuthInitContext<S>) -> AuthResult<()> {
if self.configurations.len() > 1 {
let mut ids = std::collections::HashSet::new();
for config in &self.configurations {
if config.config_id.is_empty() {
return Err(AuthError::config("configId is required for each API key configuration in the api-key plugin."));
}
if !ids.insert(&config.config_id) {
return Err(AuthError::config("configId must be unique for each API key configuration in the api-key plugin."));
}
}
}
Ok(())
}
async fn before_request(
&self,
req: &AuthRequest,
ctx: &AuthContext<S>,
) -> AuthResult<Option<BeforeRequestAction>> {
self.api_key_session(req, ctx).await
}
}
}