Skip to main content

rustauth_plugins/api_key/routes/
create.rs

1use http::{Method, StatusCode};
2use rustauth_core::crypto::random::generate_random_string;
3use rustauth_core::error::RustAuthError;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use time::OffsetDateTime;
7
8use super::{
9    body, current_identity, endpoint, error, future_expiration, json, metadata_is_object,
10    request_is_external, valid_prefix, SharedConfigurations,
11};
12use crate::api_key::cleanup;
13use crate::api_key::errors;
14use crate::api_key::hashing::{default_key_generator, default_key_hasher};
15use crate::api_key::models::{ApiKeyCreateRecord, ApiKeyRecord};
16use crate::api_key::options::{ApiKeyGeneratorInput, ApiKeyPermissions, ApiKeyReference};
17use crate::api_key::organization::{ensure_organization_permission, ApiKeyAction};
18use crate::api_key::storage::ApiKeyStore;
19
20#[derive(Debug, Clone, Deserialize, Serialize, Default)]
21#[serde(rename_all = "camelCase")]
22pub struct CreateApiKeyRequest {
23    pub config_id: Option<String>,
24    pub name: Option<String>,
25    pub expires_in: Option<i64>,
26    pub prefix: Option<String>,
27    pub remaining: Option<i64>,
28    pub metadata: Option<Value>,
29    pub refill_amount: Option<i64>,
30    pub refill_interval: Option<i64>,
31    pub rate_limit_time_window: Option<i64>,
32    pub rate_limit_max: Option<i64>,
33    pub rate_limit_enabled: Option<bool>,
34    pub permissions: Option<ApiKeyPermissions>,
35    pub user_id: Option<String>,
36    pub organization_id: Option<String>,
37}
38
39pub fn create_endpoint(
40    configurations: SharedConfigurations,
41) -> rustauth_core::api::AsyncAuthEndpoint {
42    endpoint(
43        "/api-key/create",
44        Method::POST,
45        configurations,
46        |context, request, configurations| async move {
47            let input: CreateApiKeyRequest = body(&request)?;
48            let options = configurations.resolve(input.config_id.as_deref())?;
49            let identity = current_identity(&context, &request).await?;
50            let is_external = request_is_external();
51            let reference_id = match options.reference {
52                ApiKeyReference::Organization => {
53                    let Some(organization_id) = input.organization_id.as_deref() else {
54                        return error(StatusCode::BAD_REQUEST, errors::ORGANIZATION_ID_REQUIRED);
55                    };
56                    let user_id = match identity.as_ref().map(|identity| identity.user.id.as_str())
57                    {
58                        Some(user_id) => user_id,
59                        // Only trusted server-side callers may name the actor explicitly.
60                        None if !is_external => match input.user_id.as_deref() {
61                            Some(user_id) => user_id,
62                            None => {
63                                return error(
64                                    StatusCode::UNAUTHORIZED,
65                                    errors::UNAUTHORIZED_SESSION,
66                                )
67                            }
68                        },
69                        None => {
70                            return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION)
71                        }
72                    };
73                    if let Err(error) = ensure_organization_permission(
74                        &context,
75                        user_id,
76                        organization_id,
77                        ApiKeyAction::Create,
78                    )
79                    .await
80                    {
81                        return error_response_from_rustauth(error);
82                    }
83                    organization_id.to_owned()
84                }
85                ApiKeyReference::User => {
86                    if let Some(identity) = &identity {
87                        if input
88                            .user_id
89                            .as_deref()
90                            .is_some_and(|user_id| user_id != identity.user.id)
91                        {
92                            return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
93                        }
94                        identity.user.id.clone()
95                    } else if !is_external {
96                        // Trusted server-side caller may target an explicit user id.
97                        match input.user_id.clone() {
98                            Some(user_id) => user_id,
99                            None => {
100                                return error(
101                                    StatusCode::UNAUTHORIZED,
102                                    errors::UNAUTHORIZED_SESSION,
103                                )
104                            }
105                        }
106                    } else {
107                        return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
108                    }
109                }
110            };
111
112            let uses_server_only_props = input.remaining.is_some()
113                || input.refill_amount.is_some()
114                || input.refill_interval.is_some()
115                || input.rate_limit_time_window.is_some()
116                || input.rate_limit_max.is_some()
117                || input.rate_limit_enabled.is_some()
118                || input.permissions.is_some();
119            if is_external && uses_server_only_props {
120                return error(StatusCode::BAD_REQUEST, errors::SERVER_ONLY_PROPERTY);
121            }
122
123            if let Err(code) = validate_input(&input, &options) {
124                return error(StatusCode::BAD_REQUEST, code);
125            }
126            let _ = cleanup::delete_all_expired_api_keys(&context, &options, false).await;
127
128            let prefix = input
129                .prefix
130                .as_deref()
131                .or(options.default_prefix.as_deref());
132            let key = match &options.custom_key_generator {
133                Some(generator) => {
134                    generator(ApiKeyGeneratorInput {
135                        length: options.default_key_length,
136                        prefix: prefix.map(str::to_owned),
137                    })
138                    .await?
139                }
140                None => default_key_generator(options.default_key_length, prefix),
141            };
142            let hashed = if options.disable_key_hashing {
143                key.clone()
144            } else {
145                default_key_hasher(&key)
146            };
147            let now = OffsetDateTime::now_utc();
148            let start = options.starting_characters.should_store.then(|| {
149                key.chars()
150                    .take(options.starting_characters.characters_length)
151                    .collect::<String>()
152            });
153            let expires_at = input
154                .expires_in
155                .and_then(|seconds| (seconds > 0).then_some(seconds))
156                .or(options
157                    .key_expiration
158                    .default_expires_in
159                    .map(|duration| duration.whole_seconds()))
160                .and_then(|seconds| {
161                    (seconds > 0)
162                        .then(|| future_expiration(Some(seconds)))
163                        .flatten()
164                });
165            let config_id = options
166                .config_id
167                .clone()
168                .unwrap_or_else(|| "default".to_owned());
169            let default_permissions = if let Some(resolver) = &options.default_permissions_resolver
170            {
171                resolver(&context, &reference_id).await?
172            } else {
173                options.default_permissions.clone()
174            };
175            let record = ApiKeyRecord {
176                id: generate_random_string(32),
177                config_id,
178                name: input.name.clone(),
179                start,
180                prefix: prefix.map(str::to_owned),
181                key: hashed,
182                reference_id,
183                refill_interval: input.refill_interval,
184                refill_amount: input.refill_amount,
185                last_refill_at: None,
186                enabled: true,
187                rate_limit_enabled: input
188                    .rate_limit_enabled
189                    .unwrap_or(options.rate_limit.enabled),
190                rate_limit_time_window: Some(
191                    input
192                        .rate_limit_time_window
193                        .unwrap_or(options.rate_limit.time_window.whole_milliseconds() as i64),
194                ),
195                rate_limit_max: Some(
196                    input
197                        .rate_limit_max
198                        .unwrap_or(options.rate_limit.max_requests),
199                ),
200                request_count: 0,
201                remaining: input.remaining,
202                last_request: None,
203                expires_at,
204                created_at: now,
205                updated_at: now,
206                metadata: input.metadata.clone(),
207                permissions: input.permissions.clone().or(default_permissions),
208            };
209            let created = ApiKeyStore::new(&context, &options).create(record).await?;
210            json(
211                StatusCode::OK,
212                &ApiKeyCreateRecord {
213                    record: created.public(),
214                    key,
215                },
216            )
217        },
218    )
219}
220
221fn validate_input(
222    input: &CreateApiKeyRequest,
223    options: &crate::api_key::options::ApiKeyConfiguration,
224) -> Result<(), &'static str> {
225    if let Some(metadata) = &input.metadata {
226        if !options.enable_metadata {
227            return Err(errors::METADATA_DISABLED);
228        }
229        if !metadata_is_object(&Some(metadata.clone())) {
230            return Err(errors::INVALID_METADATA_TYPE);
231        }
232    }
233    if input.refill_amount.is_some() && input.refill_interval.is_none() {
234        return Err(errors::REFILL_AMOUNT_AND_INTERVAL_REQUIRED);
235    }
236    if input.refill_interval.is_some() && input.refill_amount.is_none() {
237        return Err(errors::REFILL_INTERVAL_AND_AMOUNT_REQUIRED);
238    }
239    if let Some(expires_in) = input.expires_in {
240        if options.key_expiration.disable_custom_expires_time {
241            return Err(errors::KEY_DISABLED_EXPIRATION);
242        }
243        let days = expires_in / (60 * 60 * 24);
244        if days < options.key_expiration.min_expires_in_days {
245            return Err(errors::EXPIRES_IN_IS_TOO_SMALL);
246        }
247        if days > options.key_expiration.max_expires_in_days {
248            return Err(errors::EXPIRES_IN_IS_TOO_LARGE);
249        }
250    }
251    if let Some(prefix) = &input.prefix {
252        if !valid_prefix(prefix)
253            || prefix.len() < options.minimum_prefix_length
254            || prefix.len() > options.maximum_prefix_length
255        {
256            return Err(errors::INVALID_PREFIX_LENGTH);
257        }
258    }
259    if let Some(name) = &input.name {
260        if name.len() < options.minimum_name_length || name.len() > options.maximum_name_length {
261            return Err(errors::INVALID_NAME_LENGTH);
262        }
263    } else if options.require_name {
264        return Err(errors::NAME_REQUIRED);
265    }
266    Ok(())
267}
268
269fn error_response_from_rustauth(
270    error: RustAuthError,
271) -> Result<rustauth_core::api::ApiResponse, RustAuthError> {
272    let message = error.to_string();
273    if message.contains(errors::message(errors::USER_NOT_MEMBER_OF_ORGANIZATION)) {
274        return super::error(
275            StatusCode::FORBIDDEN,
276            errors::USER_NOT_MEMBER_OF_ORGANIZATION,
277        );
278    }
279    if message.contains(errors::message(errors::ORGANIZATION_PLUGIN_REQUIRED)) {
280        return super::error(
281            StatusCode::INTERNAL_SERVER_ERROR,
282            errors::ORGANIZATION_PLUGIN_REQUIRED,
283        );
284    }
285    super::error(
286        StatusCode::FORBIDDEN,
287        errors::INSUFFICIENT_API_KEY_PERMISSIONS,
288    )
289}