rustauth_plugins/api_key/routes/
verify.rs1use http::{Method, StatusCode};
2use rustauth_core::context::AuthContext;
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5
6use super::{body, endpoint, json, SharedConfigurations};
7use crate::api_key::cleanup;
8use crate::api_key::errors;
9use crate::api_key::hashing::default_key_hasher;
10use crate::api_key::models::{ApiKeyPublicRecord, ApiKeyRecord};
11use crate::api_key::options::{ApiKeyConfiguration, ApiKeyPermissions};
12use crate::api_key::permissions;
13use crate::api_key::rate_limit;
14use crate::api_key::storage::ApiKeyStore;
15
16const MAX_USAGE_UPDATE_ATTEMPTS: usize = 3;
17
18#[derive(Debug, Clone, Deserialize, Serialize, Default)]
19#[serde(rename_all = "camelCase")]
20pub struct VerifyApiKeyRequest {
21 pub config_id: Option<String>,
22 pub key: String,
23 pub permissions: Option<ApiKeyPermissions>,
24}
25
26#[derive(Debug, Clone, Serialize)]
27pub struct VerifyApiKeyResponse {
28 pub valid: bool,
29 pub error: Option<VerifyApiKeyErrorBody>,
30 pub key: Option<ApiKeyPublicRecord>,
31}
32
33#[derive(Debug, Clone, Serialize)]
34pub struct VerifyApiKeyErrorBody {
35 pub code: String,
36 pub message: String,
37 #[serde(skip_serializing_if = "Option::is_none", rename = "tryAgainIn")]
38 pub try_again_in: Option<i64>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ApiKeyValidationError {
43 pub code: &'static str,
44 pub status: StatusCode,
45 pub try_again_in: Option<i64>,
46}
47
48pub fn verify_endpoint(
49 configurations: SharedConfigurations,
50) -> rustauth_core::api::AsyncAuthEndpoint {
51 endpoint(
52 "/api-key/verify",
53 Method::POST,
54 configurations,
55 |context, request, configurations| async move {
56 let input: VerifyApiKeyRequest = body(&request)?;
57 let options = configurations.resolve(input.config_id.as_deref())?;
58 if let Some(validator) = &options.custom_api_key_validator {
59 if !validator(&context, &input.key).await? {
60 return json(
61 StatusCode::OK,
62 &VerifyApiKeyResponse {
63 valid: false,
64 error: Some(VerifyApiKeyErrorBody {
65 code: errors::INVALID_API_KEY.to_owned(),
66 message: errors::message(errors::INVALID_API_KEY).to_owned(),
67 try_again_in: None,
68 }),
69 key: None,
70 },
71 );
72 }
73 }
74 let hashed = if options.disable_key_hashing {
75 input.key
76 } else {
77 default_key_hasher(&input.key)
78 };
79 match validate_api_key(&context, &options, &hashed, input.permissions.as_ref()).await {
80 Ok(mut api_key) => {
81 if options.defer_updates {
82 let _ =
83 cleanup::delete_all_expired_api_keys(&context, &options, false).await;
84 }
85 ApiKeyStore::new(&context, &options)
86 .migrate_metadata_if_needed(&mut api_key)
87 .await;
88 json(
89 StatusCode::OK,
90 &VerifyApiKeyResponse {
91 valid: true,
92 error: None,
93 key: Some(api_key.public()),
94 },
95 )
96 }
97 Err(error) => json(
98 StatusCode::OK,
99 &VerifyApiKeyResponse {
100 valid: false,
101 error: Some(VerifyApiKeyErrorBody {
102 code: error.code.to_owned(),
103 message: errors::message(error.code).to_owned(),
104 try_again_in: error.try_again_in,
105 }),
106 key: None,
107 },
108 ),
109 }
110 },
111 )
112}
113
114pub async fn validate_api_key(
115 context: &AuthContext,
116 options: &ApiKeyConfiguration,
117 hashed_key: &str,
118 required_permissions: Option<&ApiKeyPermissions>,
119) -> Result<ApiKeyRecord, ApiKeyValidationError> {
120 let store = ApiKeyStore::new(context, options);
121 for attempt in 0..MAX_USAGE_UPDATE_ATTEMPTS {
122 let mut api_key = store
123 .get_by_hash(hashed_key)
124 .await
125 .map_err(|_| validation_error(errors::INVALID_API_KEY, StatusCode::UNAUTHORIZED))?
126 .ok_or_else(|| validation_error(errors::INVALID_API_KEY, StatusCode::UNAUTHORIZED))?;
127 let expected_updated_at = api_key.updated_at;
128 if !api_key.enabled {
129 return Err(validation_error(
130 errors::KEY_DISABLED,
131 StatusCode::UNAUTHORIZED,
132 ));
133 }
134 let now = OffsetDateTime::now_utc();
135 if api_key
136 .expires_at
137 .is_some_and(|expires_at| now > expires_at)
138 {
139 let _ = store.delete(&api_key).await;
140 return Err(validation_error(
141 errors::KEY_EXPIRED,
142 StatusCode::UNAUTHORIZED,
143 ));
144 }
145 if !permissions::allows(api_key.permissions.as_ref(), required_permissions) {
146 return Err(validation_error(
147 errors::KEY_NOT_FOUND,
148 StatusCode::UNAUTHORIZED,
149 ));
150 }
151 let mut remaining = api_key.remaining;
152 let mut last_refill_at = api_key.last_refill_at;
153 if api_key.remaining == Some(0) && api_key.refill_amount.is_none() {
154 let _ = store.delete(&api_key).await;
155 return Err(validation_error(
156 errors::USAGE_EXCEEDED,
157 StatusCode::TOO_MANY_REQUESTS,
158 ));
159 }
160 if let Some(current_remaining) = remaining {
161 if let (Some(refill_interval), Some(refill_amount)) =
162 (api_key.refill_interval, api_key.refill_amount)
163 {
164 let last = last_refill_at.unwrap_or(api_key.created_at);
165 if (now - last).whole_milliseconds() > i128::from(refill_interval) {
166 remaining = Some(refill_amount);
167 last_refill_at = Some(now);
168 }
169 }
170 if remaining == Some(0) {
171 return Err(validation_error(
172 errors::USAGE_EXCEEDED,
173 StatusCode::TOO_MANY_REQUESTS,
174 ));
175 }
176 if current_remaining > 0 || remaining.is_some_and(|value| value > 0) {
177 remaining = remaining.map(|value| value.saturating_sub(1));
178 }
179 }
180 let rate_limit = rate_limit::check(&api_key, options, now);
181 if !rate_limit.success {
182 return Err(ApiKeyValidationError {
183 code: errors::RATE_LIMIT_EXCEEDED,
184 status: StatusCode::UNAUTHORIZED,
185 try_again_in: rate_limit.try_again_in,
186 });
187 }
188 api_key.remaining = remaining;
189 api_key.last_refill_at = last_refill_at;
190 if let Some(last_request) = rate_limit.last_request {
191 api_key.last_request = Some(last_request);
192 }
193 if let Some(request_count) = rate_limit.request_count {
194 api_key.request_count = request_count;
195 }
196 api_key.updated_at = now;
197 if persist_api_key_update(context, options, &store, &api_key, expected_updated_at).await? {
198 store.migrate_metadata_if_needed(&mut api_key).await;
199 return Ok(api_key);
200 }
201 if attempt + 1 == MAX_USAGE_UPDATE_ATTEMPTS {
202 break;
203 }
204 }
205 Err(validation_error(
206 errors::FAILED_TO_UPDATE_API_KEY,
207 StatusCode::INTERNAL_SERVER_ERROR,
208 ))
209}
210
211async fn persist_api_key_update(
212 context: &AuthContext,
213 options: &ApiKeyConfiguration,
214 store: &ApiKeyStore<'_>,
215 api_key: &ApiKeyRecord,
216 expected_updated_at: OffsetDateTime,
217) -> Result<bool, ApiKeyValidationError> {
218 if options.defer_updates {
219 let updated = api_key.clone();
220 let options = options.clone();
221 let context = context.clone();
222 let task_context = context.clone();
223 if !context.run_background_task(Box::pin(async move {
224 let _ = ApiKeyStore::new(&task_context, &options)
225 .update_if_unchanged(&updated, expected_updated_at)
226 .await;
227 })) {
228 return store
229 .update_if_unchanged(api_key, expected_updated_at)
230 .await
231 .map(|updated| updated.is_some())
232 .map_err(|_| {
233 validation_error(
234 errors::FAILED_TO_UPDATE_API_KEY,
235 StatusCode::INTERNAL_SERVER_ERROR,
236 )
237 });
238 }
239 return Ok(true);
240 }
241 store
242 .update_if_unchanged(api_key, expected_updated_at)
243 .await
244 .map(|updated| updated.is_some())
245 .map_err(|_| {
246 validation_error(
247 errors::FAILED_TO_UPDATE_API_KEY,
248 StatusCode::INTERNAL_SERVER_ERROR,
249 )
250 })
251}
252
253fn validation_error(code: &'static str, status: StatusCode) -> ApiKeyValidationError {
254 ApiKeyValidationError {
255 code,
256 status,
257 try_again_in: None,
258 }
259}