1mod 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}