Skip to main content

reifydb_auth/service/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4mod authenticate;
5mod solana;
6mod token;
7
8use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};
9
10use reifydb_catalog::{catalog::Catalog, create_token};
11use reifydb_core::interface::catalog::token::Token;
12use reifydb_runtime::context::{clock::Clock, rng::Rng as SystemRng};
13use reifydb_transaction::transaction::{admin::AdminTransaction, query::QueryTransaction};
14use reifydb_type::{
15	error::Error,
16	value::{datetime::DateTime, identity::IdentityId},
17};
18
19use crate::{challenge::ChallengeStore, registry::AuthenticationRegistry};
20
21/// Trait abstracting the engine operations needed by the authentication service.
22///
23/// This allows the auth crate to remain independent of the engine crate while
24/// still being able to create transactions and access the catalog.
25///
26/// All transactions are created with system identity - authentication operates
27/// at a privileged level.
28pub trait AuthEngine: Send + Sync {
29	fn begin_admin(&self) -> Result<AdminTransaction, Error>;
30	fn begin_query(&self) -> Result<QueryTransaction, Error>;
31	fn catalog(&self) -> Catalog;
32}
33
34/// Response from an authentication attempt.
35#[derive(Debug, Clone)]
36pub enum AuthResponse {
37	/// Authentication succeeded. Contains the session token and identity.
38	Authenticated {
39		identity: IdentityId,
40		token: String,
41	},
42	/// The provider requires a challenge-response round-trip.
43	Challenge {
44		challenge_id: String,
45		payload: HashMap<String, String>,
46	},
47	/// Authentication failed (wrong credentials, unknown identity, etc.).
48	Failed {
49		reason: String,
50	},
51}
52
53/// Configurator for the authentication service.
54pub struct AuthConfigurator {
55	session_ttl: Option<Duration>,
56	challenge_ttl: Duration,
57}
58
59impl Default for AuthConfigurator {
60	fn default() -> Self {
61		Self::new()
62	}
63}
64
65impl AuthConfigurator {
66	pub fn new() -> Self {
67		Self {
68			session_ttl: Some(Duration::from_secs(24 * 60 * 60)), // 24 hours
69			challenge_ttl: Duration::from_secs(60),
70		}
71	}
72
73	pub fn session_ttl(mut self, ttl: Duration) -> Self {
74		self.session_ttl = Some(ttl);
75		self
76	}
77
78	pub fn no_session_ttl(mut self) -> Self {
79		self.session_ttl = None;
80		self
81	}
82
83	pub fn challenge_ttl(mut self, ttl: Duration) -> Self {
84		self.challenge_ttl = ttl;
85		self
86	}
87
88	pub fn configure(self) -> AuthServiceConfig {
89		AuthServiceConfig {
90			session_ttl: self.session_ttl,
91			challenge_ttl: self.challenge_ttl,
92		}
93	}
94}
95
96/// Immutable configuration for the authentication service.
97#[derive(Debug, Clone)]
98pub struct AuthServiceConfig {
99	/// Default session token TTL. `None` means tokens don't expire.
100	pub session_ttl: Option<Duration>,
101	/// TTL for pending challenges (default: 60 seconds).
102	pub challenge_ttl: Duration,
103}
104
105impl Default for AuthServiceConfig {
106	fn default() -> Self {
107		AuthConfigurator::new().configure()
108	}
109}
110
111pub struct Inner {
112	pub(crate) engine: Arc<dyn AuthEngine>,
113	pub(crate) auth_registry: Arc<AuthenticationRegistry>,
114	pub(crate) challenges: ChallengeStore,
115	pub(crate) rng: SystemRng,
116	pub(crate) clock: Clock,
117	pub(crate) session_ttl: Option<Duration>,
118}
119
120/// Shared authentication service.
121///
122/// Coordinates between the identity catalog, authentication providers, and
123/// token/challenge stores. All transports and embedded mode call through
124/// this single service.
125///
126/// Cheap to clone - uses `Arc` internally.
127#[derive(Clone)]
128pub struct AuthService(Arc<Inner>);
129
130impl Deref for AuthService {
131	type Target = Inner;
132	fn deref(&self) -> &Inner {
133		&self.0
134	}
135}
136
137impl AuthService {
138	pub fn new(
139		engine: Arc<dyn AuthEngine>,
140		auth_registry: Arc<AuthenticationRegistry>,
141		rng: SystemRng,
142		clock: Clock,
143		config: AuthServiceConfig,
144	) -> Self {
145		Self(Arc::new(Inner {
146			engine,
147			auth_registry,
148			challenges: ChallengeStore::new(config.challenge_ttl),
149			rng,
150			clock,
151			session_ttl: config.session_ttl,
152		}))
153	}
154
155	/// Get the current time as a DateTime.
156	pub(super) fn now(&self) -> Result<DateTime, Error> {
157		Ok(DateTime::from_nanos(self.clock.now_nanos()))
158	}
159
160	/// Compute the expiration DateTime from the configured session TTL.
161	pub(super) fn expires_at(&self) -> Result<Option<DateTime>, Error> {
162		match self.session_ttl {
163			Some(ttl) => {
164				let ttl_nanos = ttl.as_nanos() as u64;
165				let nanos = self.clock.now_nanos().saturating_add(ttl_nanos);
166				Ok(Some(DateTime::from_nanos(nanos)))
167			}
168			None => Ok(None),
169		}
170	}
171
172	/// Persist a session token to the database using the configured session TTL.
173	pub(super) fn persist_token(&self, token: &str, identity: IdentityId) -> Result<Token, Error> {
174		let mut admin = self.engine.begin_admin()?;
175
176		let def = create_token(&mut admin, token, identity, self.expires_at()?, self.now()?)?;
177
178		admin.commit()?;
179		Ok(def)
180	}
181
182	/// Create a token for an identity with an explicit expiration.
183	///
184	/// Unlike `persist_token` (which uses the configured session TTL), this
185	/// accepts an explicit `expires_at` - pass `None` for non-expiring tokens.
186	///
187	/// Used by applications to issue API tokens.
188	pub fn create_token(
189		&self,
190		token: &str,
191		identity: IdentityId,
192		expires_at: Option<DateTime>,
193	) -> Result<Token, Error> {
194		let mut admin = self.engine.begin_admin()?;
195		let def = create_token(&mut admin, token, identity, expires_at, self.now()?)?;
196		admin.commit()?;
197		Ok(def)
198	}
199}
200
201/// Generate a session token (64 hex characters) using the infrastructure RNG stream.
202///
203/// Uses `infra_bytes_32` so that session token generation does not consume
204/// from the primary RNG, ensuring deterministic test output across runners.
205pub(super) fn generate_session_token(rng: &SystemRng) -> String {
206	let bytes = rng.infra_bytes_32();
207	bytes.iter().map(|b| format!("{:02x}", b)).collect()
208}