Skip to main content

openauth_plugins/api_key/routes/
create.rs

1use http::{Method, StatusCode};
2use openauth_core::crypto::random::generate_random_string;
3use openauth_core::error::OpenAuthError;
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    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) -> openauth_core::api::AsyncAuthEndpoint {
42    endpoint(
43        "/api-key/create",
44        Method::POST,
45        configurations,
46        |context, request, configurations| {
47            Box::pin(async move {
48                let input: CreateApiKeyRequest = body(&request)?;
49                let options = configurations.resolve(input.config_id.as_deref())?;
50                let identity = current_identity(context, &request).await?;
51                let reference_id = match options.reference {
52                    ApiKeyReference::Organization => {
53                        let Some(organization_id) = input.organization_id.as_deref() else {
54                            return error(
55                                StatusCode::BAD_REQUEST,
56                                errors::ORGANIZATION_ID_REQUIRED,
57                            );
58                        };
59                        let user_id = identity
60                            .as_ref()
61                            .map(|identity| identity.user.id.as_str())
62                            .or(input.user_id.as_deref())
63                            .ok_or_else(|| {
64                                OpenAuthError::Api(
65                                    errors::message(errors::UNAUTHORIZED_SESSION).to_owned(),
66                                )
67                            })?;
68                        if let Err(error) = ensure_organization_permission(
69                            context,
70                            user_id,
71                            organization_id,
72                            ApiKeyAction::Create,
73                        )
74                        .await
75                        {
76                            return error_response_from_openauth(error);
77                        }
78                        organization_id.to_owned()
79                    }
80                    ApiKeyReference::User => {
81                        if let Some(identity) = &identity {
82                            if input
83                                .user_id
84                                .as_deref()
85                                .is_some_and(|user_id| user_id != identity.user.id)
86                            {
87                                return error(
88                                    StatusCode::UNAUTHORIZED,
89                                    errors::UNAUTHORIZED_SESSION,
90                                );
91                            }
92                            identity.user.id.clone()
93                        } else if let Some(user_id) = input.user_id.clone() {
94                            user_id
95                        } else {
96                            return error(StatusCode::UNAUTHORIZED, errors::UNAUTHORIZED_SESSION);
97                        }
98                    }
99                };
100
101                if input.remaining.is_some()
102                    || input.refill_amount.is_some()
103                    || input.refill_interval.is_some()
104                    || input.rate_limit_time_window.is_some()
105                    || input.rate_limit_max.is_some()
106                    || input.rate_limit_enabled.is_some()
107                    || input.permissions.is_some()
108                {
109                    let has_cookie = request.headers().contains_key(http::header::COOKIE);
110                    if has_cookie {
111                        return error(StatusCode::BAD_REQUEST, errors::SERVER_ONLY_PROPERTY);
112                    }
113                }
114
115                if let Err(code) = validate_input(&input, &options) {
116                    return error(StatusCode::BAD_REQUEST, code);
117                }
118                let _ = cleanup::delete_all_expired_api_keys(context, &options, false).await;
119
120                let prefix = input
121                    .prefix
122                    .as_deref()
123                    .or(options.default_prefix.as_deref());
124                let key = match &options.custom_key_generator {
125                    Some(generator) => {
126                        generator(ApiKeyGeneratorInput {
127                            length: options.default_key_length,
128                            prefix: prefix.map(str::to_owned),
129                        })
130                        .await?
131                    }
132                    None => default_key_generator(options.default_key_length, prefix),
133                };
134                let hashed = if options.disable_key_hashing {
135                    key.clone()
136                } else {
137                    default_key_hasher(&key)
138                };
139                let now = OffsetDateTime::now_utc();
140                let start = options.starting_characters.should_store.then(|| {
141                    key.chars()
142                        .take(options.starting_characters.characters_length)
143                        .collect::<String>()
144                });
145                let expires_at = input
146                    .expires_in
147                    .and_then(|seconds| (seconds > 0).then_some(seconds))
148                    .or(options.key_expiration.default_expires_in)
149                    .and_then(|seconds| {
150                        (seconds > 0)
151                            .then(|| future_expiration(Some(seconds)))
152                            .flatten()
153                    });
154                let config_id = options
155                    .config_id
156                    .clone()
157                    .unwrap_or_else(|| "default".to_owned());
158                let record = ApiKeyRecord {
159                    id: generate_random_string(32),
160                    config_id,
161                    name: input.name.clone(),
162                    start,
163                    prefix: prefix.map(str::to_owned),
164                    key: hashed,
165                    reference_id,
166                    refill_interval: input.refill_interval,
167                    refill_amount: input.refill_amount,
168                    last_refill_at: None,
169                    enabled: true,
170                    rate_limit_enabled: input
171                        .rate_limit_enabled
172                        .unwrap_or(options.rate_limit.enabled),
173                    rate_limit_time_window: Some(
174                        input
175                            .rate_limit_time_window
176                            .unwrap_or(options.rate_limit.time_window),
177                    ),
178                    rate_limit_max: Some(
179                        input
180                            .rate_limit_max
181                            .unwrap_or(options.rate_limit.max_requests),
182                    ),
183                    request_count: 0,
184                    remaining: input.remaining.or(input.refill_amount),
185                    last_request: None,
186                    expires_at,
187                    created_at: now,
188                    updated_at: now,
189                    metadata: input.metadata.clone(),
190                    permissions: input
191                        .permissions
192                        .clone()
193                        .or_else(|| options.default_permissions.clone()),
194                };
195                let created = ApiKeyStore::new(context, &options).create(record).await?;
196                json(
197                    StatusCode::OK,
198                    &ApiKeyCreateRecord {
199                        record: created.public(),
200                        key,
201                    },
202                )
203            })
204        },
205    )
206}
207
208fn validate_input(
209    input: &CreateApiKeyRequest,
210    options: &crate::api_key::options::ApiKeyConfiguration,
211) -> Result<(), &'static str> {
212    if let Some(metadata) = &input.metadata {
213        if !options.enable_metadata {
214            return Err(errors::METADATA_DISABLED);
215        }
216        if !metadata_is_object(&Some(metadata.clone())) {
217            return Err(errors::INVALID_METADATA_TYPE);
218        }
219    }
220    if input.refill_amount.is_some() && input.refill_interval.is_none() {
221        return Err(errors::REFILL_AMOUNT_AND_INTERVAL_REQUIRED);
222    }
223    if input.refill_interval.is_some() && input.refill_amount.is_none() {
224        return Err(errors::REFILL_INTERVAL_AND_AMOUNT_REQUIRED);
225    }
226    if let Some(expires_in) = input.expires_in {
227        if options.key_expiration.disable_custom_expires_time {
228            return Err(errors::KEY_DISABLED_EXPIRATION);
229        }
230        let days = expires_in / (60 * 60 * 24);
231        if days < options.key_expiration.min_expires_in_days {
232            return Err(errors::EXPIRES_IN_IS_TOO_SMALL);
233        }
234        if days > options.key_expiration.max_expires_in_days {
235            return Err(errors::EXPIRES_IN_IS_TOO_LARGE);
236        }
237    }
238    if let Some(prefix) = &input.prefix {
239        if !valid_prefix(prefix)
240            || prefix.len() < options.minimum_prefix_length
241            || prefix.len() > options.maximum_prefix_length
242        {
243            return Err(errors::INVALID_PREFIX_LENGTH);
244        }
245    }
246    if let Some(name) = &input.name {
247        if name.len() < options.minimum_name_length || name.len() > options.maximum_name_length {
248            return Err(errors::INVALID_NAME_LENGTH);
249        }
250    } else if options.require_name {
251        return Err(errors::NAME_REQUIRED);
252    }
253    Ok(())
254}
255
256fn error_response_from_openauth(
257    error: OpenAuthError,
258) -> Result<openauth_core::api::ApiResponse, OpenAuthError> {
259    let message = error.to_string();
260    if message.contains(errors::message(errors::USER_NOT_MEMBER_OF_ORGANIZATION)) {
261        return super::error(
262            StatusCode::FORBIDDEN,
263            errors::USER_NOT_MEMBER_OF_ORGANIZATION,
264        );
265    }
266    if message.contains(errors::message(errors::ORGANIZATION_PLUGIN_REQUIRED)) {
267        return super::error(
268            StatusCode::INTERNAL_SERVER_ERROR,
269            errors::ORGANIZATION_PLUGIN_REQUIRED,
270        );
271    }
272    super::error(
273        StatusCode::FORBIDDEN,
274        errors::INSUFFICIENT_API_KEY_PERMISSIONS,
275    )
276}