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/// Configuration for the authentication service.
60#[derive(Debug, Clone)]
61pub struct AuthServiceConfig {
62	/// Default session token TTL. `None` means tokens don't expire.
63	pub session_ttl: Option<Duration>,
64	/// TTL for pending challenges (default: 60 seconds).
65	pub challenge_ttl: Duration,
66}
67
68impl Default for AuthServiceConfig {
69	fn default() -> Self {
70		Self {
71			session_ttl: Some(Duration::from_secs(24 * 60 * 60)), // 24 hours
72			challenge_ttl: Duration::from_secs(60),
73		}
74	}
75}
76
77impl AuthServiceConfig {
78	pub fn session_ttl(mut self, ttl: Duration) -> Self {
79		self.session_ttl = Some(ttl);
80		self
81	}
82
83	pub fn no_session_ttl(mut self) -> Self {
84		self.session_ttl = None;
85		self
86	}
87
88	pub fn challenge_ttl(mut self, ttl: Duration) -> Self {
89		self.challenge_ttl = ttl;
90		self
91	}
92}
93
94pub struct Inner {
95	pub(crate) engine: Arc<dyn AuthEngine>,
96	pub(crate) auth_registry: Arc<AuthenticationRegistry>,
97	pub(crate) challenges: ChallengeStore,
98	pub(crate) rng: SystemRng,
99	pub(crate) clock: Clock,
100	pub(crate) session_ttl: Option<Duration>,
101}
102
103/// Shared authentication service.
104///
105/// Coordinates between the identity catalog, authentication providers, and
106/// token/challenge stores. All transports and embedded mode call through
107/// this single service.
108///
109/// Cheap to clone — uses `Arc` internally.
110#[derive(Clone)]
111pub struct AuthService(Arc<Inner>);
112
113impl Deref for AuthService {
114	type Target = Inner;
115	fn deref(&self) -> &Inner {
116		&self.0
117	}
118}
119
120impl AuthService {
121	pub fn new(
122		engine: Arc<dyn AuthEngine>,
123		auth_registry: Arc<AuthenticationRegistry>,
124		rng: SystemRng,
125		clock: Clock,
126		config: AuthServiceConfig,
127	) -> Self {
128		Self(Arc::new(Inner {
129			engine,
130			auth_registry,
131			challenges: ChallengeStore::new(config.challenge_ttl),
132			rng,
133			clock,
134			session_ttl: config.session_ttl,
135		}))
136	}
137
138	/// Get the current time as a DateTime.
139	pub(super) fn now(&self) -> Result<DateTime, Error> {
140		Ok(DateTime::from_timestamp_nanos(self.clock.now_nanos())?)
141	}
142
143	/// Compute the expiration DateTime from the configured session TTL.
144	pub(super) fn expires_at(&self) -> Result<Option<DateTime>, Error> {
145		match self.session_ttl {
146			Some(ttl) => {
147				let nanos = self.clock.now_nanos() + ttl.as_nanos();
148				Ok(Some(DateTime::from_timestamp_nanos(nanos)?))
149			}
150			None => Ok(None),
151		}
152	}
153
154	/// Persist a token to the database.
155	pub(super) fn persist_token(&self, token: &str, identity: IdentityId) -> Result<Token, Error> {
156		let mut admin = self.engine.begin_admin()?;
157
158		let def = create_token(&mut admin, token, identity, self.expires_at()?, self.now()?)?;
159
160		admin.commit()?;
161		Ok(def)
162	}
163}
164
165/// Generate a session token (64 hex characters) using the provided RNG.
166pub(super) fn generate_session_token(rng: &SystemRng) -> String {
167	let bytes = rng.bytes_32();
168	bytes.iter().map(|b| format!("{:02x}", b)).collect()
169}