authx_plugins/api_key/
service.rs1use chrono::{DateTime, Duration, Utc};
2use rand::Rng;
3use tracing::instrument;
4use uuid::Uuid;
5
6use authx_core::{
7 crypto::sha256_hex,
8 error::{AuthError, Result},
9 models::{ApiKey, CreateApiKey},
10};
11use authx_storage::ports::{ApiKeyRepository, UserRepository};
12
13const MAX_KEY_TTL: Duration = Duration::days(365);
15
16#[derive(Debug)]
18pub struct ApiKeyResponse {
19 pub key: ApiKey,
20 pub raw_key: String,
21}
22
23pub struct ApiKeyService<S> {
24 storage: S,
25}
26
27impl<S> ApiKeyService<S>
28where
29 S: UserRepository + ApiKeyRepository + Clone + Send + Sync + 'static,
30{
31 pub fn new(storage: S) -> Self {
32 Self { storage }
33 }
34
35 #[instrument(skip(self), fields(user_id = %user_id))]
41 pub async fn create(
42 &self,
43 user_id: Uuid,
44 org_id: Option<Uuid>,
45 name: String,
46 scopes: Vec<String>,
47 expires_at: DateTime<Utc>,
48 ) -> Result<ApiKeyResponse> {
49 let now = Utc::now();
50 if expires_at <= now {
51 return Err(AuthError::Internal(
52 "api key expiry must be in the future".into(),
53 ));
54 }
55 let max_expiry = now + MAX_KEY_TTL;
56 if expires_at > max_expiry {
57 return Err(AuthError::Internal(format!(
58 "api key expiry exceeds maximum allowed ({} days)",
59 MAX_KEY_TTL.num_days()
60 )));
61 }
62
63 UserRepository::find_by_id(&self.storage, user_id)
65 .await?
66 .ok_or(AuthError::UserNotFound)?;
67
68 let raw: [u8; 32] = rand::thread_rng().r#gen();
69 let raw_key = hex::encode(raw);
70 let key_hash = sha256_hex(raw_key.as_bytes());
71 let prefix = raw_key[..8].to_owned();
72
73 let key = ApiKeyRepository::create(
74 &self.storage,
75 CreateApiKey {
76 user_id,
77 org_id,
78 key_hash,
79 prefix,
80 name,
81 scopes,
82 expires_at: Some(expires_at),
83 },
84 )
85 .await?;
86
87 tracing::info!(user_id = %user_id, key_id = %key.id, "api key created");
88 Ok(ApiKeyResponse { key, raw_key })
89 }
90
91 #[instrument(skip(self), fields(user_id = %user_id))]
93 pub async fn list(&self, user_id: Uuid) -> Result<Vec<ApiKey>> {
94 let keys = ApiKeyRepository::find_by_user(&self.storage, user_id).await?;
95 tracing::debug!(user_id = %user_id, count = keys.len(), "api keys listed");
96 Ok(keys)
97 }
98
99 #[instrument(skip(self), fields(user_id = %user_id, key_id = %key_id))]
101 pub async fn revoke(&self, user_id: Uuid, key_id: Uuid) -> Result<()> {
102 ApiKeyRepository::revoke(&self.storage, key_id, user_id).await?;
103 tracing::info!(user_id = %user_id, key_id = %key_id, "api key revoked");
104 Ok(())
105 }
106
107 #[instrument(skip(self, raw_key))]
112 pub async fn authenticate(&self, raw_key: &str) -> Result<ApiKey> {
113 let key_hash = sha256_hex(raw_key.as_bytes());
114 let key = ApiKeyRepository::find_by_hash(&self.storage, &key_hash)
115 .await?
116 .ok_or(AuthError::InvalidToken)?;
117
118 if let Some(exp) = key.expires_at
119 && exp < Utc::now()
120 {
121 tracing::warn!(key_id = %key.id, "api key expired");
122 return Err(AuthError::InvalidToken);
123 }
124
125 let now = Utc::now();
126 ApiKeyRepository::touch_last_used(&self.storage, key.id, now).await?;
127 tracing::info!(key_id = %key.id, user_id = %key.user_id, "api key authenticated");
128 Ok(ApiKey {
129 last_used_at: Some(now),
130 ..key
131 })
132 }
133}