1use better_auth_core::entity::AuthUser;
2use better_auth_core::store::ConsumeApiKeyResult;
3use better_auth_core::wire::{ApiKeyView, SessionView};
4use better_auth_core::{
5 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, BeforeRequestAction,
6};
7use serde::Serialize;
8
9use super::{ApiKeyEndpoint, ApiKeyErrorCode, ApiKeyPlugin, ApiKeyReferences, config_id_matches};
10
11pub struct VerifyApiKey<'a> {
13 pub key: &'a str,
15 pub config_id: Option<&'a str>,
17 pub permissions: Option<&'a serde_json::Value>,
19}
20
21#[derive(Debug, Serialize)]
23pub struct ApiKeyErrorDetails {
24 #[serde(rename = "tryAgainIn")]
26 pub try_again_in: f64,
27}
28
29#[derive(Debug, Serialize)]
31#[serde(untagged)]
32pub enum ApiKeyErrorMessage {
33 Text(String),
35 Code {
37 code: ApiKeyErrorCode,
38 message: String,
39 },
40}
41
42impl std::fmt::Display for ApiKeyErrorMessage {
43 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Text(message) | Self::Code { message, .. } => formatter.write_str(message),
46 }
47 }
48}
49
50#[derive(Debug, Serialize)]
52pub struct ApiKeyValidationError {
53 pub code: ApiKeyErrorCode,
55 pub message: ApiKeyErrorMessage,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub details: Option<ApiKeyErrorDetails>,
60}
61
62impl ApiKeyValidationError {
63 fn new(code: ApiKeyErrorCode) -> Self {
64 Self {
65 code,
66 message: ApiKeyErrorMessage::Text(code.message().to_owned()),
67 details: None,
68 }
69 }
70
71 fn invalid_constant(code: ApiKeyErrorCode) -> Self {
72 Self {
73 code,
74 message: ApiKeyErrorMessage::Code {
75 code: ApiKeyErrorCode::InvalidApiKey,
76 message: ApiKeyErrorCode::InvalidApiKey.message().to_owned(),
77 },
78 details: None,
79 }
80 }
81
82 fn status(&self) -> u16 {
83 match self.code {
84 ApiKeyErrorCode::NoDefaultConfiguration => 400,
85 ApiKeyErrorCode::RateLimited | ApiKeyErrorCode::UsageExceeded => 429,
86 _ => 401,
87 }
88 }
89
90 fn response(&self) -> AuthResult<AuthResponse> {
91 Ok(AuthResponse::json(self.status(), self)?)
92 }
93}
94
95#[derive(Debug)]
97pub enum ApiKeyVerificationError {
98 Validation(ApiKeyValidationError),
100 Rejected(AuthError),
102 Internal(AuthError),
104 Endpoint(AuthError),
106}
107
108impl std::fmt::Display for ApiKeyVerificationError {
109 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Self::Validation(error) => write!(formatter, "{}", error.message),
112 Self::Internal(error) | Self::Endpoint(error) | Self::Rejected(error) => {
113 write!(formatter, "API key verification failed: {error}")
114 }
115 }
116 }
117}
118
119impl std::error::Error for ApiKeyVerificationError {
120 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
121 match self {
122 Self::Internal(error) | Self::Endpoint(error) | Self::Rejected(error) => Some(error),
123 Self::Validation(_) => None,
124 }
125 }
126}
127
128impl ApiKeyVerificationError {
129 pub fn into_response(self) -> AuthResult<AuthResponse> {
131 let error = match self {
132 Self::Endpoint(error) => return Ok(error.to_auth_response()),
133 Self::Rejected(error) => {
134 let body: serde_json::Value =
135 serde_json::from_slice(&error.to_auth_response().body.bytes()?)?;
136 return Ok(AuthResponse::json(
137 200,
138 &serde_json::json!({"valid":false,"error":body,"key":null}),
139 )?);
140 }
141 Self::Validation(error) => error,
142 Self::Internal(error) => {
143 better_auth_core::observability::logger::current().error(
144 "Failed to validate API key",
145 &[better_auth_core::observability::LogArgument::Error(&error)],
146 );
147 ApiKeyValidationError::invalid_constant(ApiKeyErrorCode::InvalidApiKey)
148 }
149 };
150 Ok(AuthResponse::json(
151 200,
152 &serde_json::json!({"valid":false,"error":error,"key":null}),
153 )?)
154 }
155}
156
157impl From<AuthError> for ApiKeyVerificationError {
158 fn from(error: AuthError) -> Self {
159 Self::Internal(error)
160 }
161}
162
163impl From<ApiKeyErrorCode> for ApiKeyVerificationError {
164 fn from(code: ApiKeyErrorCode) -> Self {
165 Self::Validation(ApiKeyValidationError::new(code))
166 }
167}
168
169impl ApiKeyPlugin {
170 pub async fn verify_api_key(
179 &self,
180 input: &VerifyApiKey<'_>,
181 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
182 ) -> Result<ApiKeyView, ApiKeyVerificationError> {
183 let mut body = serde_json::Map::from_iter([("key".into(), input.key.into())]);
184 if let Some(config_id) = input.config_id {
185 let _ = body.insert("configId".into(), config_id.into());
186 }
187 if let Some(permissions) = input.permissions {
188 let _ = body.insert("permissions".into(), permissions.clone());
189 }
190 let body = serde_json::Value::Object(body);
191 let endpoint = ApiKeyEndpoint::new(ctx, None, None, &body);
192 let session = self
193 .authenticate_api_key(endpoint, ctx)
194 .await
195 .map_err(ApiKeyVerificationError::Endpoint)?;
196 let _ = super::request::validate_value("verifyApiKey", Some(&body))
197 .map_err(ApiKeyVerificationError::Endpoint)?;
198 let endpoint = ApiKeyEndpoint {
199 path: Some(if session.is_some() { "/" } else { "virtual:" }),
200 ..endpoint
201 };
202 let config = self
203 .resolve_configuration(input.config_id)
204 .map_err(ApiKeyVerificationError::Endpoint)?;
205 if input.config_id.is_some()
206 && let Some(validator) = &config.custom_api_key_validator
207 && !validator
208 .validate(input.key, endpoint)
209 .await
210 .map_err(ApiKeyVerificationError::Endpoint)?
211 {
212 return Err(ApiKeyVerificationError::Validation(
213 ApiKeyValidationError::invalid_constant(ApiKeyErrorCode::KeyNotFound),
214 ));
215 }
216 let key = self
217 .validate_api_key(input, ctx, endpoint, input.config_id.is_none())
218 .await?;
219 let config = self
220 .resolve_configuration(Some(&key.config_id))
221 .map_err(ApiKeyVerificationError::Endpoint)?;
222 super::metadata::single(key, config, ctx)
223 .await
224 .map_err(ApiKeyVerificationError::Endpoint)
225 }
226
227 async fn validate_api_key(
228 &self,
229 input: &VerifyApiKey<'_>,
230 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
231 endpoint: ApiKeyEndpoint<'_>,
232 run_custom_validator: bool,
233 ) -> Result<ApiKeyView, ApiKeyVerificationError> {
234 let lookup_config = self
235 .resolve_configuration(input.config_id)
236 .map_err(|_| ApiKeyErrorCode::NoDefaultConfiguration)?;
237 let hashed = if lookup_config.disable_key_hashing {
238 input.key.to_owned()
239 } else {
240 Self::hash_key(input.key)
241 };
242 let api_key = super::storage::get_by_hash(lookup_config, ctx, &hashed)
243 .await?
244 .ok_or(ApiKeyErrorCode::InvalidApiKey)?;
245
246 if input
247 .config_id
248 .is_some_and(|expected| !config_id_matches(&api_key.config_id, expected))
249 {
250 return Err(ApiKeyErrorCode::InvalidApiKey.into());
251 }
252 let config = self
253 .resolve_configuration(Some(&api_key.config_id))
254 .map_err(|_| ApiKeyErrorCode::NoDefaultConfiguration)?;
255
256 if run_custom_validator && let Some(validator) = &config.custom_api_key_validator {
257 match validator.validate(input.key, endpoint).await {
258 Ok(true) => {}
259 Ok(false) => return Err(ApiKeyErrorCode::KeyNotFound.into()),
260 Err(
261 error @ (AuthError::Internal(_)
262 | AuthError::Database(_)
263 | AuthError::Config(_)
264 | AuthError::Serialization(_)),
265 ) => return Err(ApiKeyVerificationError::Internal(error)),
266 Err(error) => return Err(ApiKeyVerificationError::Rejected(error)),
267 }
268 }
269
270 if !api_key.enabled.is_truthy()? {
271 return Err(ApiKeyErrorCode::KeyDisabled.into());
272 }
273 if let Some(expires_at) = &api_key.expires_at
274 && chrono::Utc::now().timestamp_millis() as f64 > expires_at.milliseconds()
275 {
276 super::storage::delete_for_verification(config, ctx, &api_key).await?;
277 return Err(ApiKeyErrorCode::KeyExpired.into());
278 }
279
280 if let Some(required) = input.permissions {
281 let permitted = api_key.permissions.as_deref().is_some_and(|permissions| {
282 super::handlers::check_permissions(permissions, required)
283 });
284 if !permitted {
285 return Err(ApiKeyErrorCode::KeyNotFound.into());
286 }
287 }
288
289 let updated = match super::storage::consume(config, ctx, &api_key).await? {
290 ConsumeApiKeyResult::Allowed(key) => key,
291 ConsumeApiKeyResult::RateLimited { try_again_in } => {
292 let mut error = ApiKeyValidationError::new(ApiKeyErrorCode::RateLimited);
293 error.details = Some(ApiKeyErrorDetails { try_again_in });
294 return Err(ApiKeyVerificationError::Validation(error));
295 }
296 ConsumeApiKeyResult::UsageExhausted => {
297 return Err(ApiKeyErrorCode::UsageExceeded.into());
298 }
299 };
300 Ok(ApiKeyView::from(updated.as_ref()))
301 }
302
303 pub(super) async fn api_key_session(
304 &self,
305 req: &AuthRequest,
306 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
307 ) -> AuthResult<Option<BeforeRequestAction>> {
308 let body = if let Some(body) = req.parsed_http_body() {
309 body.clone()
310 } else {
311 match req.body.as_deref().filter(|body| !body.is_empty()) {
312 Some(body)
313 if req.headers.iter().any(|(name, value)| {
314 name.eq_ignore_ascii_case("content-type")
315 && value
316 .to_ascii_lowercase()
317 .contains("application/x-www-form-urlencoded")
318 }) =>
319 {
320 serde_json::Value::Object(
321 url::form_urlencoded::parse(body)
322 .map(|(name, value)| {
323 (
324 name.into_owned(),
325 serde_json::Value::String(value.into_owned()),
326 )
327 })
328 .collect(),
329 )
330 }
331 Some(body) => crate::plugins::json_body::decode(body).map_err(AuthError::from)?,
332 None => serde_json::Value::Null,
333 }
334 };
335 let endpoint = ApiKeyEndpoint::new(ctx, Some(req), Some(req.path()), &body);
336 let (session, user) = match self.authenticate_api_key(endpoint, ctx).await {
337 Ok(Some(session)) => session,
338 Ok(None) => return Ok(None),
339 Err(error @ AuthError::Response(_)) => {
340 return Ok(Some(BeforeRequestAction::Respond(error.to_auth_response())));
341 }
342 Err(error) => return Err(error),
343 };
344 if req.path() == "/get-session" {
346 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
347 200,
348 &serde_json::json!({
349 "user": user,
350 "session": {
351 "id": session.id,
352 "token": session.token,
353 "userId": session.user_id,
354 "userAgent": session.user_agent,
355 "ipAddress": session.ip_address,
356 "createdAt": better_auth_core::FieldValue::from(session.created_at.clone()).json()?,
357 "updatedAt": better_auth_core::FieldValue::from(session.updated_at.clone()).json()?,
358 "expiresAt": better_auth_core::FieldValue::from(session.expires_at.clone()).json()?,
359 },
360 }),
361 )?)));
362 }
363 Ok(Some(BeforeRequestAction::InjectSession {
364 session: Box::new(session),
365 }))
366 }
367
368 pub(super) async fn authenticate_api_key(
369 &self,
370 endpoint: ApiKeyEndpoint<'_>,
371 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
372 ) -> AuthResult<Option<(SessionView, better_auth_core::wire::UserView)>> {
373 match self.find_session_key(endpoint) {
375 Ok(None) => return Ok(None),
376 Ok(Some(_)) => {}
377 Err(error) => {
378 better_auth_core::observability::logger::current().error(
379 "API key hook matcher failed",
380 &[better_auth_core::observability::LogArgument::Error(&error)],
381 );
382 return Err(AuthResponse::json(500, &serde_json::json!({
383 "message": "An error occurred during hook matcher execution. Check the logs for more details."
384 }))?.into());
385 }
386 }
387 let context = better_auth_core::hooks::current_request_hook_context();
388 let route = context
389 .as_ref()
390 .map(|context| context.path.as_deref().unwrap_or("/:virtual"))
391 .or(endpoint.path)
392 .unwrap_or("/");
393 let operation_id = context
394 .as_ref()
395 .and_then(|context| context.operation_id.as_deref())
396 .unwrap_or(route);
397 better_auth_core::observability::with_span(
398 &ctx.config.experimental.instrumentation,
399 &format!("hook before {route} plugin:api-key"),
400 better_auth_core::observability::SpanAttributes {
401 route: Some(route),
402 operation_id: Some(operation_id),
403 hook_type: Some("before"),
404 context: Some("plugin:api-key"),
405 ..Default::default()
406 },
407 async {
408 let endpoint = ApiKeyEndpoint {
409 path: endpoint.path.or(Some("/")),
410 ..endpoint
411 };
412 let Some((config, key)) = self
413 .find_session_key(endpoint)
414 .map_err(|error| super::callbacks::callback_error(error, endpoint.request))?
415 else {
416 return Err(AuthError::internal(
417 "API key getter stopped matching during hook execution",
418 ));
419 };
420 if key.encode_utf16().count() < config.key_length {
421 return Err(AuthResponse::json(
422 403,
423 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
424 )?
425 .into());
426 }
427 if let Some(validator) = &config.custom_api_key_validator
428 && !validator.validate(&key, endpoint).await.map_err(|error| {
429 super::callbacks::callback_error(error, endpoint.request)
430 })?
431 {
432 return Err(AuthResponse::json(
433 403,
434 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
435 )?
436 .into());
437 }
438 let input = VerifyApiKey {
439 key: &key,
440 config_id: Some(&config.config_id),
441 permissions: None,
442 };
443 let view = match self.validate_api_key(&input, ctx, endpoint, false).await {
444 Ok(view) => view,
445 Err(ApiKeyVerificationError::Validation(error)) => {
446 return Err(error.response()?.into());
447 }
448 Err(
449 ApiKeyVerificationError::Internal(error)
450 | ApiKeyVerificationError::Endpoint(error)
451 | ApiKeyVerificationError::Rejected(error),
452 ) => return Err(error),
453 };
454 self.maybe_delete_expired(ctx).await;
455
456 if config.references != ApiKeyReferences::User {
457 return Err(ApiKeyValidationError::new(
458 ApiKeyErrorCode::InvalidReferenceIdFromApiKey,
459 )
460 .response()?
461 .into());
462 }
463 let Some(user) = ctx.database.get_user_by_id(&view.reference_id).await? else {
464 return Err(ApiKeyValidationError::new(
465 ApiKeyErrorCode::InvalidReferenceIdFromApiKey,
466 )
467 .response()?
468 .into());
469 };
470
471 let now = chrono::Utc::now();
472 let expires_at = match view.expires_at {
473 Some(value) => value,
474 None => (now
476 + chrono::Duration::milliseconds(
477 ctx.config.session.expires_in().num_seconds(),
478 ))
479 .into(),
480 };
481 let meta = endpoint.request.map(|req| {
482 better_auth_core::RequestMeta::from_request_with_config(
483 req,
484 &ctx.config.advanced.ip_address,
485 )
486 });
487 let session = SessionView {
488 visible_fields: None,
489 id: view.id,
490 token: key.to_owned(),
491 user_id: user.id().into_owned(),
492 created_at: now.into(),
493 updated_at: now.into(),
494 expires_at,
495 ip_address: meta.as_ref().and_then(|meta| meta.ip_address.clone()),
496 user_agent: meta.and_then(|meta| meta.user_agent),
497 impersonated_by: None,
498 active_organization_id: None,
499 active_team_id: None,
500 active: true,
501 additional_fields: Default::default(),
502 };
503 Ok(Some((session, ctx.user_view(&user).await?)))
504 },
505 )
506 .await
507 }
508
509 fn find_session_key(
510 &self,
511 endpoint: ApiKeyEndpoint<'_>,
512 ) -> AuthResult<Option<(&super::ApiKeyConfig, String)>> {
513 for config in &self.configurations {
514 if !config.enable_session_for_api_keys {
515 continue;
516 }
517 let key = if let Some(getter) = &config.custom_api_key_getter {
518 getter.get(endpoint)?
519 } else {
520 config.api_key_headers.iter().find_map(|header| {
521 endpoint
522 .request
523 .and_then(|request| request.headers.get(&header.to_ascii_lowercase()))
524 .filter(|key| !key.is_empty())
525 .cloned()
526 })
527 };
528 if let Some(key) = key.filter(|key| !key.is_empty()) {
529 return Ok(Some((config, key)));
530 }
531 }
532 Ok(None)
533 }
534}