Skip to main content

openauth_plugins/api_key/routes/
verify.rs

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