Skip to main content

alibi_core/store/
oauth_clients.rs

1//! Durable OAuth provider clients, grants, and resource policy.
2use crate::{AuthError, AuthResult};
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use serde_json::Value;
6#[derive(Clone, Debug)]
7pub struct OAuthClient {
8    pub id: String,
9    pub client_id: String,
10    pub user_id: Option<String>,
11    pub client_secret: Option<String>,
12    pub metadata: Value,
13    pub created_at: Option<DateTime<Utc>>,
14    pub updated_at: Option<DateTime<Utc>>,
15}
16#[derive(Clone, Debug)]
17pub struct CreateOAuthClient {
18    pub client_id: String,
19    pub user_id: Option<String>,
20    pub client_secret: Option<String>,
21    pub metadata: Value,
22    pub created_at: DateTime<Utc>,
23}
24/// Durable token kind; refresh and opaque access tokens have distinct lifecycles.
25#[derive(Clone, Copy, Debug)]
26pub enum OAuthTokenKind {
27    Access,
28    Refresh,
29}
30#[derive(Clone, Debug)]
31pub struct OAuthToken {
32    pub id: String,
33    pub token: String,
34    pub client_id: String,
35    pub user_id: Option<String>,
36    pub session_id: Option<String>,
37    pub authorization_code_id: Option<String>,
38    pub metadata: Value,
39    pub created_at: DateTime<Utc>,
40    pub expires_at: DateTime<Utc>,
41}
42#[derive(Clone, Debug)]
43pub struct CreateOAuthToken {
44    pub token: String,
45    pub client_id: String,
46    pub user_id: Option<String>,
47    pub session_id: Option<String>,
48    pub authorization_code_id: Option<String>,
49    pub metadata: Value,
50    pub created_at: DateTime<Utc>,
51    pub expires_at: DateTime<Utc>,
52}
53#[async_trait]
54pub trait OAuthClientStore: Send + Sync {
55    /// Claim default refresh rotation with both revocation and rotation timestamps.
56    async fn store_oauth_refresh_replay(&self, id: &str, ciphertext: &str) -> AuthResult<()> {
57        let _ = (id, ciphertext);
58        Err(AuthError::config(
59            "OAuth token persistence is not supported by this store",
60        ))
61    }
62    async fn claim_oauth_refresh_rotation(
63        &self,
64        _token: &str,
65        _at: DateTime<Utc>,
66        _replay_expires: Option<DateTime<Utc>>,
67    ) -> AuthResult<bool> {
68        Err(AuthError::config(
69            "OAuth token persistence is not supported by this store",
70        ))
71    }
72
73    async fn list_oauth_tokens_for_session(
74        &self,
75        _kind: OAuthTokenKind,
76        _session_id: &str,
77    ) -> AuthResult<Vec<OAuthToken>> {
78        Err(AuthError::config(
79            "OAuth token persistence is not supported by this store",
80        ))
81    }
82    async fn mark_oauth_token_revoked(
83        &self,
84        _kind: OAuthTokenKind,
85        _id: &str,
86        _at: DateTime<Utc>,
87    ) -> AuthResult<()> {
88        Err(AuthError::config(
89            "OAuth token persistence is not supported by this store",
90        ))
91    }
92
93    async fn delete_oauth_token(&self, _kind: OAuthTokenKind, _token: &str) -> AuthResult<()> {
94        Err(AuthError::config(
95            "OAuth token persistence is not supported by this store",
96        ))
97    }
98    /// Atomically claim an unrevoked refresh token; concurrent claimants lose.
99    async fn revoke_oauth_refresh_token(
100        &self,
101        _token: &str,
102        _revoked_at: DateTime<Utc>,
103    ) -> AuthResult<bool> {
104        Err(AuthError::config(
105            "OAuth token persistence is not supported by this store",
106        ))
107    }
108    async fn delete_oauth_access_tokens_for_refresh(&self, _refresh_id: &str) -> AuthResult<()> {
109        Err(AuthError::config(
110            "OAuth token persistence is not supported by this store",
111        ))
112    }
113    async fn delete_oauth_refresh_family(
114        &self,
115        _client_id: &str,
116        _user_id: &str,
117    ) -> AuthResult<()> {
118        Err(AuthError::config(
119            "OAuth token persistence is not supported by this store",
120        ))
121    }
122    async fn create_oauth_token(
123        &self,
124        _kind: OAuthTokenKind,
125        _data: CreateOAuthToken,
126    ) -> AuthResult<OAuthToken> {
127        Err(AuthError::config(
128            "OAuth token persistence is not supported by this store",
129        ))
130    }
131    async fn get_oauth_token(
132        &self,
133        _kind: OAuthTokenKind,
134        _token: &str,
135    ) -> AuthResult<Option<OAuthToken>> {
136        Err(AuthError::config(
137            "OAuth token persistence is not supported by this store",
138        ))
139    }
140    async fn delete_oauth_tokens_for_code(&self, _code: &str) -> AuthResult<()> {
141        Err(AuthError::config(
142            "OAuth token persistence is not supported by this store",
143        ))
144    }
145
146    async fn delete_oauth_client_resource(
147        &self,
148        _client_id: &str,
149        _resource_id: &str,
150    ) -> AuthResult<()> {
151        Err(AuthError::config(
152            "OAuth resource linkage is not supported by this store",
153        ))
154    }
155    async fn create_oauth_client_resource(
156        &self,
157        _data: CreateOAuthClientResource,
158    ) -> AuthResult<OAuthClientResource> {
159        Err(AuthError::config(
160            "OAuth resource linkage is not supported by this store",
161        ))
162    }
163    async fn delete_oauth_resource(&self, _identifier: &str) -> AuthResult<()> {
164        Err(AuthError::config(
165            "OAuth resource persistence is not supported by this store",
166        ))
167    }
168    async fn update_oauth_resource(
169        &self,
170        _identifier: &str,
171        _metadata: Value,
172        _updated_at: DateTime<Utc>,
173    ) -> AuthResult<Option<OAuthResource>> {
174        Err(AuthError::config(
175            "OAuth resource persistence is not supported by this store",
176        ))
177    }
178    async fn list_oauth_resources(&self) -> AuthResult<Vec<OAuthResource>> {
179        Err(AuthError::config(
180            "OAuth resource persistence is not supported by this store",
181        ))
182    }
183    async fn list_oauth_client_resources(
184        &self,
185        _client_id: &str,
186    ) -> AuthResult<Vec<OAuthClientResource>> {
187        Err(AuthError::config(
188            "OAuth resource linkage is not supported by this store",
189        ))
190    }
191    async fn create_oauth_resource(&self, _data: CreateOAuthResource) -> AuthResult<OAuthResource> {
192        Err(AuthError::config(
193            "OAuth resource persistence is not supported by this store",
194        ))
195    }
196    async fn get_oauth_resource(&self, _identifier: &str) -> AuthResult<Option<OAuthResource>> {
197        Err(AuthError::config(
198            "OAuth resource persistence is not supported by this store",
199        ))
200    }
201    async fn delete_oauth_consent(&self, _id: &str) -> AuthResult<()> {
202        Err(AuthError::config(
203            "OAuth consent persistence is not supported by this store",
204        ))
205    }
206    async fn get_oauth_consent(&self, _id: &str) -> AuthResult<Option<OAuthConsent>> {
207        Err(AuthError::config(
208            "OAuth consent persistence is not supported by this store",
209        ))
210    }
211    async fn create_oauth_consent(&self, _data: CreateOAuthConsent) -> AuthResult<OAuthConsent> {
212        Err(AuthError::config(
213            "OAuth consent persistence is not supported by this store",
214        ))
215    }
216    async fn list_oauth_consents_by_user(&self, _user_id: &str) -> AuthResult<Vec<OAuthConsent>> {
217        Err(AuthError::config(
218            "OAuth consent persistence is not supported by this store",
219        ))
220    }
221    async fn update_oauth_consent_metadata(
222        &self,
223        _id: &str,
224        _metadata: Value,
225        _updated_at: DateTime<Utc>,
226    ) -> AuthResult<Option<OAuthConsent>> {
227        Err(AuthError::config(
228            "OAuth consent persistence is not supported by this store",
229        ))
230    }
231    async fn create_oauth_client(&self, _data: CreateOAuthClient) -> AuthResult<OAuthClient> {
232        Err(AuthError::config(
233            "OAuth client persistence is not supported by this store",
234        ))
235    }
236    async fn get_oauth_client(&self, _client_id: &str) -> AuthResult<Option<OAuthClient>> {
237        Err(AuthError::config(
238            "OAuth client persistence is not supported by this store",
239        ))
240    }
241    async fn list_oauth_clients_by_user(&self, _user_id: &str) -> AuthResult<Vec<OAuthClient>> {
242        Err(AuthError::config(
243            "OAuth client persistence is not supported by this store",
244        ))
245    }
246    async fn delete_oauth_client(&self, _client_id: &str) -> AuthResult<()> {
247        Err(AuthError::config(
248            "OAuth client persistence is not supported by this store",
249        ))
250    }
251    async fn update_oauth_client_secret(
252        &self,
253        _client_id: &str,
254        _secret: String,
255        _updated_at: DateTime<Utc>,
256    ) -> AuthResult<Option<OAuthClient>> {
257        Err(AuthError::config(
258            "OAuth client persistence is not supported by this store",
259        ))
260    }
261    async fn update_oauth_client_metadata(
262        &self,
263        _client_id: &str,
264        _metadata: Value,
265        _updated_at: DateTime<Utc>,
266    ) -> AuthResult<Option<OAuthClient>> {
267        Err(AuthError::config(
268            "OAuth client persistence is not supported by this store",
269        ))
270    }
271}
272
273/// RFC client metadata names mapped to the TypeScript persistence schema.
274const CLIENT_FIELDS: &[(&str, &str)] = &[
275    ("client_discovery_id", "clientDiscoveryId"),
276    ("disabled", "disabled"),
277    ("skip_consent", "skipConsent"),
278    ("enable_end_session", "enableEndSession"),
279    ("subject_type", "subjectType"),
280    ("client_credentials_scopes", "clientCredentialsScopes"),
281    ("contacts", "contacts"),
282    ("software_id", "softwareId"),
283    ("software_version", "softwareVersion"),
284    ("software_statement", "softwareStatement"),
285    ("redirect_uris", "redirectUris"),
286    ("post_logout_redirect_uris", "postLogoutRedirectUris"),
287    ("backchannel_logout_uri", "backchannelLogoutUri"),
288    (
289        "backchannel_logout_session_required",
290        "backchannelLogoutSessionRequired",
291    ),
292    ("token_endpoint_auth_method", "tokenEndpointAuthMethod"),
293    ("application_type", "applicationType"),
294    ("jwks", "jwks"),
295    ("jwks_uri", "jwksUri"),
296    ("grant_types", "grantTypes"),
297    ("response_types", "responseTypes"),
298    ("require_pkce", "requirePKCE"),
299    ("dpop_bound_access_tokens", "dpopBoundAccessTokens"),
300    ("reference_id", "referenceId"),
301    ("client_name", "name"),
302    ("client_uri", "uri"),
303    ("logo_uri", "icon"),
304    ("tos_uri", "tos"),
305    ("policy_uri", "policy"),
306    ("scope", "scopes"),
307];
308
309/// Separate registration fields from application metadata for physical persistence.
310/// # Errors
311/// Rejects metadata that is not an object.
312pub fn metadata_to_columns(metadata: Value) -> AuthResult<Value> {
313    let Value::Object(mut metadata) = metadata else {
314        return Err(AuthError::bad_request(
315            "OAuth client metadata must be an object",
316        ));
317    };
318    let mut fields = serde_json::Map::new();
319    for (rfc, column) in CLIENT_FIELDS {
320        if let Some(mut value) = metadata.remove(*rfc) {
321            if *rfc == "scope"
322                && let Some(scope) = value.as_str()
323            {
324                value = Value::Array(
325                    scope
326                        .split_whitespace()
327                        .map(|scope| Value::String(scope.into()))
328                        .collect(),
329                );
330            }
331            if *rfc == "jwks" && value.is_object() {
332                value = Value::String(serde_json::to_string(&value)?);
333            }
334            _ = fields.insert((*column).into(), value);
335        }
336    }
337    if !fields.contains_key("redirectUris") {
338        _ = fields.insert("redirectUris".into(), serde_json::json!([]));
339    }
340    _ = fields.insert(
341        "metadata".into(),
342        if metadata.is_empty() {
343            Value::Null
344        } else {
345            Value::Object(metadata)
346        },
347    );
348    Ok(Value::Object(fields))
349}
350
351/// Rebuild the registration projection from the TypeScript client columns.
352/// # Errors
353/// Returns an error when a stored client is malformed.
354pub fn metadata_from_columns(fields: &Value) -> AuthResult<Value> {
355    let fields = fields
356        .as_object()
357        .ok_or_else(|| AuthError::internal("Stored OAuth client must be an object"))?;
358    let mut metadata = fields
359        .get("metadata")
360        .and_then(Value::as_object)
361        .cloned()
362        .unwrap_or_default();
363    for (rfc, column) in CLIENT_FIELDS {
364        if let Some(mut value) = fields
365            .get(*column)
366            .filter(|value| !value.is_null())
367            .cloned()
368        {
369            if *rfc == "scope"
370                && let Some(scopes) = value.as_array()
371            {
372                value = Value::String(
373                    scopes
374                        .iter()
375                        .filter_map(Value::as_str)
376                        .collect::<Vec<_>>()
377                        .join(" "),
378                );
379            }
380            if *rfc == "jwks"
381                && let Some(jwks) = value.as_str()
382            {
383                value = serde_json::from_str(jwks)?;
384            }
385            _ = metadata.insert((*rfc).into(), value);
386        }
387    }
388    Ok(Value::Object(metadata))
389}
390
391/// Durable consent attached to the client and consenting user.
392#[derive(Clone, Debug)]
393pub struct OAuthConsent {
394    pub id: String,
395    pub client_id: String,
396    pub user_id: Option<String>,
397    pub metadata: Value,
398    pub created_at: DateTime<Utc>,
399    pub updated_at: DateTime<Utc>,
400}
401#[derive(Clone, Debug)]
402pub struct CreateOAuthConsent {
403    pub client_id: String,
404    pub user_id: Option<String>,
405    pub metadata: Value,
406    pub created_at: DateTime<Utc>,
407}
408
409#[derive(Clone, Debug)]
410pub struct OAuthResource {
411    pub id: String,
412    pub identifier: String,
413    pub metadata: Value,
414    pub created_at: Option<DateTime<Utc>>,
415    pub updated_at: Option<DateTime<Utc>>,
416}
417#[derive(Clone, Debug)]
418pub struct CreateOAuthResource {
419    pub identifier: String,
420    pub metadata: Value,
421    pub created_at: DateTime<Utc>,
422}
423
424#[derive(Clone, Debug)]
425pub struct OAuthClientResource {
426    pub id: String,
427    pub client_id: String,
428    pub resource_id: String,
429    pub metadata: Value,
430    pub created_at: Option<DateTime<Utc>>,
431}
432
433#[derive(Clone, Debug)]
434pub struct CreateOAuthClientResource {
435    pub client_id: String,
436    pub resource_id: String,
437    pub created_at: DateTime<Utc>,
438}