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(
415 &self,
416 req: &AuthRequest,
417 ctx: &AuthContext<impl alibi_core::AuthSchema>,
418 ) -> AuthResult<Option<BeforeRequestAction>> {
419 if self
423 .find_session_key(req, ctx)
424 .map_err(|_error| AuthError::Api {
425 status: 500,
426 code: None,
427 message: "An error occurred during hook matcher execution. Check the logs for more details.".into(),
428 })?
429 .is_none()
430 {
431 return Ok(None);
432 }
433 let (config, key) = match self.find_session_key(req, ctx) {
434 Ok(Some(value)) => value,
435 Ok(None) => {
436 tracing::error!("API key getter did not return a key after matching");
437 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
438 }
439 Err(error)
440 if error.status_code() >= 500
441 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) =>
442 {
443 tracing::error!(error = %error, "API key getter failed");
444 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
445 }
446 Err(error) => return Err(error),
447 };
448
449 if f64::from(
450 u32::try_from(key.encode_utf16().count())
451 .map_err(|error| AuthError::internal(error.to_string()))?,
452 ) < config.key_length
453 {
454 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
455 403,
456 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
457 )?)));
458 }
459 if let Some(validator) = &config.custom_api_key_validator {
460 let valid = match validator
461 .validate(
462 &ApiKeyCallbackContext::new(Some(req), ctx, &config.config_id),
463 &key,
464 )
465 .await
466 {
467 Ok(valid) => valid,
468 Err(error)
469 if error.status_code() >= 500
470 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) =>
471 {
472 tracing::error!(error = %error, "API key validator failed");
473 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
474 }
475 Err(error) => return Err(error),
476 };
477 if !valid {
478 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
479 403,
480 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
481 )?)));
482 }
483 }
484 let input = VerifyApiKey {
485 key: &key,
486 config_id: Some(&config.config_id),
487 permissions: None,
488 };
489 let view = match self
490 .verify_api_key_checked(&input, Some(req), ctx, false)
491 .await
492 {
493 Ok(view) => view,
494 Err(ApiKeyVerificationError::Validation(error)) => {
495 return Ok(Some(BeforeRequestAction::Respond(error.response()?)));
496 }
497 Err(
498 ApiKeyVerificationError::Internal(error)
499 | ApiKeyVerificationError::ExplicitValidator(error),
500 ) => {
501 if error.status_code() >= 500
504 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. })
505 {
506 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
507 }
508 return Err(error);
509 }
510 };
511 if config.defer_updates {
512 let completion = self.start_configured_cleanup(ctx).await?;
513 if let Some(handler) = &ctx.config.background_tasks {
514 if let Err(error) = handler.handle(completion) {
515 if error.status_code() >= 500
516 && !matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. })
517 {
518 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::new(500))));
519 }
520 return Err(error);
521 }
522 } else {
523 drop(completion);
524 }
525 } else {
526 self.maybe_delete_expired(ctx).await?;
527 }
528
529 if config.references != ApiKeyReferences::User {
530 return Ok(Some(BeforeRequestAction::Respond(
531 ApiKeyValidationError::new(ApiKeyErrorCode::InvalidReferenceIdFromApiKey)
532 .response()?,
533 )));
534 }
535 let Some(user) = ctx.database.get_user_by_id(&view.reference_id).await? else {
536 return Ok(Some(BeforeRequestAction::Respond(
537 ApiKeyValidationError::new(ApiKeyErrorCode::InvalidReferenceIdFromApiKey)
538 .response()?,
539 )));
540 };
541
542 let session = Self::virtual_session_from_key(&view, &key, &user, Some(req), ctx)?;
543 if req.path() == "/get-session" {
545 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
546 200,
547 &serde_json::json!({
548 "user": ctx.user_view(&user),
549 "session": session,
550 }),
551 )?)));
552 }
553 Ok(Some(BeforeRequestAction::InjectSession { session }))
554 }
555}
556
557impl ApiKeyPlugin {
558 pub(super) fn virtual_session_from_key<S: alibi_core::AuthSchema>(
559 view: &ApiKeyView,
560 key: &str,
561 user: &S::User,
562 request: Option<&AuthRequest>,
563 ctx: &AuthContext<S>,
564 ) -> AuthResult<SessionView> {
565 let now = chrono::Utc::now();
566 let expires_at = match view.expires_at.as_deref() {
567 Some(value) => chrono::DateTime::parse_from_rfc3339(value)
568 .map_err(|error| {
569 AuthError::internal(format!("Invalid stored API key expiration: {error}"))
570 })?
571 .with_timezone(&chrono::Utc),
572 None => {
574 now + chrono::Duration::milliseconds(ctx.config.session.expires_in.num_seconds())
575 }
576 };
577 let ip_policy = request
580 .and_then(|request| {
581 request
582 .extensions()
583 .get::<alibi_core::config::IpAddressConfig>()
584 })
585 .unwrap_or_default();
586 let session = SessionView {
587 omitted_fields: std::collections::BTreeSet::default(),
588 active_team_id: None,
589 extension_fields: std::collections::BTreeMap::default(),
590 id: view.id.clone(),
591 token: key.to_owned(),
592 user_id: user.id().into_owned(),
593 created_at: now,
594 updated_at: now,
595 expires_at,
596 ip_address: request.and_then(|request| ip_policy.resolve_ip(&request.headers)),
597 user_agent: request.and_then(|request| {
598 request
599 .headers
600 .iter()
601 .find(|(name, _)| name.eq_ignore_ascii_case("user-agent"))
602 .map(|(_, value)| value.clone())
603 }),
604 impersonated_by: None,
605 active_organization_id: None,
606 active: true,
607 };
608 Ok(session)
609 }
610}
611
612impl std::fmt::Debug for VerifyApiKey<'_> {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 f.debug_struct("VerifyApiKey").finish_non_exhaustive()
615 }
616}