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