Skip to main content

mcp_oauth/store/
mod.rs

1//! Pluggable storage traits for the OAuth layer.
2//!
3//! The library ships with JSON-file-backed implementations
4//! ([`json_file`] module) that replicate the original in-memory +
5//! file-persistence behaviour.  Consumers can implement these traits
6//! to back the OAuth layer with `SQLite`, an encrypted store, or any
7//! other backend.
8
9pub mod json_file;
10
11use std::fmt;
12use std::future::Future;
13
14use serde::{Deserialize, Serialize};
15use webauthn_rs::prelude::{AuthenticationResult, Passkey};
16
17// ---------------------------------------------------------------------------
18// Public data types (used in trait signatures)
19// ---------------------------------------------------------------------------
20
21/// An authorization code awaiting exchange for tokens.
22#[derive(Debug, Serialize, Deserialize, Clone)]
23#[non_exhaustive]
24pub struct AuthCode {
25    pub client_id: String,
26    pub redirect_uri: String,
27    pub code_challenge: String,
28    pub created_at: u64,
29}
30
31impl AuthCode {
32    #[must_use]
33    pub const fn new(
34        client_id: String,
35        redirect_uri: String,
36        code_challenge: String,
37        created_at: u64,
38    ) -> Self {
39        Self {
40            client_id,
41            redirect_uri,
42            code_challenge,
43            created_at,
44        }
45    }
46}
47
48/// A stored access token.
49#[derive(Debug, Serialize, Deserialize, Clone)]
50#[non_exhaustive]
51pub struct AccessTokenEntry {
52    pub client_id: String,
53    pub created_at: u64,
54    pub expires_in_secs: u64,
55    pub refresh_token: String,
56}
57
58impl AccessTokenEntry {
59    #[must_use]
60    pub const fn new(
61        client_id: String,
62        created_at: u64,
63        expires_in_secs: u64,
64        refresh_token: String,
65    ) -> Self {
66        Self {
67            client_id,
68            created_at,
69            expires_in_secs,
70            refresh_token,
71        }
72    }
73}
74
75/// A stored refresh token.
76#[derive(Debug, Serialize, Deserialize, Clone)]
77#[non_exhaustive]
78pub struct RefreshTokenEntry {
79    pub client_id: String,
80}
81
82impl RefreshTokenEntry {
83    #[must_use]
84    pub const fn new(client_id: String) -> Self {
85        Self { client_id }
86    }
87}
88
89/// A dynamically registered OAuth client.
90#[derive(Serialize, Deserialize, Clone)]
91#[non_exhaustive]
92pub struct RegisteredClient {
93    pub client_secret: String,
94    pub redirect_uris: Vec<String>,
95}
96
97impl RegisteredClient {
98    #[must_use]
99    pub const fn new(client_secret: String, redirect_uris: Vec<String>) -> Self {
100        Self {
101            client_secret,
102            redirect_uris,
103        }
104    }
105}
106
107// Manual Debug impl to redact client_secret
108impl fmt::Debug for RegisteredClient {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("RegisteredClient")
111            .field("client_secret", &"[REDACTED]")
112            .field("redirect_uris", &self.redirect_uris)
113            .finish()
114    }
115}
116
117// ---------------------------------------------------------------------------
118// Shared constants
119// ---------------------------------------------------------------------------
120
121/// TTL for transient state entries (auth codes, registration/authentication sessions).
122pub const TRANSIENT_STATE_TTL_SECS: u64 = 300;
123
124// ---------------------------------------------------------------------------
125// Error type
126// ---------------------------------------------------------------------------
127
128/// Errors returned by store operations.
129#[derive(Debug)]
130pub enum StoreError {
131    /// The store has reached its capacity limit.
132    CapacityExceeded,
133    /// A backend-specific error (I/O, serialization, …).
134    Backend(Box<dyn std::error::Error + Send + Sync>),
135}
136
137impl fmt::Display for StoreError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::CapacityExceeded => write!(f, "store capacity exceeded"),
141            Self::Backend(e) => write!(f, "store backend error: {e}"),
142        }
143    }
144}
145
146impl std::error::Error for StoreError {
147    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
148        match self {
149            Self::Backend(e) => Some(&**e),
150            Self::CapacityExceeded => None,
151        }
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Storage traits
157// ---------------------------------------------------------------------------
158
159/// Token storage: authorization codes, access tokens, refresh tokens.
160pub trait TokenStore: Send + Sync + 'static {
161    /// Store an authorization code.
162    fn store_auth_code(
163        &self,
164        code: String,
165        entry: AuthCode,
166    ) -> impl Future<Output = Result<(), StoreError>> + Send;
167
168    /// Remove and return an authorization code (single-use).
169    fn consume_auth_code(
170        &self,
171        code: &str,
172    ) -> impl Future<Output = Result<Option<AuthCode>, StoreError>> + Send;
173
174    /// Store an access token.
175    fn store_access_token(
176        &self,
177        token: String,
178        entry: AccessTokenEntry,
179    ) -> impl Future<Output = Result<(), StoreError>> + Send;
180
181    /// Retrieve an access token without removing it.
182    fn get_access_token(
183        &self,
184        token: &str,
185    ) -> impl Future<Output = Result<Option<AccessTokenEntry>, StoreError>> + Send;
186
187    /// Revoke all access tokens associated with the given refresh token.
188    fn revoke_access_tokens_by_refresh(
189        &self,
190        refresh_token: &str,
191    ) -> impl Future<Output = Result<(), StoreError>> + Send;
192
193    /// Store a refresh token.
194    fn store_refresh_token(
195        &self,
196        token: String,
197        entry: RefreshTokenEntry,
198    ) -> impl Future<Output = Result<(), StoreError>> + Send;
199
200    /// Look up a refresh token without removing it.
201    fn get_refresh_token(
202        &self,
203        token: &str,
204    ) -> impl Future<Output = Result<Option<RefreshTokenEntry>, StoreError>> + Send;
205
206    /// Consume (remove and return) a refresh token.
207    fn consume_refresh_token(
208        &self,
209        token: &str,
210    ) -> impl Future<Output = Result<Option<RefreshTokenEntry>, StoreError>> + Send;
211
212    /// Remove tokens whose `created_at + expires_in_secs < now`.
213    fn cleanup_expired_tokens(
214        &self,
215        now: u64,
216    ) -> impl Future<Output = Result<(), StoreError>> + Send;
217}
218
219/// Client registration storage.
220pub trait ClientStore: Send + Sync + 'static {
221    /// Register a new dynamic client.
222    fn register_client(
223        &self,
224        id: String,
225        client: RegisteredClient,
226    ) -> impl Future<Output = Result<(), StoreError>> + Send;
227
228    /// Atomically register a client if the store is under its configured
229    /// client cap.
230    ///
231    /// Returns `Ok(true)` if the client was registered, `Ok(false)` if the
232    /// cap has been reached (registration locked). Implementations **must**
233    /// check the count and insert under the same lock to prevent TOCTOU
234    /// races.
235    ///
236    /// A cap of `Some(1)` (the default in [`crate::CapacityConfig`]) preserves
237    /// the historical single-client lock. `None` means unlimited dynamic
238    /// client registrations.
239    fn try_register_client(
240        &self,
241        id: String,
242        client: RegisteredClient,
243    ) -> impl Future<Output = Result<bool, StoreError>> + Send;
244
245    /// Look up a registered client by ID.
246    fn get_client(
247        &self,
248        id: &str,
249    ) -> impl Future<Output = Result<Option<RegisteredClient>, StoreError>> + Send;
250
251    /// Return the number of registered clients.
252    fn client_count(&self) -> impl Future<Output = Result<usize, StoreError>> + Send;
253}
254
255/// Passkey (`WebAuthn` credential) storage.
256pub trait PasskeyStore: Send + Sync + 'static {
257    /// Return all registered passkeys.
258    fn list_passkeys(&self) -> impl Future<Output = Result<Vec<Passkey>, StoreError>> + Send;
259
260    /// Atomically add a passkey only if no passkeys exist yet.
261    ///
262    /// Returns `Ok(true)` if the passkey was added, `Ok(false)` if
263    /// passkeys already exist (registration locked).  Implementations
264    /// **must** check emptiness and insert under the same lock.
265    fn add_passkey_if_none(
266        &self,
267        passkey: Passkey,
268    ) -> impl Future<Output = Result<bool, StoreError>> + Send;
269
270    /// Persist a newly registered passkey.
271    fn add_passkey(&self, passkey: Passkey) -> impl Future<Output = Result<(), StoreError>> + Send;
272
273    /// Update credential counters after a successful authentication.
274    fn update_passkey(
275        &self,
276        auth_result: &AuthenticationResult,
277    ) -> impl Future<Output = Result<(), StoreError>> + Send;
278
279    /// Check whether any passkeys are registered.
280    fn has_passkeys(&self) -> impl Future<Output = Result<bool, StoreError>> + Send;
281}