rustauth_plugins/api_key/routes/
create.rs1use 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 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 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}