1use super::{
2 ApiKeyCallbackContext, ApiKeyConfig, ApiKeyErrorCode, ApiKeyPlugin, ApiKeyReferences,
3 config_id_matches,
4};
5use alibi_core::entity::AuthUser;
6use alibi_core::store::ConsumeApiKeyResult;
7use alibi_core::wire::{ApiKeyView, SessionView};
8use alibi_core::{
9 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, BeforeRequestAction,
10};
11use serde::Serialize;
12
13pub struct VerifyApiKey<'a> {
15 pub key: &'a str,
17 pub config_id: Option<&'a str>,
19 pub permissions: Option<&'a serde_json::Value>,
21}
22
23#[derive(Debug, Serialize)]
25pub struct ApiKeyErrorDetails {
26 #[serde(rename = "tryAgainIn")]
28 pub try_again_in: f64,
29}
30
31#[derive(Debug, Serialize)]
33#[serde(untagged)]
34pub enum ApiKeyErrorMessage {
35 Text(String),
36 CodeMessage {
37 code: ApiKeyErrorCode,
38 message: String,
39 },
40}
41
42impl ApiKeyErrorMessage {
43 fn text(&self) -> &str {
44 match self {
45 Self::Text(value) | Self::CodeMessage { message: value, .. } => value,
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 pub(super) fn new(code: ApiKeyErrorCode) -> Self {
64 Self {
65 code,
66 message: ApiKeyErrorMessage::Text(code.message().to_owned()),
67 details: None,
68 }
69 }
70
71 pub(super) const fn status(&self) -> u16 {
72 match self.code {
73 ApiKeyErrorCode::NoDefaultConfiguration => 400,
74 ApiKeyErrorCode::RateLimited | ApiKeyErrorCode::UsageExceeded => 429,
75 ApiKeyErrorCode::InvalidApiKey
76 | ApiKeyErrorCode::KeyDisabled
77 | ApiKeyErrorCode::KeyExpired
78 | ApiKeyErrorCode::KeyNotFound
79 | ApiKeyErrorCode::UnauthorizedSession
80 | ApiKeyErrorCode::InvalidPrefixLength
81 | ApiKeyErrorCode::InvalidNameLength
82 | ApiKeyErrorCode::MetadataDisabled
83 | ApiKeyErrorCode::NoValuesToUpdate
84 | ApiKeyErrorCode::KeyDisabledExpiration
85 | ApiKeyErrorCode::ExpiresInTooSmall
86 | ApiKeyErrorCode::ExpiresInTooLarge
87 | ApiKeyErrorCode::InvalidRemaining
88 | ApiKeyErrorCode::RefillAmountAndIntervalRequired
89 | ApiKeyErrorCode::RefillIntervalAndAmountRequired
90 | ApiKeyErrorCode::NameRequired
91 | ApiKeyErrorCode::InvalidUserIdFromApiKey
92 | ApiKeyErrorCode::InvalidReferenceIdFromApiKey
93 | ApiKeyErrorCode::OrganizationIdRequired
94 | ApiKeyErrorCode::OrganizationPluginRequired
95 | ApiKeyErrorCode::UserNotMemberOfOrganization
96 | ApiKeyErrorCode::InsufficientApiKeyPermissions
97 | ApiKeyErrorCode::ServerOnlyProperty
98 | ApiKeyErrorCode::FailedToUpdateApiKey
99 | ApiKeyErrorCode::InvalidMetadataType => 401,
100 }
101 }
102
103 fn response(&self) -> AuthResult<AuthResponse> {
104 Ok(AuthResponse::json(self.status(), self)?)
105 }
106}
107
108#[derive(Debug)]
110pub enum ApiKeyVerificationError {
111 Validation(ApiKeyValidationError),
113 Internal(AuthError),
115 ExplicitValidator(AuthError),
118}
119
120impl std::fmt::Display for ApiKeyVerificationError {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 Self::Validation(error) => f.write_str(error.message.text()),
124 Self::Internal(error) | Self::ExplicitValidator(error) => {
125 write!(f, "API key verification failed: {error}")
126 }
127 }
128 }
129}
130
131impl std::error::Error for ApiKeyVerificationError {
132 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
133 match self {
134 Self::Internal(error) | Self::ExplicitValidator(error) => Some(error),
135 Self::Validation(_) => None,
136 }
137 }
138}
139
140impl From<AuthError> for ApiKeyVerificationError {
141 fn from(error: AuthError) -> Self {
142 Self::Internal(error)
143 }
144}
145
146impl From<ApiKeyErrorCode> for ApiKeyVerificationError {
147 fn from(code: ApiKeyErrorCode) -> Self {
148 Self::Validation(ApiKeyValidationError::new(code))
149 }
150}
151
152impl ApiKeyPlugin {
153 pub async fn verify_api_key(
166 &self,
167 input: &VerifyApiKey<'_>,
168 ctx: &AuthContext<impl alibi_core::AuthSchema>,
169 ) -> Result<ApiKeyView, ApiKeyVerificationError> {
170 self.verify_api_key_with_registration(input, None, ctx)
171 .await
172 }
173
174 pub async fn verify_api_key_with_request(
181 &self,
182 input: &VerifyApiKey<'_>,
183 request: &AuthRequest,
184 ctx: &AuthContext<impl alibi_core::AuthSchema>,
185 ) -> Result<ApiKeyView, ApiKeyVerificationError> {
186 self.verify_api_key_with_registration(input, Some(request), ctx)
187 .await
188 }
189
190 pub(super) async fn verify_api_key_with_registration(
191 &self,
192 input: &VerifyApiKey<'_>,
193 request: Option<&AuthRequest>,
194 ctx: &AuthContext<impl alibi_core::AuthSchema>,
195 ) -> Result<ApiKeyView, ApiKeyVerificationError> {
196 let view = self
197 .verify_api_key_checked(input, request, ctx, true)
198 .await?;
199 if self
200 .resolve_configuration(Some(&view.config_id))?
201 .defer_updates
202 {
203 self.register_expired_cleanup(ctx).await?;
204 }
205 Ok(view)
206 }
207
208 pub(super) async fn verify_api_key_checked(
209 &self,
210 input: &VerifyApiKey<'_>,
211 request: Option<&AuthRequest>,
212 ctx: &AuthContext<impl alibi_core::AuthSchema>,
213 server_operation: bool,
214 ) -> Result<ApiKeyView, ApiKeyVerificationError> {
215 let lookup_config = self
216 .resolve_configuration(input.config_id)
217 .map_err(|_error| ApiKeyErrorCode::NoDefaultConfiguration)?;
218 if server_operation
219 && input.config_id.is_some()
220 && let Some(validator) = &lookup_config.custom_api_key_validator
221 && !validator
222 .validate(
223 &ApiKeyCallbackContext::new(request, ctx, &lookup_config.config_id)
224 .with_verification_input(input),
225 input.key,
226 )
227 .await
228 .map_err(ApiKeyVerificationError::ExplicitValidator)?
229 {
230 return Err(ApiKeyVerificationError::Validation(ApiKeyValidationError {
231 code: ApiKeyErrorCode::KeyNotFound,
232 message: ApiKeyErrorMessage::CodeMessage {
233 code: ApiKeyErrorCode::InvalidApiKey,
234 message: ApiKeyErrorCode::InvalidApiKey.message().to_owned(),
235 },
236 details: None,
237 }));
238 }
239 let hashed = if lookup_config.disable_key_hashing {
240 input.key.to_owned()
241 } else {
242 Self::hash_key(input.key)
243 };
244 let api_key = lookup_config
245 .read_key(ctx, &hashed, true)
246 .await?
247 .ok_or(ApiKeyErrorCode::InvalidApiKey)?;
248
249 if input
250 .config_id
251 .is_some_and(|expected| !config_id_matches(&api_key.config_id, expected))
252 {
253 return Err(ApiKeyErrorCode::InvalidApiKey.into());
254 }
255 let config = self
256 .resolve_configuration(Some(&api_key.config_id))
257 .map_err(|_error| ApiKeyErrorCode::NoDefaultConfiguration)?;
258
259 if server_operation
260 && input.config_id.is_none()
261 && let Some(validator) = &config.custom_api_key_validator
262 && !validator
263 .validate(
264 &ApiKeyCallbackContext::new(request, ctx, &config.config_id)
265 .with_verification_input(input),
266 input.key,
267 )
268 .await?
269 {
270 return Err(ApiKeyErrorCode::KeyNotFound.into());
271 }
272
273 if !api_key.enabled {
274 return Err(ApiKeyErrorCode::KeyDisabled.into());
275 }
276 if let Some(expires_at) = api_key.expires_at.as_deref() {
277 let expiration = chrono::DateTime::parse_from_rfc3339(expires_at).map_err(|error| {
278 AuthError::internal(format!("Invalid stored API key expiration: {error}"))
279 })?;
280 if chrono::Utc::now() > expiration {
281 Self::delete_rejected_key(&api_key, config, ctx).await?;
282 return Err(ApiKeyErrorCode::KeyExpired.into());
283 }
284 }
285 if let Some(required) = input.permissions {
286 let permitted = match api_key.permissions.as_deref() {
287 Some(permissions) => super::handlers::check_permissions(permissions, required)?,
288 None => false,
289 };
290 if !permitted {
291 return Err(ApiKeyErrorCode::KeyNotFound.into());
292 }
293 }
294
295 if api_key.remaining == Some(0.0) && api_key.refill_amount.is_none() {
298 Self::delete_rejected_key(&api_key, config, ctx).await?;
299 return Err(ApiKeyErrorCode::UsageExceeded.into());
300 }
301
302 if !config.uses_database() {
303 let updated = Self::consume_secondary_usage(&api_key, config, ctx).await?;
304 return Ok(ApiKeyView::from(&updated));
305 }
306 let updated = match ctx
307 .database
308 .consume_api_key_usage_from_snapshot(&api_key, config.rate_limit.enabled)
309 .await?
310 {
311 ConsumeApiKeyResult::Allowed(key) => key,
312 ConsumeApiKeyResult::RateLimited { try_again_in } => {
313 let mut error = ApiKeyValidationError::new(ApiKeyErrorCode::RateLimited);
314 error.details = Some(ApiKeyErrorDetails { try_again_in });
315 return Err(ApiKeyVerificationError::Validation(error));
316 }
317 ConsumeApiKeyResult::UsageExhausted => {
318 return Err(ApiKeyErrorCode::UsageExceeded.into());
319 }
320 };
321 config.cache_key(updated.as_ref()).await?;
322 Ok(ApiKeyView::from(updated.as_ref()))
323 }
324
325 async fn delete_rejected_key(
326 key: &alibi_core::ApiKey,
327 config: &ApiKeyConfig,
328 ctx: &AuthContext<impl alibi_core::AuthSchema>,
329 ) -> AuthResult<()> {
330 if !config.defer_updates {
331 return config.remove_key(ctx, key).await;
332 }
333 let database = std::sync::Arc::clone(&ctx.database);
334 let key = key.clone();
335 let config = config.clone();
336 let storage = config.secondary();
337 let completion = Self::start_background_work(async move {
338 let deletion = async {
339 if config.storage == super::ApiKeyStorageMode::SecondaryStorage {
340 let storage = storage
341 .ok_or_else(|| AuthError::internal("Secondary storage is required"))?;
342 super::storage::remove_storage(storage, &key, config.fallback_to_database)
343 .await?;
344 }
345 if config.uses_database() {
346 database.delete_api_key(&key.id).await?;
347 }
348 Ok::<_, AuthError>(())
349 }
350 .await;
351 if let Err(error) = deletion {
352 tracing::error!(%error, "Deferred update failed");
353 }
354 Ok(())
355 })
356 .await?;
357 if let Some(handler) = &ctx.config.background_tasks {
358 handler.handle(completion)
359 } else {
360 drop(completion);
361 Ok(())
362 }
363 }
364
365 pub(super) fn find_session_key<'a>(
366 &'a self,
367 req: &AuthRequest,
368 ctx: &AuthContext<impl alibi_core::AuthSchema>,
369 ) -> AuthResult<Option<(&'a ApiKeyConfig, String)>> {
370 self.find_session_key_for_input(&req.headers, Some(req), None, ctx)
371 }
372
373 pub(super) fn find_session_key_for_input<'a>(
374 &'a self,
375 headers: &std::collections::HashMap<String, String>,
376 request: Option<&AuthRequest>,
377 endpoint: Option<&alibi_core::endpoint::EndpointCall>,
378 ctx: &AuthContext<impl alibi_core::AuthSchema>,
379 ) -> AuthResult<Option<(&'a ApiKeyConfig, String)>> {
380 for config in self
381 .configurations
382 .iter()
383 .filter(|config| config.enable_session_for_api_keys)
384 {
385 let key = match &config.custom_api_key_getter {
386 Some(getter) => {
387 let mut context = ApiKeyCallbackContext::new(request, ctx, &config.config_id);
388 if let Some(endpoint) = endpoint {
389 context = context.with_endpoint(endpoint);
390 }
391 getter.get_key(&context)?
392 }
393 None => config.api_key_headers.iter().find_map(|header| {
394 headers
395 .get(&header.to_ascii_lowercase())
396 .filter(|key| !key.is_empty())
397 .cloned()
398 }),
399 };
400 if let Some(key) = key.filter(|key| !key.is_empty()) {
401 return Ok(Some((config, key)));
402 }
403 }
404 Ok(None)
405 }
406
407 #[expect(
408 clippy::too_many_lines,
409 reason = "Keep API key validation and session substitution in one ordered middleware decision"
410 )]
411 pub(super) async fn api_key_session(
412 &self,
413 req: &AuthRequest,
414 ctx: &AuthContext<impl alibi_core::AuthSchema>,
415 ) -> AuthResult<Option<BeforeRequestAction>> {
416 if self
420 .find_session_key(req, ctx)
421 .map_err(|_error| AuthError::Api {
422 status: 500,
423 code: None,
424 message: "An error occurred during hook matcher execution. Check the logs for more details.".into(),
425 })?
426 .is_none()
427 {
428 return Ok(None);
429 }
430 let (config, key) = match self.find_session_key(req, ctx) {
431 Ok(Some(value)) => value,
432 Ok(None) => {
433 tracing::error!("API key getter did not return a key after matching");
434 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
435 }
436 Err(error)
437 if error.status_code() >= 500
438 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) =>
439 {
440 tracing::error!(error = %error, "API key getter failed");
441 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
442 }
443 Err(error) => return Err(error),
444 };
445
446 if f64::from(
447 u32::try_from(key.encode_utf16().count())
448 .map_err(|error| AuthError::internal(error.to_string()))?,
449 ) < config.key_length
450 {
451 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
452 403,
453 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
454 )?)));
455 }
456 if let Some(validator) = &config.custom_api_key_validator {
457 let valid = match validator
458 .validate(
459 &ApiKeyCallbackContext::new(Some(req), ctx, &config.config_id),
460 &key,
461 )
462 .await
463 {
464 Ok(valid) => valid,
465 Err(error)
466 if error.status_code() >= 500
467 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) =>
468 {
469 tracing::error!(error = %error, "API key validator failed");
470 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
471 }
472 Err(error) => return Err(error),
473 };
474 if !valid {
475 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
476 403,
477 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
478 )?)));
479 }
480 }
481 let input = VerifyApiKey {
482 key: &key,
483 config_id: Some(&config.config_id),
484 permissions: None,
485 };
486 let view = match self
487 .verify_api_key_checked(&input, Some(req), ctx, false)
488 .await
489 {
490 Ok(view) => view,
491 Err(ApiKeyVerificationError::Validation(error)) => {
492 return Ok(Some(BeforeRequestAction::Respond(error.response()?)));
493 }
494 Err(
495 ApiKeyVerificationError::Internal(error)
496 | ApiKeyVerificationError::ExplicitValidator(error),
497 ) => {
498 if error.status_code() >= 500
501 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. })
502 {
503 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
504 }
505 return Err(error);
506 }
507 };
508 if config.defer_updates {
509 let completion = self.start_configured_cleanup(ctx).await?;
510 if let Some(handler) = &ctx.config.background_tasks {
511 if let Err(error) = handler.handle(completion) {
512 if error.status_code() >= 500
513 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. })
514 {
515 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
516 }
517 return Err(error);
518 }
519 } else {
520 drop(completion);
521 }
522 } else {
523 self.maybe_delete_expired(ctx).await?;
524 }
525
526 if config.references != ApiKeyReferences::User {
527 return Ok(Some(BeforeRequestAction::Respond(
528 ApiKeyValidationError::new(ApiKeyErrorCode::InvalidReferenceIdFromApiKey)
529 .response()?,
530 )));
531 }
532 let Some(user) = ctx.database.get_user_by_id(&view.reference_id).await? else {
533 return Ok(Some(BeforeRequestAction::Respond(
534 ApiKeyValidationError::new(ApiKeyErrorCode::InvalidReferenceIdFromApiKey)
535 .response()?,
536 )));
537 };
538
539 let session = Self::virtual_session_from_key(&view, &key, &user, Some(req), ctx)?;
540 if req.path() == "/get-session" {
542 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
543 200,
544 &serde_json::json!({
545 "user": ctx.user_view(&user),
546 "session": session,
547 }),
548 )?)));
549 }
550 Ok(Some(BeforeRequestAction::InjectSession { session }))
551 }
552}
553
554impl ApiKeyPlugin {
555 pub(super) fn virtual_session_from_key<S: alibi_core::AuthSchema>(
556 view: &ApiKeyView,
557 key: &str,
558 user: &S::User,
559 request: Option<&AuthRequest>,
560 ctx: &AuthContext<S>,
561 ) -> AuthResult<SessionView> {
562 let now = chrono::Utc::now();
563 let expires_at = match view.expires_at.as_deref() {
564 Some(value) => chrono::DateTime::parse_from_rfc3339(value)
565 .map_err(|error| {
566 AuthError::internal(format!("Invalid stored API key expiration: {error}"))
567 })?
568 .with_timezone(&chrono::Utc),
569 None => {
571 now + chrono::Duration::milliseconds(ctx.config.session.expires_in.num_seconds())
572 }
573 };
574 let ip_policy = request
577 .and_then(|request| {
578 request
579 .extensions()
580 .get::<alibi_core::config::IpAddressConfig>()
581 })
582 .unwrap_or_default();
583 let session = SessionView {
584 omitted_fields: std::collections::BTreeSet::default(),
585 active_team_id: None,
586 extension_fields: std::collections::BTreeMap::default(),
587 id: view.id.clone(),
588 token: key.to_owned(),
589 user_id: user.id().into_owned(),
590 created_at: now,
591 updated_at: now,
592 expires_at,
593 ip_address: request.and_then(|request| ip_policy.resolve_ip(&request.headers)),
594 user_agent: request.and_then(|request| {
595 request
596 .headers
597 .iter()
598 .find(|(name, _)| name.eq_ignore_ascii_case("user-agent"))
599 .map(|(_, value)| value.clone())
600 }),
601 impersonated_by: None,
602 active_organization_id: None,
603 active: true,
604 };
605 Ok(session)
606 }
607}
608
609impl std::fmt::Debug for VerifyApiKey<'_> {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 f.debug_struct("VerifyApiKey").finish_non_exhaustive()
612 }
613}