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