Skip to main content

reifydb_auth/service/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4mod authenticate;
5mod github;
6mod solana;
7mod token;
8
9use std::{collections::HashMap, ops::Deref, sync::Arc};
10
11use reifydb_catalog::{catalog::Catalog, create_token};
12use reifydb_core::interface::catalog::token::Token;
13use reifydb_runtime::context::{clock::Clock, rng::Rng as SystemRng};
14use reifydb_transaction::transaction::{Transaction, admin::AdminTransaction, query::QueryTransaction};
15use reifydb_value::{
16	error::Error,
17	value::{Value, datetime::DateTime, duration::Duration, identity::IdentityId, value_type::ValueType},
18};
19use tracing::instrument;
20
21use crate::{
22	challenge::ChallengeStore,
23	github::{GithubApi, GithubConfig, default_api},
24	registry::AuthenticationRegistry,
25};
26
27pub trait AuthEngine: Send + Sync {
28	fn begin_admin(&self) -> Result<AdminTransaction, Error>;
29	fn begin_query(&self) -> Result<QueryTransaction, Error>;
30	fn catalog(&self) -> Catalog;
31}
32
33#[derive(Debug, Clone)]
34pub enum AuthResponse {
35	Authenticated {
36		identity: IdentityId,
37		token: String,
38	},
39
40	Challenge {
41		challenge_id: String,
42		payload: HashMap<String, String>,
43	},
44
45	Failed {
46		reason: String,
47	},
48}
49
50pub struct AuthConfigurator {
51	session_ttl: Option<Duration>,
52	challenge_ttl: Duration,
53	github: Option<GithubConfig>,
54}
55
56impl Default for AuthConfigurator {
57	fn default() -> Self {
58		Self::new()
59	}
60}
61
62impl AuthConfigurator {
63	pub fn new() -> Self {
64		Self {
65			session_ttl: Some(Duration::from_seconds(24 * 60 * 60).unwrap()),
66			challenge_ttl: Duration::from_seconds(60).unwrap(),
67			github: None,
68		}
69	}
70
71	pub fn session_ttl(mut self, ttl: Duration) -> Self {
72		self.session_ttl = Some(ttl);
73		self
74	}
75
76	pub fn no_session_ttl(mut self) -> Self {
77		self.session_ttl = None;
78		self
79	}
80
81	pub fn challenge_ttl(mut self, ttl: Duration) -> Self {
82		self.challenge_ttl = ttl;
83		self
84	}
85
86	pub fn github(mut self, config: GithubConfig) -> Self {
87		self.github = Some(config);
88		self
89	}
90
91	pub fn configure(self) -> AuthServiceConfig {
92		AuthServiceConfig {
93			session_ttl: self.session_ttl,
94			challenge_ttl: self.challenge_ttl,
95			github: self.github,
96		}
97	}
98}
99
100#[derive(Debug, Clone)]
101pub struct AuthServiceConfig {
102	pub session_ttl: Option<Duration>,
103
104	pub challenge_ttl: Duration,
105
106	pub github: Option<GithubConfig>,
107}
108
109impl Default for AuthServiceConfig {
110	fn default() -> Self {
111		AuthConfigurator::new().configure()
112	}
113}
114
115pub struct Inner {
116	pub(crate) engine: Arc<dyn AuthEngine>,
117	pub(crate) auth_registry: Arc<AuthenticationRegistry>,
118	pub(crate) challenges: ChallengeStore,
119	pub(crate) rng: SystemRng,
120	pub(crate) clock: Clock,
121	pub(crate) session_ttl: Option<Duration>,
122	pub(crate) github: Option<GithubAuth>,
123}
124
125pub(crate) struct GithubAuth {
126	pub(crate) config: GithubConfig,
127	pub(crate) api: Arc<dyn GithubApi>,
128}
129
130#[derive(Clone)]
131pub struct AuthService(Arc<Inner>);
132
133impl Deref for AuthService {
134	type Target = Inner;
135	fn deref(&self) -> &Inner {
136		&self.0
137	}
138}
139
140impl AuthService {
141	pub fn new(
142		engine: Arc<dyn AuthEngine>,
143		auth_registry: Arc<AuthenticationRegistry>,
144		rng: SystemRng,
145		clock: Clock,
146		config: AuthServiceConfig,
147	) -> Self {
148		Self::with_github_api(engine, auth_registry, rng, clock, config, default_api())
149	}
150
151	pub fn with_github_api(
152		engine: Arc<dyn AuthEngine>,
153		auth_registry: Arc<AuthenticationRegistry>,
154		rng: SystemRng,
155		clock: Clock,
156		config: AuthServiceConfig,
157		api: Arc<dyn GithubApi>,
158	) -> Self {
159		Self(Arc::new(Inner {
160			engine,
161			auth_registry,
162			challenges: ChallengeStore::new(config.challenge_ttl),
163			rng,
164			clock,
165			session_ttl: config.session_ttl,
166			github: config.github.map(|config| GithubAuth {
167				config,
168				api,
169			}),
170		}))
171	}
172
173	pub fn auth_registry(&self) -> &Arc<AuthenticationRegistry> {
174		&self.auth_registry
175	}
176
177	pub(super) fn now(&self) -> Result<DateTime, Error> {
178		Ok(self.clock.now())
179	}
180
181	pub(super) fn expires_at(&self) -> Result<Option<DateTime>, Error> {
182		match self.session_ttl {
183			Some(ttl) => {
184				let ttl_nanos = ttl.as_nanos()? as u64;
185				let nanos = self.clock.now().to_nanos().saturating_add(ttl_nanos);
186				Ok(Some(DateTime::from_nanos(nanos)))
187			}
188			None => Ok(None),
189		}
190	}
191
192	pub(super) fn persist_token(&self, token: &str, identity: IdentityId) -> Result<Token, Error> {
193		let mut admin = self.engine.begin_admin()?;
194
195		let def = create_token(&mut admin, token, identity, self.expires_at()?, self.now()?)?;
196
197		admin.commit()?;
198		Ok(def)
199	}
200
201	pub fn create_token(
202		&self,
203		token: &str,
204		identity: IdentityId,
205		expires_at: Option<DateTime>,
206	) -> Result<Token, Error> {
207		let mut admin = self.engine.begin_admin()?;
208		let def = create_token(&mut admin, token, identity, expires_at, self.now()?)?;
209		admin.commit()?;
210		Ok(def)
211	}
212
213	#[instrument(name = "auth::create_session", level = "debug", skip(self))]
214	pub fn create_session(&self, identity: IdentityId, ttl: Option<Duration>) -> Result<Token, Error> {
215		let expires_at = match ttl {
216			Some(ttl) => {
217				let nanos = self.clock.now().to_nanos().saturating_add(ttl.as_nanos()? as u64);
218				Some(DateTime::from_nanos(nanos))
219			}
220			None => self.expires_at()?,
221		};
222		self.create_token(&generate_session_token(&self.rng), identity, expires_at)
223	}
224
225	pub(super) fn set_lookup_attribute(
226		&self,
227		admin: &mut AdminTransaction,
228		identity: IdentityId,
229		name: &str,
230		value: &str,
231	) -> Result<(), Error> {
232		let catalog = self.engine.catalog();
233		let attribute =
234			match catalog.find_identity_attribute_by_name(&mut Transaction::Admin(&mut *admin), name)? {
235				Some(attribute) => attribute,
236				None => catalog.create_identity_attribute(admin, name, ValueType::Utf8)?,
237			};
238		catalog.set_identity_attribute_value(admin, identity, &attribute, Value::Utf8(value.to_string()))?;
239		Ok(())
240	}
241}
242
243pub(super) fn generate_session_token(rng: &SystemRng) -> String {
244	let bytes = rng.infra_bytes_32();
245	bytes.iter().map(|b| format!("{:02x}", b)).collect()
246}