pub mod auth; mod client; mod util; use client::{ email::{EmailMessenger, EmailMessengerError, LocalMailer}, spice::{Spice, SpiceError}, store::{ sql_db::{PgClient, SqliteClient}, Store, StoreError, }, }; use email_address::EmailAddress; use log::{error, info}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::{env::var, str::FromStr, sync::Arc}; use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use url::Url; use uuid::Uuid; pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING"; pub const ENV_EMAIL_MESSENGER: &str = "SECD_EMAIL_MESSENGER"; pub const ENV_EMAIL_MESSENGER_CLIENT_ID: &str = "SECD_EMAIL_MESSENGER_CLIENT_ID"; pub const ENV_EMAIL_MESSENGER_CLIENT_SECRET: &str = "SECD_EMAIL_MESSENGER_CLIENT_SECRET"; pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET"; pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER"; const SESSION_SIZE_BYTES: usize = 32; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6; const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5; const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true; pub type AddressId = Uuid; pub type AddressValidationId = Uuid; pub type CredentialId = Uuid; pub type IdentityId = Uuid; pub type MotifId = Uuid; pub type PhoneNumber = String; pub type RefId = Uuid; pub type SessionToken = String; #[derive(Debug, derive_more::Display, thiserror::Error)] pub enum SecdError { AddressValidationFailed, AddressValidationSessionExchangeFailed, AddressValidationExpiredOrConsumed, TooManyIdentities, IdentityNotFound, EmailMessengerError(#[from] EmailMessengerError), InvalidEmaillAddress(#[from] email_address::Error), FailedToProvideSessionIdentity(String), InvalidSession, StoreError(#[from] StoreError), StoreInitFailure(String), FailedToDecodeInput(#[from] hex::FromHexError), AuthorizationNotSupported(String), SpiceClient(#[from] SpiceError), Todo, } pub struct Secd { store: Arc, email_messenger: Arc, spice: Option>, } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] #[strum(ascii_case_insensitive)] pub enum AuthStore { Sqlite { conn: String }, Postgres { conn: String }, Redis { conn: String }, } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] #[strum(ascii_case_insensitive)] pub enum AuthEmailMessenger { Local, Ses, Mailgun, Sendgrid, } #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct Address { id: AddressId, t: AddressType, #[serde(with = "time::serde::timestamp")] created_at: OffsetDateTime, } #[serde_as] #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct AddressValidation { pub id: AddressValidationId, pub identity_id: Option, pub address: Address, pub method: AddressValidationMethod, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp")] pub expires_at: OffsetDateTime, #[serde(with = "time::serde::timestamp::option")] pub revoked_at: Option, #[serde(with = "time::serde::timestamp::option")] pub validated_at: Option, pub attempts: i32, #[serde_as(as = "serde_with::hex::Hex")] hashed_token: Vec, #[serde_as(as = "serde_with::hex::Hex")] hashed_code: Vec, } #[derive(Debug, Display, Serialize, EnumString)] pub enum AddressValidationMethod { Email, Sms, Oauth, } #[derive(Debug, Display, Serialize, EnumString)] pub enum AddressType { Email { email_address: Option }, Sms { phone_number: Option }, } #[derive(Debug, Serialize)] pub struct Credential { pub id: CredentialId, pub identity_id: IdentityId, pub t: CredentialType, } #[serde_as] #[derive(Debug, Serialize)] pub enum CredentialType { Passphrase { key: String, value: String, }, Oicd { value: String, }, OneTimeCodes { codes: Vec, }, Totp { #[serde_as(as = "DisplayFromStr")] url: Url, code: String, }, WebAuthn { value: String, }, } #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct Identity { pub id: IdentityId, pub address_validations: Vec, pub credentials: Vec, pub rules: Vec, // TODO: rules for (e.g. mfa reqs) pub metadata: Option, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp::option")] pub deleted_at: Option, } #[serde_as] #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct Session { pub identity_id: IdentityId, #[serde_as(as = "serde_with::hex::Hex")] #[serde(skip_serializing_if = "Vec::is_empty")] pub token: Vec, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp")] pub expired_at: OffsetDateTime, #[serde(with = "time::serde::timestamp::option")] pub revoked_at: Option, } impl Secd { /// init /// /// Initialize SecD with the specified configuration, established the necessary /// constraints, persistance stores, and options. pub async fn init(z_schema: Option<&str>) -> Result { let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok()); let email_messenger = AuthEmailMessenger::from_str( &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()), ) .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5"); let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok(); let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok(); info!("starting client with auth_store: {:?}", auth_store); info!("starting client with email_messenger: {:?}", auth_store); let store = match auth_store { AuthStore::Sqlite { conn } => { if z_schema.is_some() { return Err(SecdError::AuthorizationNotSupported( "sqlite is currently unsupported".into(), )); } SqliteClient::new( sqlx::sqlite::SqlitePoolOptions::new() .connect(&conn) .await .map_err(|e| { SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) })?, ) .await } AuthStore::Postgres { conn } => { PgClient::new( sqlx::postgres::PgPoolOptions::new() .connect(&conn) .await .map_err(|e| { SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) })?, ) .await } rest @ _ => { error!( "requested an AuthStore which has not yet been implemented: {:?}", rest ); unimplemented!() } }; let email_sender = match email_messenger { AuthEmailMessenger::Local => LocalMailer {}, _ => unimplemented!(), }; let spice = match z_schema { Some(schema) => { let c: Arc = Arc::new(Spice::new().await); c.write_schema(schema) .await .expect("failed to write authorization schema".into()); Some(c) } None => None, }; Ok(Secd { store, email_messenger: Arc::new(email_sender), spice, }) } }