1use std::collections::HashMap;
2
3use better_auth_core::{AuthContext, AuthResult, CreateApiKey, UpdateApiKey};
4
5use super::ApiKeyPlugin;
6use super::types::*;
7use crate::plugins::helpers;
8
9pub(super) fn check_permissions(key_permissions_json: &str, required: &serde_json::Value) -> bool {
20 let required_map = match required.as_object() {
21 Some(m) => m,
22 None => return false,
23 };
24
25 let key_map: HashMap<String, Vec<String>> = match serde_json::from_str(key_permissions_json) {
26 Ok(v) => v,
27 Err(_) => return false,
28 };
29
30 for (resource, requested_actions) in required_map {
31 let allowed_actions = match key_map.get(resource) {
33 Some(a) => a,
34 None => return false,
36 };
37
38 if let Some(actions_array) = requested_actions.as_array() {
42 for action_val in actions_array {
44 let action = match action_val.as_str() {
45 Some(s) => s,
46 None => return false,
47 };
48 if !allowed_actions.iter().any(|a| a == action) {
49 return false;
50 }
51 }
52 } else if let Some(obj) = requested_actions.as_object() {
53 let actions = match obj.get("actions").and_then(|v| v.as_array()) {
55 Some(a) => a,
56 None => return false,
57 };
58 let connector = obj
59 .get("connector")
60 .and_then(|v| v.as_str())
61 .unwrap_or("AND");
62
63 if connector == "OR" {
64 let any_allowed = actions.iter().any(|action_val| {
66 action_val
67 .as_str()
68 .is_some_and(|action| allowed_actions.iter().any(|a| a == action))
69 });
70 if !any_allowed {
71 return false;
72 }
73 } else {
74 for action_val in actions {
76 let action = match action_val.as_str() {
77 Some(s) => s,
78 None => return false,
79 };
80 if !allowed_actions.iter().any(|a| a == action) {
81 return false;
82 }
83 }
84 }
85 } else {
86 return false;
88 }
89 }
90
91 true
92}
93
94impl ApiKeyPlugin {
99 pub async fn create_key(
102 &self,
103 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
104 body: &CreateKeyRequest,
105 ) -> AuthResult<CreateKeyResponse> {
106 let callback_body = serde_json::to_value(body)?;
107 let session = self
108 .authenticate_api_key(
109 super::ApiKeyEndpoint::new(ctx, None, Some("/api-key/create"), &callback_body),
110 ctx,
111 )
112 .await?;
113 let validated = super::request::validate_numbers(
114 "/api-key/create",
115 Some(&callback_body),
116 &[
117 ("expiresIn", body.expires_in),
118 ("remaining", body.remaining),
119 ("refillAmount", body.refill_amount),
120 ("refillInterval", body.refill_interval),
121 ("rateLimitTimeWindow", body.rate_limit_time_window),
122 ("rateLimitMax", body.rate_limit_max),
123 ],
124 )?;
125 let body = validated.get::<CreateKeyRequest>().ok_or_else(|| {
126 better_auth_core::AuthError::internal(
127 "API key body validator returned a different input type",
128 )
129 })?;
130 let config = self.resolve_configuration(body.config_id.as_deref())?;
131 if config.references == super::ApiKeyReferences::User
132 && let Some((session, _)) = &session
133 && body
134 .user_id
135 .as_ref()
136 .is_some_and(|user_id| !user_id.is_empty() && session.user_id != user_id.as_str())
137 {
138 return Err(super::api_key_error(
139 super::ApiKeyErrorCode::UnauthorizedSession,
140 ));
141 }
142 let user_id = session
143 .as_ref()
144 .and_then(|(session, _)| session.user_id.as_str())
145 .or(body.user_id.as_deref().filter(|id| !id.is_empty()))
146 .ok_or_else(|| super::api_key_error(super::ApiKeyErrorCode::UnauthorizedSession))?;
147 create_key_for_user(body, user_id, self, ctx, None).await
148 }
149
150 pub async fn update_key(
153 &self,
154 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
155 body: &UpdateKeyRequest,
156 ) -> AuthResult<ApiKeyView> {
157 let callback_body = serde_json::to_value(body)?;
158 let session = self
159 .authenticate_api_key(
160 super::ApiKeyEndpoint::new(ctx, None, Some("/api-key/update"), &callback_body),
161 ctx,
162 )
163 .await?;
164 let validated = super::request::validate_numbers(
165 "/api-key/update",
166 Some(&callback_body),
167 &[
168 ("expiresIn", body.expires_in.flatten()),
169 ("remaining", body.remaining),
170 ("refillAmount", body.refill_amount),
171 ("refillInterval", body.refill_interval),
172 ("rateLimitTimeWindow", body.rate_limit_time_window),
173 ("rateLimitMax", body.rate_limit_max),
174 ],
175 )?;
176 let body = validated.get::<UpdateKeyRequest>().ok_or_else(|| {
177 better_auth_core::AuthError::internal(
178 "API key body validator returned a different input type",
179 )
180 })?;
181 if let Some((session, _)) = &session
182 && body
183 .user_id
184 .as_ref()
185 .is_some_and(|user_id| !user_id.is_empty() && session.user_id != user_id.as_str())
186 {
187 return Err(super::api_key_error(
188 super::ApiKeyErrorCode::UnauthorizedSession,
189 ));
190 }
191 let user_id = session
192 .as_ref()
193 .and_then(|(session, _)| session.user_id.as_str())
194 .or(body.user_id.as_deref().filter(|id| !id.is_empty()))
195 .ok_or_else(|| super::api_key_error(super::ApiKeyErrorCode::UnauthorizedSession))?;
196 update_key_for_user(body, user_id, self, ctx).await
197 }
198}
199
200pub(super) fn validate_client_create(body: &CreateKeyRequest) -> AuthResult<()> {
201 if body.refill_amount.is_some()
202 || body.refill_interval.is_some()
203 || body.rate_limit_max.is_some()
204 || body.rate_limit_time_window.is_some()
205 || body.rate_limit_enabled.is_some()
206 || body.permissions.is_some()
207 || body.remaining.is_some()
208 {
209 return Err(super::api_key_error(
210 super::ApiKeyErrorCode::ServerOnlyProperty,
211 ));
212 }
213 Ok(())
214}
215
216pub(super) async fn create_key_for_user(
217 body: &CreateKeyRequest,
218 user_id: &str,
219 plugin: &ApiKeyPlugin,
220 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
221 request: Option<&better_auth_core::AuthRequest>,
222) -> AuthResult<CreateKeyResponse> {
223 let config = plugin.resolve_configuration(body.config_id.as_deref())?;
224 let reference_id = match config.references {
225 super::ApiKeyReferences::User => user_id.to_string(),
226 super::ApiKeyReferences::Organization => {
227 let organization_id = body
228 .organization_id
229 .as_deref()
230 .filter(|id| !id.is_empty())
231 .ok_or_else(|| {
232 super::api_key_error(super::ApiKeyErrorCode::OrganizationIdRequired)
233 })?;
234 helpers::require_org_api_key_permission(ctx, user_id, organization_id, "create")
235 .await?;
236 organization_id.to_string()
237 }
238 };
239
240 ApiKeyPlugin::validate_metadata(config, &body.metadata)?;
241 ApiKeyPlugin::validate_refill(
242 body.refill_interval.filter(|value| *value != 0.0),
243 body.refill_amount.filter(|value| *value != 0.0),
244 )?;
245 let effective_expires_in = ApiKeyPlugin::validate_expires_in(config, body.expires_in)?;
246 ApiKeyPlugin::validate_prefix(config, body.prefix.as_deref())?;
247 ApiKeyPlugin::validate_name(config, body.name.as_deref(), true)?;
248
249 plugin.maybe_delete_expired(ctx).await;
250 let (full_key, hash) = if let Some(generator) = &config.custom_key_generator {
251 let key = generator
252 .generate(
253 config.key_length,
254 body.prefix
255 .as_deref()
256 .filter(|prefix| !prefix.is_empty())
257 .or(config.prefix.as_deref()),
258 )
259 .await
260 .map_err(|error| super::callbacks::callback_error(error, request))?;
261 let hash = if config.disable_key_hashing {
262 key.clone()
263 } else {
264 ApiKeyPlugin::hash_key(&key)
265 };
266 (key, hash)
267 } else {
268 ApiKeyPlugin::generate_key(config, body.prefix.as_deref())
269 };
270 let start = config.store_starting_characters.then(|| {
271 better_auth_core::ApiKeyStart::prefix(&full_key, config.starting_characters_length)
272 });
273 let dynamic_permissions = if let Some(callback) = &config.default_permissions_callback {
274 let mut callback_body: serde_json::Map<String, serde_json::Value> =
275 serde_json::from_value(serde_json::to_value(body)?)?;
276 let _ = callback_body.insert("remaining".into(), serde_json::to_value(body.remaining)?);
277 let _ = callback_body.insert("expiresIn".into(), serde_json::to_value(body.expires_in)?);
278 let callback_body = serde_json::Value::Object(callback_body);
279 Some(
280 callback
281 .permissions(
282 &reference_id,
283 super::ApiKeyEndpoint::new(
284 ctx,
285 request,
286 Some("/api-key/create"),
287 &callback_body,
288 ),
289 )
290 .await
291 .map_err(|error| super::callbacks::callback_error(error, request))?,
292 )
293 } else {
294 None
295 };
296 let input = CreateApiKey {
297 additional_fields: Default::default(),
298 reference_id,
299 config_id: config.config_id.clone(),
300 name: body.name.clone().into(),
301 prefix: body.prefix.clone().or_else(|| config.prefix.clone()),
302 key_hash: hash,
303 start,
304 expires_at: expiration_date(effective_expires_in)?,
305 remaining: body.remaining,
306 rate_limit_enabled: body.rate_limit_enabled.unwrap_or(config.rate_limit.enabled),
307 rate_limit_time_window: body
308 .rate_limit_time_window
309 .or(Some(config.rate_limit.time_window)),
310 rate_limit_max: body.rate_limit_max.or(Some(config.rate_limit.max_requests)),
311 refill_interval: body.refill_interval,
312 refill_amount: body.refill_amount,
313 permissions: body
314 .permissions
315 .as_ref()
316 .or(dynamic_permissions.as_ref())
317 .or(config.default_permissions.as_ref())
318 .map(serde_json::to_string)
319 .transpose()?,
320 metadata: Some(
321 body.metadata
322 .as_ref()
323 .filter(|value| json_truthy(value))
324 .unwrap_or(&serde_json::Value::Null)
325 .to_string(),
326 ),
327 enabled: true,
328 };
329 let api_key = super::storage::create(config, ctx, input).await?;
330 let mut api_key = ApiKeyView::from(&api_key);
331 api_key.metadata = body.metadata.clone();
333 Ok(CreateKeyResponse {
334 key: full_key,
335 api_key,
336 })
337}
338
339fn json_truthy(value: &serde_json::Value) -> bool {
340 match value {
341 serde_json::Value::Null => false,
342 serde_json::Value::Bool(value) => *value,
343 serde_json::Value::Number(value) => value.as_f64().is_some_and(|value| value != 0.0),
344 serde_json::Value::String(value) => !value.is_empty(),
345 _ => true,
346 }
347}
348
349fn expiration_date(seconds: Option<f64>) -> AuthResult<Option<better_auth_core::FieldDate>> {
350 let Some(seconds) = seconds.filter(|seconds| *seconds != 0.0) else {
351 return Ok(None);
352 };
353 let milliseconds = seconds * 1000.0;
354 if !milliseconds.is_finite() || milliseconds.abs() > i64::MAX as f64 {
355 return Err(better_auth_core::AuthError::bad_request(
356 "expiresIn is out of range",
357 ));
358 }
359 let duration = chrono::Duration::milliseconds(milliseconds as i64);
360 let date = chrono::Utc::now()
361 .checked_add_signed(duration)
362 .ok_or_else(|| better_auth_core::AuthError::bad_request("expiresIn is out of range"))?;
363 Ok(Some(date.into()))
364}
365
366pub(crate) async fn get_key_core(
367 id: &str,
368 config_id: Option<&str>,
369 user_id: impl AsRef<str>,
370 plugin: &ApiKeyPlugin,
371 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
372) -> AuthResult<ApiKeyView> {
373 let config = plugin.resolve_configuration(config_id)?;
374 let api_key = helpers::get_owned_api_key(ctx, config, id, user_id.as_ref(), "read").await?;
375 plugin.maybe_delete_expired(ctx).await;
376 super::metadata::single(ApiKeyView::from(&api_key), config, ctx).await
377}
378
379pub(crate) async fn list_keys_core(
380 user_id: impl AsRef<str>,
381 query: &ListKeysQuery,
382 plugin: &ApiKeyPlugin,
383 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
384) -> AuthResult<ListKeysResponse> {
385 let organization_id = query.organization_id.as_deref().filter(|id| !id.is_empty());
386 if let Some(organization_id) = organization_id {
387 helpers::require_org_api_key_permission(ctx, user_id.as_ref(), organization_id, "read")
388 .await?;
389 }
390 let reference_id = query.organization_id.as_deref().unwrap_or(user_id.as_ref());
391 let references = if organization_id.is_some() {
392 super::ApiKeyReferences::Organization
393 } else {
394 super::ApiKeyReferences::User
395 };
396 let config_id = query.config_id.as_deref().filter(|id| !id.is_empty());
397 if config_id.is_some() {
398 let _ = plugin.resolve_configuration(config_id)?;
399 }
400
401 let configurations = if let Some(id) = config_id {
402 vec![plugin.resolve_configuration(Some(id))?]
403 } else {
404 let mut groups = std::collections::HashSet::new();
405 plugin
406 .configurations
407 .iter()
408 .filter(|config| groups.insert(super::storage::group(config)))
409 .collect()
410 };
411 let sort = query
412 .sort_by
413 .as_deref()
414 .filter(|field| !field.is_empty())
415 .map(|field| (field, query.sort_direction.as_deref().unwrap_or("asc")));
416 let groups = futures_util::future::join_all(
417 configurations
418 .into_iter()
419 .map(|config| super::storage::list(config, ctx, reference_id, sort)),
420 )
421 .await;
422 let groups: Vec<_> = groups.into_iter().collect::<AuthResult<_>>()?;
423 let mut keys: Vec<_> = groups.into_iter().flatten().collect();
424 super::storage::deduplicate(&mut keys);
425 let mut views: Vec<ApiKeyView> = keys
426 .iter()
427 .filter(|key| {
428 let key_references = plugin
429 .configurations
430 .iter()
431 .find(|config| super::config_id_matches(&key.config_id, &config.config_id))
432 .map(|config| config.references)
433 .unwrap_or_default();
434 key_references == references
435 && key.reference_id == reference_id
436 && config_id.is_none_or(|id| super::config_id_matches(&key.config_id, id))
437 })
438 .map(ApiKeyView::from)
439 .collect();
440
441 let total = views.len();
442 if let Some(offset) = query.offset {
443 views = views.split_off(offset.min(views.len() as u64) as usize);
444 }
445 if let Some(limit) = query.limit {
446 views.truncate(limit.min(views.len() as u64) as usize);
447 }
448 plugin.maybe_delete_expired(ctx).await;
449 super::metadata::batch(&mut views, &plugin.configurations, ctx).await;
450 Ok(ListKeysResponse {
451 api_keys: views,
452 total,
453 limit: query.limit,
454 offset: query.offset,
455 })
456}
457
458pub(crate) async fn update_key_core(
459 body: &UpdateKeyRequest,
460 user_id: impl AsRef<str>,
461 plugin: &ApiKeyPlugin,
462 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
463) -> AuthResult<ApiKeyView> {
464 if body
465 .user_id
466 .as_deref()
467 .is_some_and(|id| !id.is_empty() && id != user_id.as_ref())
468 {
469 return Err(super::api_key_error(
470 super::ApiKeyErrorCode::UnauthorizedSession,
471 ));
472 }
473 if body.refill_amount.is_some()
474 || body.refill_interval.is_some()
475 || body.rate_limit_max.is_some()
476 || body.rate_limit_time_window.is_some()
477 || body.rate_limit_enabled.is_some()
478 || body.remaining.is_some()
479 || body.permissions.is_some()
480 {
481 return Err(super::api_key_error(
482 super::ApiKeyErrorCode::ServerOnlyProperty,
483 ));
484 }
485 update_key_for_user(body, user_id.as_ref(), plugin, ctx).await
486}
487
488pub(super) async fn update_key_for_user(
489 body: &UpdateKeyRequest,
490 user_id: &str,
491 plugin: &ApiKeyPlugin,
492 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
493) -> AuthResult<ApiKeyView> {
494 let config = plugin.resolve_configuration(body.config_id.as_deref())?;
495 let api_key = helpers::get_owned_api_key(ctx, config, &body.key_id, user_id, "update").await?;
496 ApiKeyPlugin::validate_name(config, body.name.as_deref(), false)?;
497
498 let expires_at = match body.expires_in {
499 None => None,
500 Some(_) if config.key_expiration.disable_custom_expires_time => {
501 return Err(super::api_key_error(
502 super::ApiKeyErrorCode::KeyDisabledExpiration,
503 ));
504 }
505 Some(None) => Some(None),
506 Some(Some(seconds)) => {
507 let validated = ApiKeyPlugin::validate_expires_in(config, Some(seconds))?;
508 Some(expiration_date(validated)?)
509 }
510 };
511 let metadata = body.metadata.as_ref().filter(|_| config.enable_metadata);
512 if let Some(metadata) = metadata
513 && !(metadata.is_null() || metadata.is_object() || metadata.is_array())
514 {
515 return Err(super::api_key_error(
516 super::ApiKeyErrorCode::InvalidMetadataType,
517 ));
518 }
519 ApiKeyPlugin::validate_refill(body.refill_interval, body.refill_amount)?;
520 if body.name.is_none()
521 && body.enabled.is_none()
522 && expires_at.is_none()
523 && metadata.is_none()
524 && body.remaining.is_none()
525 && body.refill_amount.is_none()
526 && body.refill_interval.is_none()
527 && body.rate_limit_enabled.is_none()
528 && body.rate_limit_time_window.is_none()
529 && body.rate_limit_max.is_none()
530 && body.permissions.is_none()
531 {
532 return Err(super::api_key_error(
533 super::ApiKeyErrorCode::NoValuesToUpdate,
534 ));
535 }
536 let update = UpdateApiKey {
537 name: body.name.clone().map(|value| Some(value).into()),
538 enabled: body.enabled,
539 remaining: body.remaining,
540 rate_limit_enabled: body.rate_limit_enabled,
541 rate_limit_time_window: body.rate_limit_time_window,
542 rate_limit_max: body.rate_limit_max,
543 refill_interval: body.refill_interval,
544 refill_amount: body.refill_amount,
545 permissions: body
546 .permissions
547 .as_ref()
548 .map(serde_json::to_string)
549 .transpose()?,
550 metadata: metadata.map(ToString::to_string),
551 expires_at,
552 ..Default::default()
553 };
554 let updated = super::storage::update(config, ctx, api_key, update).await?;
555 plugin.maybe_delete_expired(ctx).await;
556 super::metadata::single(ApiKeyView::from(&updated), config, ctx).await
557}
558
559pub(crate) async fn delete_key_core(
560 body: &DeleteKeyRequest,
561 user_id: impl AsRef<str>,
562 plugin: &ApiKeyPlugin,
563 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
564) -> AuthResult<serde_json::Value> {
565 let config = plugin.resolve_configuration(body.config_id.as_deref())?;
566 let api_key =
567 helpers::get_owned_api_key(ctx, config, &body.key_id, user_id.as_ref(), "delete").await?;
568 super::storage::delete(config, ctx, &api_key).await?;
569 plugin.maybe_delete_expired(ctx).await;
570 Ok(serde_json::json!({ "success": true }))
571}