reifydb-auth 0.9.1

Authentication and authorization module for ReifyDB
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 ReifyDB

mod authenticate;
mod github;
mod solana;
mod token;

use std::{collections::HashMap, ops::Deref, sync::Arc};

use reifydb_catalog::{catalog::Catalog, create_token};
use reifydb_core::interface::catalog::token::Token;
use reifydb_runtime::context::{clock::Clock, rng::Rng as SystemRng};
use reifydb_transaction::transaction::{Transaction, admin::AdminTransaction, query::QueryTransaction};
use reifydb_value::{
	error::Error,
	value::{Value, datetime::DateTime, duration::Duration, identity::IdentityId, value_type::ValueType},
};
use tracing::instrument;

use crate::{
	challenge::ChallengeStore,
	github::{GithubApi, GithubConfig, default_api},
	registry::AuthenticationRegistry,
};

pub trait AuthEngine: Send + Sync {
	fn begin_admin(&self) -> Result<AdminTransaction, Error>;
	fn begin_query(&self) -> Result<QueryTransaction, Error>;
	fn catalog(&self) -> Catalog;
}

#[derive(Debug, Clone)]
pub enum AuthResponse {
	Authenticated {
		identity: IdentityId,
		token: String,
	},

	Challenge {
		challenge_id: String,
		payload: HashMap<String, String>,
	},

	Failed {
		reason: String,
	},
}

pub struct AuthConfigurator {
	session_ttl: Option<Duration>,
	challenge_ttl: Duration,
	github: Option<GithubConfig>,
}

impl Default for AuthConfigurator {
	fn default() -> Self {
		Self::new()
	}
}

impl AuthConfigurator {
	pub fn new() -> Self {
		Self {
			session_ttl: Some(Duration::from_seconds(24 * 60 * 60).unwrap()),
			challenge_ttl: Duration::from_seconds(60).unwrap(),
			github: None,
		}
	}

	pub fn session_ttl(mut self, ttl: Duration) -> Self {
		self.session_ttl = Some(ttl);
		self
	}

	pub fn no_session_ttl(mut self) -> Self {
		self.session_ttl = None;
		self
	}

	pub fn challenge_ttl(mut self, ttl: Duration) -> Self {
		self.challenge_ttl = ttl;
		self
	}

	pub fn github(mut self, config: GithubConfig) -> Self {
		self.github = Some(config);
		self
	}

	pub fn configure(self) -> AuthServiceConfig {
		AuthServiceConfig {
			session_ttl: self.session_ttl,
			challenge_ttl: self.challenge_ttl,
			github: self.github,
		}
	}
}

#[derive(Debug, Clone)]
pub struct AuthServiceConfig {
	pub session_ttl: Option<Duration>,

	pub challenge_ttl: Duration,

	pub github: Option<GithubConfig>,
}

impl Default for AuthServiceConfig {
	fn default() -> Self {
		AuthConfigurator::new().configure()
	}
}

pub struct Inner {
	pub(crate) engine: Arc<dyn AuthEngine>,
	pub(crate) auth_registry: Arc<AuthenticationRegistry>,
	pub(crate) challenges: ChallengeStore,
	pub(crate) rng: SystemRng,
	pub(crate) clock: Clock,
	pub(crate) session_ttl: Option<Duration>,
	pub(crate) github: Option<GithubAuth>,
}

pub(crate) struct GithubAuth {
	pub(crate) config: GithubConfig,
	pub(crate) api: Arc<dyn GithubApi>,
}

#[derive(Clone)]
pub struct AuthService(Arc<Inner>);

impl Deref for AuthService {
	type Target = Inner;
	fn deref(&self) -> &Inner {
		&self.0
	}
}

impl AuthService {
	pub fn new(
		engine: Arc<dyn AuthEngine>,
		auth_registry: Arc<AuthenticationRegistry>,
		rng: SystemRng,
		clock: Clock,
		config: AuthServiceConfig,
	) -> Self {
		Self::with_github_api(engine, auth_registry, rng, clock, config, default_api())
	}

	pub fn with_github_api(
		engine: Arc<dyn AuthEngine>,
		auth_registry: Arc<AuthenticationRegistry>,
		rng: SystemRng,
		clock: Clock,
		config: AuthServiceConfig,
		api: Arc<dyn GithubApi>,
	) -> Self {
		Self(Arc::new(Inner {
			engine,
			auth_registry,
			challenges: ChallengeStore::new(config.challenge_ttl),
			rng,
			clock,
			session_ttl: config.session_ttl,
			github: config.github.map(|config| GithubAuth {
				config,
				api,
			}),
		}))
	}

	pub fn auth_registry(&self) -> &Arc<AuthenticationRegistry> {
		&self.auth_registry
	}

	pub(super) fn now(&self) -> Result<DateTime, Error> {
		Ok(self.clock.now())
	}

	pub(super) fn expires_at(&self) -> Result<Option<DateTime>, Error> {
		match self.session_ttl {
			Some(ttl) => {
				let ttl_nanos = ttl.as_nanos()? as u64;
				let nanos = self.clock.now().to_nanos().saturating_add(ttl_nanos);
				Ok(Some(DateTime::from_nanos(nanos)))
			}
			None => Ok(None),
		}
	}

	pub(super) fn persist_token(&self, token: &str, identity: IdentityId) -> Result<Token, Error> {
		let mut admin = self.engine.begin_admin()?;

		let def = create_token(&mut admin, token, identity, self.expires_at()?, self.now()?)?;

		admin.commit()?;
		Ok(def)
	}

	pub fn create_token(
		&self,
		token: &str,
		identity: IdentityId,
		expires_at: Option<DateTime>,
	) -> Result<Token, Error> {
		let mut admin = self.engine.begin_admin()?;
		let def = create_token(&mut admin, token, identity, expires_at, self.now()?)?;
		admin.commit()?;
		Ok(def)
	}

	#[instrument(name = "auth::create_session", level = "debug", skip(self))]
	pub fn create_session(&self, identity: IdentityId, ttl: Option<Duration>) -> Result<Token, Error> {
		let expires_at = match ttl {
			Some(ttl) => {
				let nanos = self.clock.now().to_nanos().saturating_add(ttl.as_nanos()? as u64);
				Some(DateTime::from_nanos(nanos))
			}
			None => self.expires_at()?,
		};
		self.create_token(&generate_session_token(&self.rng), identity, expires_at)
	}

	pub(super) fn set_lookup_attribute(
		&self,
		admin: &mut AdminTransaction,
		identity: IdentityId,
		name: &str,
		value: &str,
	) -> Result<(), Error> {
		let catalog = self.engine.catalog();
		let attribute =
			match catalog.find_identity_attribute_by_name(&mut Transaction::Admin(&mut *admin), name)? {
				Some(attribute) => attribute,
				None => catalog.create_identity_attribute(admin, name, ValueType::Utf8)?,
			};
		catalog.set_identity_attribute_value(admin, identity, &attribute, Value::Utf8(value.to_string()))?;
		Ok(())
	}
}

pub(super) fn generate_session_token(rng: &SystemRng) -> String {
	let bytes = rng.infra_bytes_32();
	bytes.iter().map(|b| format!("{:02x}", b)).collect()
}