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    /// Replace a client's serialized inline JWK Set only while it still holds
361    /// `expected_jwks`. `None` means the client or its key set changed.
362    async fn replace_oauth_client_jwks(
363        &self,
364        _client_id: &str,
365        _expected_jwks: &str,
366        _jwks: String,
367        _updated_at: DateTime<Utc>,
368    ) -> AuthResult<Option<OAuthClient>> {
369        Err(AuthError::config(
370            "OAuth client persistence is not supported by this store",
371        ))
372    }
373}
374
375/// RFC client metadata names mapped to the TypeScript persistence schema.
376const CLIENT_FIELDS: &[(&str, &str)] = &[
377    ("client_discovery_id", "clientDiscoveryId"),
378    ("disabled", "disabled"),
379    ("skip_consent", "skipConsent"),
380    ("enable_end_session", "enableEndSession"),
381    ("subject_type", "subjectType"),
382    ("client_credentials_scopes", "clientCredentialsScopes"),
383    ("contacts", "contacts"),
384    ("software_id", "softwareId"),
385    ("software_version", "softwareVersion"),
386    ("software_statement", "softwareStatement"),
387    ("redirect_uris", "redirectUris"),
388    ("post_logout_redirect_uris", "postLogoutRedirectUris"),
389    ("backchannel_logout_uri", "backchannelLogoutUri"),
390    (
391        "backchannel_logout_session_required",
392        "backchannelLogoutSessionRequired",
393    ),
394    ("token_endpoint_auth_method", "tokenEndpointAuthMethod"),
395    ("application_type", "applicationType"),
396    ("jwks", "jwks"),
397    ("jwks_uri", "jwksUri"),
398    ("grant_types", "grantTypes"),
399    ("response_types", "responseTypes"),
400    ("require_pkce", "requirePKCE"),
401    ("dpop_bound_access_tokens", "dpopBoundAccessTokens"),
402    ("reference_id", "referenceId"),
403    ("client_name", "name"),
404    ("client_uri", "uri"),
405    ("logo_uri", "icon"),
406    ("tos_uri", "tos"),
407    ("policy_uri", "policy"),
408    ("scope", "scopes"),
409];
410
411/// Separate registration fields from application metadata for physical persistence.
412/// # Errors
413/// Rejects metadata that is not an object.
414pub fn metadata_to_columns(metadata: Value) -> AuthResult<Value> {
415    let Value::Object(mut metadata) = metadata else {
416        return Err(AuthError::bad_request(
417            "OAuth client metadata must be an object",
418        ));
419    };
420    let mut fields = serde_json::Map::new();
421    // A protocol metadata envelope stays opaque even when a safe extension name
422    // coincides with a native projection key (for example client_discovery_id).
423    let extensions = metadata.remove("metadata");
424    for (rfc, column) in CLIENT_FIELDS {
425        if let Some(mut value) = metadata.remove(*rfc) {
426            if *rfc == "scope"
427                && let Some(scope) = value.as_str()
428            {
429                value = Value::Array(
430                    // oauthToSchema uses JavaScript split(" "), including the empty scope sentinel.
431                    scope
432                        .split(' ')
433                        .map(|scope| Value::String(scope.into()))
434                        .collect(),
435                );
436            }
437            if *rfc == "jwks" && value.is_object() {
438                value = Value::String(serde_json::to_string(&value)?);
439            }
440            _ = fields.insert((*column).into(), value);
441        }
442    }
443    if !fields.contains_key("redirectUris") {
444        _ = fields.insert("redirectUris".into(), serde_json::json!([]));
445    }
446    if let Some(Value::Object(extensions)) = extensions {
447        metadata.extend(extensions);
448    }
449    _ = fields.insert(
450        "metadata".into(),
451        if metadata.is_empty() {
452            Value::Null
453        } else {
454            Value::Object(metadata)
455        },
456    );
457    Ok(Value::Object(fields))
458}
459
460/// Rebuild the registration projection from the TypeScript client columns.
461/// # Errors
462/// Returns an error when a stored client is malformed.
463pub fn metadata_from_columns(fields: &Value) -> AuthResult<Value> {
464    let fields = fields
465        .as_object()
466        .ok_or_else(|| AuthError::internal("Stored OAuth client must be an object"))?;
467    let mut metadata = fields
468        .get("metadata")
469        .and_then(Value::as_object)
470        .cloned()
471        .unwrap_or_default();
472    // Keep opaque aliases out of the native field namespace even when the
473    // authoritative column is null. The envelope also makes a later write or
474    // callback projection lossless without changing standard-client accessors.
475    let mut extensions = serde_json::Map::new();
476    for (rfc, column) in CLIENT_FIELDS {
477        if let Some(value) = metadata.remove(*rfc) {
478            _ = extensions.insert((*rfc).into(), value);
479        }
480        if let Some(mut value) = fields
481            .get(*column)
482            .filter(|value| !value.is_null())
483            .cloned()
484        {
485            if *rfc == "scope"
486                && let Some(scopes) = value.as_array()
487            {
488                value = Value::String(
489                    scopes
490                        .iter()
491                        .filter_map(Value::as_str)
492                        .collect::<Vec<_>>()
493                        .join(" "),
494                );
495            }
496            if *rfc == "jwks"
497                && let Some(jwks) = value.as_str()
498            {
499                value = serde_json::from_str(jwks)?;
500            }
501            _ = metadata.insert((*rfc).into(), value);
502        }
503    }
504    if !extensions.is_empty() {
505        _ = metadata.insert("metadata".into(), Value::Object(extensions));
506    }
507    Ok(Value::Object(metadata))
508}
509
510/// Durable consent attached to the client and consenting user.
511#[derive(Clone, Debug)]
512pub struct OAuthConsent {
513    pub id: String,
514    pub client_id: String,
515    pub user_id: Option<String>,
516    pub metadata: Value,
517    pub created_at: DateTime<Utc>,
518    pub updated_at: DateTime<Utc>,
519}
520#[derive(Clone, Debug)]
521pub struct CreateOAuthConsent {
522    pub client_id: String,
523    pub user_id: Option<String>,
524    pub metadata: Value,
525    pub created_at: DateTime<Utc>,
526}
527
528#[derive(Clone, Debug)]
529pub struct OAuthResource {
530    pub id: String,
531    pub identifier: String,
532    pub metadata: Value,
533    pub created_at: Option<DateTime<Utc>>,
534    pub updated_at: Option<DateTime<Utc>>,
535}
536#[derive(Clone, Debug)]
537pub struct CreateOAuthResource {
538    pub identifier: String,
539    pub metadata: Value,
540    pub created_at: DateTime<Utc>,
541}
542
543#[derive(Clone, Debug)]
544pub struct OAuthClientResource {
545    pub id: String,
546    pub client_id: String,
547    pub resource_id: String,
548    pub metadata: Value,
549    pub created_at: Option<DateTime<Utc>>,
550}
551
552#[derive(Clone, Debug)]
553pub struct CreateOAuthClientResource {
554    pub client_id: String,
555    pub resource_id: String,
556    pub created_at: DateTime<Utc>,
557}