Skip to main content

rustauth_plugins/api_key/routes/
verify.rs

1use 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}