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