pub mod auth; mod client; mod util; use async_trait::async_trait; use auth::z::Relationship; use client::{ email::{EmailMessenger, EmailMessengerError, LocalMailer, Sendgrid}, spice::{Spice, SpiceError}, store::{ sql_db::{PgClient, SqliteClient}, Store, StoreError, }, }; use email_address::EmailAddress; use lettre::message::Mailbox; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::{ env::{set_var, var}, str::FromStr, sync::Arc, }; use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use util::crypter::{Crypter, CrypterError}; use uuid::Uuid; pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING"; pub const ENV_CRYPTER_SECRET_KEY: &str = "SECD_CRYPTER_SECRET_KEY"; pub const ENV_EMAIL_ADDRESS_FROM: &str = "SECD_EMAIL_ADDRESS_FROM"; pub const ENV_EMAIL_ADDRESS_REPLYTO: &str = "SECD_EMAIL_ADDRESS_REPLYTO"; 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_EMAIL_SENDGRID_API_KEY: &str = "SECD_EMAIL_SENDGRID_API_KEY"; pub const ENV_EMAIL_SIGNIN_MESSAGE: &str = "SECD_EMAIL_SIGNIN_MESSAGE"; pub const ENV_EMAIL_SIGNUP_MESSAGE: &str = "SECD_EMAIL_SIGNUP_MESSAGE"; pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET"; pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER"; const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6; const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5; const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true; const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t"; const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; const SESSION_SIZE_BYTES: usize = 32; 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 { // AuthenticationError(AuthnError); // AuthorizationError(AuthzError); // // InitializationError(...) AddressValidationFailed, AddressValidationSessionExchangeFailed, AddressValidationExpiredOrConsumed, CredentialAlreadyExists, CrypterError(#[from] CrypterError), 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 { crypter: Crypter, email_messenger: Arc, spice: Option>, store: Arc, cfg: Cfg, } struct Cfg { email_address_from: Option, email_address_replyto: Option, email_signup_message: Option, email_signin_message: Option, } #[async_trait] pub trait Authentication { async fn validate_address( &self, address_type: AddressType, identity_id: Option, ) -> Result; async fn complete_address_validation( &self, validation_id: &AddressValidationId, plaintext_token: Option, plaintext_code: Option, ) -> Result; async fn create_credential( &self, t: &CredentialType, identity_id: Option, ) -> Result; // async fn update_credential(&self, t: &CredentialType) -> Result<(), SecdError>; async fn reset_credential( &self, t: &CredentialType, address: &AddressType, ) -> Result; async fn validate_credential(&self, t: &CredentialType) -> Result; // async fn expire_session_chain(&self, t: &SessionToken) -> Result<(), SecdError>; // async fn expire_sessions(&self, i: &IdentityId) -> Result<(), SecdError>; // async fn get_identity(&self, t: &SessionToken) -> Result; // async fn get_session(&self, t: &SessionToken) -> Result; } #[async_trait] pub trait Authorization { async fn check(&self, r: &Relationship) -> Result; async fn expand(&self) -> Result<(), SecdError>; async fn read(&self) -> Result<(), SecdError>; async fn watch(&self) -> Result<(), SecdError>; async fn write(&self, relationships: &[Relationship]) -> Result<(), SecdError>; } #[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 }, } #[serde_as] #[derive(Debug, Serialize)] pub struct Credential { pub id: CredentialId, pub identity_id: IdentityId, pub t: CredentialType, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp::option")] pub revoked_at: Option, #[serde(with = "time::serde::timestamp::option")] pub deleted_at: Option, } #[serde_as] #[derive(Debug, Display, Serialize, Deserialize, EnumString)] pub enum CredentialType { Passphrase { key: String, value: String }, Oidc { value: String }, OneTimeCodes { codes: Vec }, // Totp { // #[serde_as(as = "DisplayFromStr")] // url: Url, // code: String, // }, WebAuthn { value: String }, } struct SecuredCredential { pub id: CredentialId, pub identity_id: IdentityId, pub t: CredentialType, pub created_at: OffsetDateTime, pub revoked_at: Option, pub deleted_at: Option, } enum SecuredCredentialType { Passphrase { key: String, value: String }, Oidc { 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 crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| { warn!( "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE ENVIORNMENT VARIABLE {}. THE DEFAULT KEY IS: {}", ENV_CRYPTER_SECRET_KEY, CRYPTER_SECRET_KEY_DEFAULT, ); CRYPTER_SECRET_KEY_DEFAULT.to_string() }); info!("starting client with auth_store: {:?}", auth_store); info!("starting client with email_messenger: {:?}", auth_store); let cfg = Cfg { email_address_from: var(ENV_EMAIL_ADDRESS_FROM) .ok() .and_then(|s| s.parse().ok()), email_address_replyto: var(ENV_EMAIL_ADDRESS_REPLYTO) .ok() .and_then(|s| s.parse().ok()), email_signup_message: var(ENV_EMAIL_SIGNUP_MESSAGE).ok(), email_signin_message: var(ENV_EMAIL_SIGNIN_MESSAGE).ok(), }; 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::new(), AuthEmailMessenger::Sendgrid => Sendgrid::new( var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"), ), _ => 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, }; let crypter = Crypter::new(crypter_secret_key.as_bytes()); Ok(Secd { crypter, email_messenger: email_sender, spice, store, cfg, }) } }