Skip to main content

reifydb_auth/service/
mod.rs

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