oauth2_broker/
store.rs

1//! Storage contracts and built-in store implementations for broker token records.
2
3pub mod file;
4pub mod memory;
5
6pub use file::FileStore;
7pub use memory::MemoryStore;
8
9// self
10use crate::{
11	_prelude::*,
12	auth::{ScopeSet, TokenFamily, TokenRecord},
13};
14
15/// Persistence contract for broker-issued tokens.
16pub type StoreFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, StoreError>> + 'a + Send>>;
17
18/// Storage backend contract implemented by broker token stores.
19pub trait BrokerStore
20where
21	Self: Send + Sync,
22{
23	/// Persists or replaces a token record for the provided family + scope.
24	fn save(&self, record: TokenRecord) -> StoreFuture<'_, ()>;
25
26	/// Fetches the record associated with the family + scope, if present.
27	fn fetch<'a>(
28		&'a self,
29		family: &'a TokenFamily,
30		scope: &'a ScopeSet,
31	) -> StoreFuture<'a, Option<TokenRecord>>;
32
33	/// Atomically rotates a refresh token if the expected secret matches.
34	fn compare_and_swap_refresh<'a>(
35		&'a self,
36		family: &'a TokenFamily,
37		scope: &'a ScopeSet,
38		expected_refresh: Option<&'a str>,
39		replacement: TokenRecord,
40	) -> StoreFuture<'a, CompareAndSwapOutcome>;
41
42	/// Marks a record as revoked at the provided instant.
43	fn revoke<'a>(
44		&'a self,
45		family: &'a TokenFamily,
46		scope: &'a ScopeSet,
47		instant: OffsetDateTime,
48	) -> StoreFuture<'a, Option<TokenRecord>>;
49}
50
51/// Result of a refresh-token compare-and-swap attempt.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub enum CompareAndSwapOutcome {
54	/// The refresh secret matched the expected value and the record was updated.
55	Updated,
56	/// The record exists but the expected refresh secret did not match.
57	RefreshMismatch,
58	/// No record matched the provided family + scope.
59	Missing,
60}
61
62/// Error type produced by [`BrokerStore`] implementations.
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ThisError)]
64pub enum StoreError {
65	/// Serialization failures (e.g., serde/bincode) surfaced by the backend.
66	#[error("Serialization error: {message}.")]
67	Serialization {
68		/// Human-readable error payload.
69		message: String,
70	},
71	/// Backend-level failure for the storage engine.
72	#[error("Backend failure: {message}.")]
73	Backend {
74		/// Human-readable error payload.
75		message: String,
76	},
77}
78
79/// Unique key identifying a stored token record.
80#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub struct StoreKey {
82	/// Token family component.
83	pub family: TokenFamily,
84	/// Scope fingerprint used for partitioning.
85	pub scope_fingerprint: String,
86}
87impl StoreKey {
88	/// Builds a key using the provided family and scope fingerprint.
89	pub fn new(family: &TokenFamily, scope: &ScopeSet) -> Self {
90		Self { family: family.clone(), scope_fingerprint: scope.fingerprint() }
91	}
92}
93
94#[cfg(test)]
95mod tests {
96	// self
97	use super::*;
98	use crate::{
99		auth::{PrincipalId, ScopeSet, TenantId},
100		error::Error,
101	};
102	use std::error::Error as StdError;
103
104	#[test]
105	fn store_error_converts_into_broker_error_with_source() {
106		let store_error = StoreError::Backend { message: "database unreachable".into() };
107		let broker_error: Error = store_error.clone().into();
108
109		assert!(matches!(broker_error, Error::Storage(_)));
110		assert!(broker_error.to_string().contains("database unreachable"));
111
112		let source = StdError::source(&broker_error)
113			.expect("Broker error should expose the original store error as its source.");
114
115		assert_eq!(source.to_string(), store_error.to_string());
116	}
117
118	#[test]
119	fn store_key_uses_scope_fingerprint() {
120		let tenant = TenantId::new("tenant-1").expect("Tenant fixture should be valid.");
121		let principal =
122			PrincipalId::new("principal-1").expect("Principal fixture should be valid.");
123		let family = TokenFamily::new(tenant, principal);
124		let scope_a =
125			ScopeSet::new(["profile", "email"]).expect("First scope fixture should be valid.");
126		let scope_b =
127			ScopeSet::new(["email", "profile"]).expect("Second scope fixture should be valid.");
128		let key_a = StoreKey::new(&family, &scope_a);
129		let key_b = StoreKey::new(&family, &scope_b);
130
131		assert_eq!(key_a.scope_fingerprint, key_b.scope_fingerprint);
132		assert_eq!(key_a.family, key_b.family);
133		assert_eq!(key_a, key_b);
134	}
135
136	#[test]
137	fn compare_and_swap_outcome_can_be_serialized() {
138		let payload = serde_json::to_string(&CompareAndSwapOutcome::Updated)
139			.expect("CompareAndSwapOutcome should serialize to JSON.");
140
141		assert_eq!(payload, "\"Updated\"");
142
143		let round_trip: CompareAndSwapOutcome = serde_json::from_str(&payload)
144			.expect("Serialized outcome should deserialize from JSON.");
145
146		assert_eq!(round_trip, CompareAndSwapOutcome::Updated);
147	}
148}