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    /// Native standard-client fields and flattened opaque extensions. Extensions
13    /// colliding with native aliases remain under `metadata`, never as authority.
14    pub metadata: Value,
15    pub created_at: Option<DateTime<Utc>>,
16    pub updated_at: Option<DateTime<Utc>>,
17}
18#[derive(Clone, Debug)]
19pub struct CreateOAuthClient {
20    pub client_id: String,
21    pub user_id: Option<String>,
22    pub client_secret: Option<String>,
23    pub metadata: Value,
24    pub created_at: DateTime<Utc>,
25}
26/// One atomic client row write and additive default/requested resource linkage.
27/// Replacement metadata is a patch: omitted model fields survive, explicit nulls clear.
28#[derive(Clone, Debug)]
29pub struct PersistOAuthClientRegistration {
30    pub client: CreateOAuthClient,
31    /// `None` creates an ordinary or discovery-owned client without replacement authority.
32    pub replacement: Option<OAuthClientRegistrationReplacement>,
33    pub resources: Vec<String>,
34    pub updated_at: DateTime<Utc>,
35}
36
37/// Conditional replacement authority, used only for trusted discovery-owned clients.
38#[derive(Clone, Debug)]
39pub struct OAuthClientRegistrationReplacement {
40    pub discovery_id: String,
41    /// Replacement snapshot timestamp; absence is filtered out like an undefined adapter field.
42    pub existing_created_at: Option<DateTime<Utc>>,
43}
44
45#[derive(Debug, thiserror::Error)]
46pub enum OAuthClientRegistrationWriteError {
47    #[error(transparent)]
48    Failure(#[from] AuthError),
49    #[error("client registration collision: {cause}")]
50    Collision {
51        cause: AuthError,
52        resource_id: Option<String>,
53    },
54}
55impl OAuthClientRegistrationWriteError {
56    /// Bundled adapters retain unique diagnostics in their constraint contract.
57    #[must_use]
58    pub fn classify(cause: AuthError, resource_id: Option<String>) -> Self {
59        if matches!(&cause, AuthError::Database(crate::error::DatabaseError::Constraint(message)) if message.to_ascii_lowercase().contains("unique"))
60        {
61            Self::Collision { cause, resource_id }
62        } else {
63            Self::Failure(cause)
64        }
65    }
66}
67/// Durable token kind; refresh and opaque access tokens have distinct lifecycles.
68#[derive(Clone, Copy, Debug)]
69pub enum OAuthTokenKind {
70    Access,
71    Refresh,
72}
73#[derive(Clone, Debug)]
74pub struct OAuthToken {
75    pub id: String,
76    pub token: String,
77    pub client_id: String,
78    pub user_id: Option<String>,
79    pub session_id: Option<String>,
80    pub authorization_code_id: Option<String>,
81    pub metadata: Value,
82    pub created_at: DateTime<Utc>,
83    pub expires_at: DateTime<Utc>,
84}
85#[derive(Clone, Debug)]
86pub struct CreateOAuthToken {
87    pub token: String,
88    pub client_id: String,
89    pub user_id: Option<String>,
90    pub session_id: Option<String>,
91    pub authorization_code_id: Option<String>,
92    pub metadata: Value,
93    pub created_at: DateTime<Utc>,
94    pub expires_at: DateTime<Utc>,
95}
96#[async_trait]
97pub trait OAuthClientStore: Send + Sync {
98    /// Exact relation lookup, independent of the configured list page limit.
99    async fn has_oauth_client_resource(
100        &self,
101        _client_id: &str,
102        _resource_id: &str,
103    ) -> AuthResult<bool> {
104        Err(AuthError::config(
105            "OAuth resource linkage is not supported by this store",
106        ))
107    }
108    /// Atomically insert any client or conditionally update a discovery-owned client and ensure
109    /// its resource links. `None` means the replacement ownership predicate lost.
110    /// Existing links (including their metadata/timestamps) are never replaced.
111    async fn persist_oauth_client_registration(
112        &self,
113        _data: PersistOAuthClientRegistration,
114    ) -> Result<Option<OAuthClient>, OAuthClientRegistrationWriteError> {
115        Err(
116            AuthError::config("Atomic OAuth client registration is not supported by this store")
117                .into(),
118        )
119    }
120    /// Atomically claim a caller-supplied assertion digest; false means it already exists.
121    async fn claim_oauth_client_assertion(
122        &self,
123        id: &str,
124        expires_at: DateTime<Utc>,
125    ) -> AuthResult<bool> {
126        let _ = (id, expires_at);
127        Err(AuthError::config(
128            "OAuth assertion persistence is not supported by this store",
129        ))
130    }
131
132    /// Claim default refresh rotation with both revocation and rotation timestamps.
133    async fn store_oauth_refresh_replay(&self, id: &str, ciphertext: &str) -> AuthResult<()> {
134        let _ = (id, ciphertext);
135        Err(AuthError::config(
136            "OAuth token persistence is not supported by this store",
137        ))
138    }
139    /// Atomically rotate the retained row supplied to a trusted grant handler.
140    async fn claim_oauth_refresh_rotation_by_id(
141        &self,
142        id: &str,
143        at: DateTime<Utc>,
144        replay_expires: Option<DateTime<Utc>>,
145    ) -> AuthResult<bool> {
146        let _ = (id, at, replay_expires);
147        Err(AuthError::config(
148            "OAuth refresh rotation by ID is not supported by this store",
149        ))
150    }
151    async fn claim_oauth_refresh_rotation(
152        &self,
153        _token: &str,
154        _at: DateTime<Utc>,
155        _replay_expires: Option<DateTime<Utc>>,
156    ) -> AuthResult<bool> {
157        Err(AuthError::config(
158            "OAuth token persistence is not supported by this store",
159        ))
160    }
161
162    async fn list_oauth_tokens_for_session(
163        &self,
164        _kind: OAuthTokenKind,
165        _session_id: &str,
166    ) -> AuthResult<Vec<OAuthToken>> {
167        Err(AuthError::config(
168            "OAuth token persistence is not supported by this store",
169        ))
170    }
171    async fn mark_oauth_token_revoked(
172        &self,
173        _kind: OAuthTokenKind,
174        _id: &str,
175        _at: DateTime<Utc>,
176    ) -> AuthResult<()> {
177        Err(AuthError::config(
178            "OAuth token persistence is not supported by this store",
179        ))
180    }
181
182    async fn delete_oauth_token(&self, _kind: OAuthTokenKind, _token: &str) -> AuthResult<()> {
183        Err(AuthError::config(
184            "OAuth token persistence is not supported by this store",
185        ))
186    }
187    /// Atomically claim an unrevoked refresh token; concurrent claimants lose.
188    async fn revoke_oauth_refresh_token(
189        &self,
190        _token: &str,
191        _revoked_at: DateTime<Utc>,
192    ) -> AuthResult<bool> {
193        Err(AuthError::config(
194            "OAuth token persistence is not supported by this store",
195        ))
196    }
197    async fn delete_oauth_access_tokens_for_refresh(&self, _refresh_id: &str) -> AuthResult<()> {
198        Err(AuthError::config(
199            "OAuth token persistence is not supported by this store",
200        ))
201    }
202    async fn delete_oauth_refresh_family(
203        &self,
204        _client_id: &str,
205        _user_id: &str,
206    ) -> AuthResult<()> {
207        Err(AuthError::config(
208            "OAuth token persistence is not supported by this store",
209        ))
210    }
211    async fn create_oauth_token(
212        &self,
213        _kind: OAuthTokenKind,
214        _data: CreateOAuthToken,
215    ) -> AuthResult<OAuthToken> {
216        Err(AuthError::config(
217            "OAuth token persistence is not supported by this store",
218        ))
219    }
220    async fn get_oauth_token(
221        &self,
222        _kind: OAuthTokenKind,
223        _token: &str,
224    ) -> AuthResult<Option<OAuthToken>> {
225        Err(AuthError::config(
226            "OAuth token persistence is not supported by this store",
227        ))
228    }
229    async fn delete_oauth_tokens_for_code(&self, _code: &str) -> AuthResult<()> {
230        Err(AuthError::config(
231            "OAuth token persistence is not supported by this store",
232        ))
233    }
234
235    async fn delete_oauth_client_resource(
236        &self,
237        _client_id: &str,
238        _resource_id: &str,
239    ) -> AuthResult<()> {
240        Err(AuthError::config(
241            "OAuth resource linkage is not supported by this store",
242        ))
243    }
244    async fn create_oauth_client_resource(
245        &self,
246        _data: CreateOAuthClientResource,
247    ) -> AuthResult<OAuthClientResource> {
248        Err(AuthError::config(
249            "OAuth resource linkage is not supported by this store",
250        ))
251    }
252    async fn delete_oauth_resource(&self, _identifier: &str) -> AuthResult<()> {
253        Err(AuthError::config(
254            "OAuth resource persistence is not supported by this store",
255        ))
256    }
257    async fn update_oauth_resource(
258        &self,
259        _identifier: &str,
260        _metadata: Value,
261        _updated_at: DateTime<Utc>,
262    ) -> AuthResult<Option<OAuthResource>> {
263        Err(AuthError::config(
264            "OAuth resource persistence is not supported by this store",
265        ))
266    }
267    async fn list_oauth_resources(&self) -> AuthResult<Vec<OAuthResource>> {
268        Err(AuthError::config(
269            "OAuth resource persistence is not supported by this store",
270        ))
271    }
272    async fn list_oauth_client_resources(
273        &self,
274        _client_id: &str,
275    ) -> AuthResult<Vec<OAuthClientResource>> {
276        Err(AuthError::config(
277            "OAuth resource linkage is not supported by this store",
278        ))
279    }
280    async fn create_oauth_resource(&self, _data: CreateOAuthResource) -> AuthResult<OAuthResource> {
281        Err(AuthError::config(
282            "OAuth resource persistence is not supported by this store",
283        ))
284    }
285    async fn get_oauth_resource(&self, _identifier: &str) -> AuthResult<Option<OAuthResource>> {
286        Err(AuthError::config(
287            "OAuth resource persistence is not supported by this store",
288        ))
289    }
290    async fn delete_oauth_consent(&self, _id: &str) -> AuthResult<()> {
291        Err(AuthError::config(
292            "OAuth consent persistence is not supported by this store",
293        ))
294    }
295    async fn get_oauth_consent(&self, _id: &str) -> AuthResult<Option<OAuthConsent>> {
296        Err(AuthError::config(
297            "OAuth consent persistence is not supported by this store",
298        ))
299    }
300    async fn create_oauth_consent(&self, _data: CreateOAuthConsent) -> AuthResult<OAuthConsent> {
301        Err(AuthError::config(
302            "OAuth consent persistence is not supported by this store",
303        ))
304    }
305    async fn list_oauth_consents_by_user(&self, _user_id: &str) -> AuthResult<Vec<OAuthConsent>> {
306        Err(AuthError::config(
307            "OAuth consent persistence is not supported by this store",
308        ))
309    }
310    async fn update_oauth_consent_metadata(
311        &self,
312        _id: &str,
313        _metadata: Value,
314        _updated_at: DateTime<Utc>,
315    ) -> AuthResult<Option<OAuthConsent>> {
316        Err(AuthError::config(
317            "OAuth consent persistence is not supported by this store",
318        ))
319    }
320    async fn create_oauth_client(&self, _data: CreateOAuthClient) -> AuthResult<OAuthClient> {
321        Err(AuthError::config(
322            "OAuth client persistence is not supported by this store",
323        ))
324    }
325    async fn get_oauth_client(&self, _client_id: &str) -> AuthResult<Option<OAuthClient>> {
326        Err(AuthError::config(
327            "OAuth client persistence is not supported by this store",
328        ))
329    }
330    async fn list_oauth_clients_by_user(&self, _user_id: &str) -> AuthResult<Vec<OAuthClient>> {
331        Err(AuthError::config(
332            "OAuth client persistence is not supported by this store",
333        ))
334    }
335    async fn delete_oauth_client(&self, _client_id: &str) -> AuthResult<()> {
336        Err(AuthError::config(
337            "OAuth client persistence is not supported by this store",
338        ))
339    }
340    async fn update_oauth_client_secret(
341        &self,
342        _client_id: &str,
343        _secret: String,
344        _updated_at: DateTime<Utc>,
345    ) -> AuthResult<Option<OAuthClient>> {
346        Err(AuthError::config(
347            "OAuth client persistence is not supported by this store",
348        ))
349    }
350    async fn update_oauth_client_metadata(
351        &self,
352        _client_id: &str,
353        _metadata: Value,
354        _updated_at: DateTime<Utc>,
355    ) -> AuthResult<Option<OAuthClient>> {
356        Err(AuthError::config(
357            "OAuth client persistence is not supported by this store",
358        ))
359    }
360}
361
362/// RFC client metadata names mapped to the TypeScript persistence schema.
363const CLIENT_FIELDS: &[(&str, &str)] = &[
364    ("client_discovery_id", "clientDiscoveryId"),
365    ("disabled", "disabled"),
366    ("skip_consent", "skipConsent"),
367    ("enable_end_session", "enableEndSession"),
368    ("subject_type", "subjectType"),
369    ("client_credentials_scopes", "clientCredentialsScopes"),
370    ("contacts", "contacts"),
371    ("software_id", "softwareId"),
372    ("software_version", "softwareVersion"),
373    ("software_statement", "softwareStatement"),
374    ("redirect_uris", "redirectUris"),
375    ("post_logout_redirect_uris", "postLogoutRedirectUris"),
376    ("backchannel_logout_uri", "backchannelLogoutUri"),
377    (
378        "backchannel_logout_session_required",
379        "backchannelLogoutSessionRequired",
380    ),
381    ("token_endpoint_auth_method", "tokenEndpointAuthMethod"),
382    ("application_type", "applicationType"),
383    ("jwks", "jwks"),
384    ("jwks_uri", "jwksUri"),
385    ("grant_types", "grantTypes"),
386    ("response_types", "responseTypes"),
387    ("require_pkce", "requirePKCE"),
388    ("dpop_bound_access_tokens", "dpopBoundAccessTokens"),
389    ("reference_id", "referenceId"),
390    ("client_name", "name"),
391    ("client_uri", "uri"),
392    ("logo_uri", "icon"),
393    ("tos_uri", "tos"),
394    ("policy_uri", "policy"),
395    ("scope", "scopes"),
396];
397
398/// Separate registration fields from application metadata for physical persistence.
399/// # Errors
400/// Rejects metadata that is not an object.
401pub fn metadata_to_columns(metadata: Value) -> AuthResult<Value> {
402    let Value::Object(mut metadata) = metadata else {
403        return Err(AuthError::bad_request(
404            "OAuth client metadata must be an object",
405        ));
406    };
407    let mut fields = serde_json::Map::new();
408    // A protocol metadata envelope stays opaque even when a safe extension name
409    // coincides with a native projection key (for example client_discovery_id).
410    let extensions = metadata.remove("metadata");
411    for (rfc, column) in CLIENT_FIELDS {
412        if let Some(mut value) = metadata.remove(*rfc) {
413            if *rfc == "scope"
414                && let Some(scope) = value.as_str()
415            {
416                value = Value::Array(
417                    // oauthToSchema uses JavaScript split(" "), including the empty scope sentinel.
418                    scope
419                        .split(' ')
420                        .map(|scope| Value::String(scope.into()))
421                        .collect(),
422                );
423            }
424            if *rfc == "jwks" && value.is_object() {
425                value = Value::String(serde_json::to_string(&value)?);
426            }
427            _ = fields.insert((*column).into(), value);
428        }
429    }
430    if !fields.contains_key("redirectUris") {
431        _ = fields.insert("redirectUris".into(), serde_json::json!([]));
432    }
433    if let Some(Value::Object(extensions)) = extensions {
434        metadata.extend(extensions);
435    }
436    _ = fields.insert(
437        "metadata".into(),
438        if metadata.is_empty() {
439            Value::Null
440        } else {
441            Value::Object(metadata)
442        },
443    );
444    Ok(Value::Object(fields))
445}
446
447/// Rebuild the registration projection from the TypeScript client columns.
448/// # Errors
449/// Returns an error when a stored client is malformed.
450pub fn metadata_from_columns(fields: &Value) -> AuthResult<Value> {
451    let fields = fields
452        .as_object()
453        .ok_or_else(|| AuthError::internal("Stored OAuth client must be an object"))?;
454    let mut metadata = fields
455        .get("metadata")
456        .and_then(Value::as_object)
457        .cloned()
458        .unwrap_or_default();
459    // Keep opaque aliases out of the native field namespace even when the
460    // authoritative column is null. The envelope also makes a later write or
461    // callback projection lossless without changing standard-client accessors.
462    let mut extensions = serde_json::Map::new();
463    for (rfc, column) in CLIENT_FIELDS {
464        if let Some(value) = metadata.remove(*rfc) {
465            _ = extensions.insert((*rfc).into(), value);
466        }
467        if let Some(mut value) = fields
468            .get(*column)
469            .filter(|value| !value.is_null())
470            .cloned()
471        {
472            if *rfc == "scope"
473                && let Some(scopes) = value.as_array()
474            {
475                value = Value::String(
476                    scopes
477                        .iter()
478                        .filter_map(Value::as_str)
479                        .collect::<Vec<_>>()
480                        .join(" "),
481                );
482            }
483            if *rfc == "jwks"
484                && let Some(jwks) = value.as_str()
485            {
486                value = serde_json::from_str(jwks)?;
487            }
488            _ = metadata.insert((*rfc).into(), value);
489        }
490    }
491    if !extensions.is_empty() {
492        _ = metadata.insert("metadata".into(), Value::Object(extensions));
493    }
494    Ok(Value::Object(metadata))
495}
496
497/// Durable consent attached to the client and consenting user.
498#[derive(Clone, Debug)]
499pub struct OAuthConsent {
500    pub id: String,
501    pub client_id: String,
502    pub user_id: Option<String>,
503    pub metadata: Value,
504    pub created_at: DateTime<Utc>,
505    pub updated_at: DateTime<Utc>,
506}
507#[derive(Clone, Debug)]
508pub struct CreateOAuthConsent {
509    pub client_id: String,
510    pub user_id: Option<String>,
511    pub metadata: Value,
512    pub created_at: DateTime<Utc>,
513}
514
515#[derive(Clone, Debug)]
516pub struct OAuthResource {
517    pub id: String,
518    pub identifier: String,
519    pub metadata: Value,
520    pub created_at: Option<DateTime<Utc>>,
521    pub updated_at: Option<DateTime<Utc>>,
522}
523#[derive(Clone, Debug)]
524pub struct CreateOAuthResource {
525    pub identifier: String,
526    pub metadata: Value,
527    pub created_at: DateTime<Utc>,
528}
529
530#[derive(Clone, Debug)]
531pub struct OAuthClientResource {
532    pub id: String,
533    pub client_id: String,
534    pub resource_id: String,
535    pub metadata: Value,
536    pub created_at: Option<DateTime<Utc>>,
537}
538
539#[derive(Clone, Debug)]
540pub struct CreateOAuthClientResource {
541    pub client_id: String,
542    pub resource_id: String,
543    pub created_at: DateTime<Utc>,
544}