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