Skip to main content

reifydb_auth/service/
mod.rs

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