diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/secd/src/command/admin.rs | 57 | ||||
| -rw-r--r-- | crates/secd/src/command/authn.rs | 446 | ||||
| -rw-r--r-- | crates/secd/src/command/mod.rs | 58 |
3 files changed, 284 insertions, 277 deletions
diff --git a/crates/secd/src/command/admin.rs b/crates/secd/src/command/admin.rs deleted file mode 100644 index b04dbef..0000000 --- a/crates/secd/src/command/admin.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::str::FromStr; - -use time::OffsetDateTime; -use url::Url; - -use crate::{OauthProviderName, Secd, SecdError}; - -impl OauthProviderName { - fn base_url(&self) -> Url { - match self { - OauthProviderName::Google => { - Url::from_str("https://accounts.google.com/o/oauth2/v2/auth").unwrap() - } - OauthProviderName::Microsoft => { - Url::from_str("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") - .unwrap() - } - _ => unimplemented!(), - } - } - - fn default_scope(&self) -> String { - match self { - OauthProviderName::Google => "openid%20email".into(), - OauthProviderName::Microsoft => "openid%20email".into(), - _ => unimplemented!(), - } - } -} - -impl Secd { - pub async fn create_oauth_provider( - &self, - provider: &OauthProviderName, - client_id: String, - client_secret: String, - redirect_url: Url, - ) -> Result<(), SecdError> { - self.store - .write_oauth_provider(&crate::OauthProvider { - name: provider.clone(), - flow: Some("default".into()), - base_url: provider.base_url(), - response: crate::OauthResponseType::Code, - default_scope: provider.default_scope(), - client_id, - client_secret, - redirect_url, - created_at: OffsetDateTime::now_utc(), - deleted_at: None, - }) - .await - .map_err(|_| SecdError::Todo)?; - - Ok(()) - } -} diff --git a/crates/secd/src/command/authn.rs b/crates/secd/src/command/authn.rs index 9c2babe..5590e8c 100644 --- a/crates/secd/src/command/authn.rs +++ b/crates/secd/src/command/authn.rs @@ -1,225 +1,281 @@ -use email_address::EmailAddress; -use log::debug; -use rand::distributions::{Alphanumeric, DistString}; -use time::Duration; -use time::OffsetDateTime; -use uuid::Uuid; +use std::str::FromStr; -use crate::util::{build_oauth_auth_url, get_oauth_access_token}; -use crate::OauthRedirectAuthUrl; -use crate::Validation; -use crate::ValidationType; -use crate::INTERNAL_ERR_MSG; use crate::{ - client, util, EmailValidation, Identity, OauthProviderName, Secd, SecdError, Session, - ValidationRequestId, ValidationSecretCode, EMAIL_VALIDATION_DURATION, SESSION_DURATION, - SESSION_SIZE_BYTES, VALIDATION_CODE_SIZE, + client::{ + email::{EmailValidationMessage, Sendable}, + store::{ + AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError, + }, + }, + util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, + Credential, CredentialType, Identity, IdentityId, PhoneNumber, Secd, SecdError, Session, + SessionToken, ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS, + ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION, }; +use email_address::EmailAddress; +use log::warn; +use rand::Rng; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; impl Secd { - /// create_validation_request_oauth - /// - /// Generate a request to validate with the specified oauth provider.[ - // TODO: How to handle different oauth "flows"? e.g. web app vs desktop vs mobile... - pub async fn create_validation_request_oauth( + pub async fn validate_email( &self, - provider: &OauthProviderName, - scope: Option<String>, - ) -> Result<OauthRedirectAuthUrl, SecdError> { - if scope.is_some() { - return Err(SecdError::NotImplemented( - "Only default scopes are currently supported.".into(), - )); + email_address: &str, + identity_id: Option<Uuid>, + ) -> Result<AddressValidation, SecdError> { + let email_address = EmailAddress::from_str(email_address)?; + // record address (idempotent operation) + let mut address = Address { + id: Uuid::new_v4(), + t: AddressType::Email { + email_address: Some(email_address.clone()), + }, + created_at: OffsetDateTime::now_utc(), + }; + + if let Err(StoreError::IdempotentCheckAlreadyExists) = + address.write(self.store.clone()).await + { + address = Address::find( + self.store.clone(), + &AddressLens { + id: None, + t: Some(&AddressType::Email { + email_address: Some(email_address.clone()), + }), + }, + ) + .await? + .into_iter() + .next() + .ok_or(SecdError::AddressValidationFailed)?; } - let p = self - .store - .read_oauth_provider(provider, None) - .await - .map_err(|_| SecdError::InternalError(INTERNAL_ERR_MSG.to_string()))?; + let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>()); + let code: String = vec![0; ADDRESSS_VALIDATION_CODE_SIZE as usize] + .into_iter() + .map(|_| char::from_digit(rand::thread_rng().gen_range(0..=9), 10).unwrap()) + .collect(); - let req_id = self - .store - .write_oauth_validation(&crate::OauthValidation { - id: Some(Uuid::new_v4()), - identity_id: None, - oauth_provider: p.clone(), - access_token: None, - raw_response: None, - created_at: OffsetDateTime::now_utc(), - validated_at: None, - revoked_at: None, - deleted_at: None, - }) - .await - .map_err(|e| util::to_secd_err(e, SecdError::OauthValidationRequestError))?; + let mut validation = AddressValidation { + id: Uuid::new_v4(), + identity_id, + address, + method: AddressValidationMethod::Email, + created_at: OffsetDateTime::now_utc(), + expires_at: OffsetDateTime::now_utc() + .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0)) + .ok_or(SecdError::Todo)?, + revoked_at: None, + validated_at: None, + attempts: 0, + hashed_token: util::hash(&secret.as_bytes()), + hashed_code: util::hash(&code.as_bytes()), + }; + + validation.write(self.store.clone()).await?; - build_oauth_auth_url(&p, req_id) + let msg =EmailValidationMessage { + recipient: email_address.clone(), + subject: "Confirm Your Email".into(), + body: format!("This is an email validation message. Click this link [{:?}?s={}] or use the code [{}]", validation.id, secret, code), + }; + + match msg.send().await { + Ok(_) => { /* TODO: Write down the message*/ } + Err(e) => { + validation.revoked_at = Some(OffsetDateTime::now_utc()); + validation.write(self.store.clone()).await?; + return Err(SecdError::EmailMessengerError(e)); + } + } + + Ok(validation) } - /// create_validation_request_email - /// - /// Generate a request to validate the provided email. - pub async fn create_validation_request_email( + pub async fn validate_sms( &self, - email: &str, - ) -> Result<ValidationRequestId, SecdError> { - let now = OffsetDateTime::now_utc(); + phone_number: &PhoneNumber, + ) -> Result<AddressValidation, SecdError> { + todo!() + } + pub async fn complete_address_validation( + &self, + validation_id: &AddressValidationId, + plaintext_token: Option<String>, + plaintext_code: Option<String>, + ) -> Result<Session, SecdError> { + let mut validation = AddressValidation::find( + self.store.clone(), + &AddressValidationLens { + id: Some(validation_id), + }, + ) + .await? + .into_iter() + .next() + .ok_or(SecdError::AddressValidationFailed)?; - let email = if EmailAddress::is_valid(email) { - email - } else { - return Err(SecdError::InvalidEmailAddress); - }; + if validation.validated_at.is_some() { + return Err(SecdError::AddressValidationExpiredOrConsumed); + } - let mut ev = EmailValidation { - id: None, - identity_id: None, - email_address: email.to_string(), - code: Some( - Alphanumeric - .sample_string(&mut rand::thread_rng(), VALIDATION_CODE_SIZE) - .to_lowercase(), - ), - is_oauth_derived: false, - created_at: now, - expired_at: now - .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0)) - .ok_or(SecdError::EmailValidationExpiryOverflow)?, - validated_at: None, - revoked_at: None, - deleted_at: None, - }; + validation.attempts += 1; + if validation.attempts > ADDRESS_VALIDATION_ALLOWS_ATTEMPTS as i32 { + warn!( + "validation failed: Too many validation attempts were tried for validation {:?}", + validation.id + ); + validation.write(self.store.clone()).await?; + return Err(SecdError::AddressValidationExpiredOrConsumed); + } - let (req_id, mail_type) = match self - .store - .find_identity(None, Some(email)) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Todo))? - { - Some(identity) => { - let req_id = { - ev.identity_id = Some(identity.id); - self.store - .write_email_validation(&ev) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Todo))? - }; - (req_id, client::EmailType::Login) + let hashed_token = plaintext_token.map(|s| util::hash(s.as_bytes())); + let hashed_code = plaintext_code.map(|c| util::hash(c.as_bytes())); + + let mut warn_msg = None; + match (hashed_token, hashed_code) { + (None, None) => { + warn_msg = Some("neither token nor hash was provided during the address validation session exchange"); } - None => { - self.store - .write_email(email) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Todo))?; - - let req_id = { - self.store - .write_email_validation(&ev) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Todo))? - }; - - (req_id, client::EmailType::Signup) + (Some(t), None) => { + if validation.hashed_token != t { + warn_msg = + Some("the provided token does not match the address validation token"); + } + } + (None, Some(c)) => { + if validation.hashed_code != c { + warn_msg = Some("the provided code does not match the address validation code"); + } + } + (Some(t), Some(c)) => { + if validation.hashed_token != t || validation.hashed_code != c { + warn_msg = Some("the provided token and code must both match the address validation token and code"); + } } }; - self.email_messenger - .send_email(email, &req_id.to_string(), &ev.code.unwrap(), mail_type) - .await?; + if let Some(msg) = warn_msg { + warn!("validation failed: {}", msg); + validation.write(self.store.clone()).await?; + return Err(SecdError::AddressValidationSessionExchangeFailed); + } + + let identity = Identity::find( + self.store.clone(), + &IdentityLens { + id: None, + address_type: Some(&validation.address.t), + validated_address: Some(true), + session_token_hash: None, + }, + ) + .await?; + + if !ADDRESS_VALIDATION_IDENTITY_SURJECTION && identity.len() > 1 { + warn!("validation failed: identity validation surjection disallowed"); + validation.write(self.store.clone()).await?; + return Err(SecdError::TooManyIdentities); + } + + let mut identity = identity.into_iter().next(); + if identity.is_none() { + let i = Identity { + id: Uuid::new_v4(), + address_validations: vec![], + credentials: vec![], + rules: vec![], + metadata: None, + created_at: OffsetDateTime::now_utc(), + deleted_at: None, + }; + i.write(self.store.clone()).await?; + identity = Some(i); + } - Ok(req_id) + assert!(identity.is_some()); + + // If the validation was attached to another identity, unless surjection is allowed, it cannot be recorded. + if !ADDRESS_VALIDATION_IDENTITY_SURJECTION + && validation.identity_id.is_some() + && identity.as_ref().map(|i| i.id) != validation.identity_id + { + warn!("validation failed: identity validation surjection is disallowed, but found existing identity for another account"); + validation.write(self.store.clone()).await?; + return Err(SecdError::TooManyIdentities); + } + + validation.identity_id = identity.map(|i| i.id); + validation.validated_at = Some(OffsetDateTime::now_utc()); + validation.write(self.store.clone()).await?; + + let session = Session::new(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?; + session.write(self.store.clone()).await?; + + Ok(session) } - /// exchange_secret_for_session - /// - /// Exchanges a secret, which consists of a validation_request_id and secret_code - /// for a session which allows authentication on behalf of the associated identity. - /// - /// Session secrets should be used to return authorization for the associated identity. - pub async fn exchange_code_for_session( + pub async fn create_credential( &self, - validation_request_id: ValidationRequestId, - code: ValidationSecretCode, - ) -> Result<Session, SecdError> { - let mut v: Box<dyn Validation> = match self - .store - .find_validation_type(&validation_request_id) - .await - .map_err(|e| util::to_secd_err(e, SecdError::Todo))? - { - ValidationType::Email => Box::new( - self.store - .find_email_validation(Some(&validation_request_id), Some(&code)) - .await - .map_err(|e| { - util::log_err(e.into(), SecdError::EmailValidationExpiryOverflow) - })?, - ), - ValidationType::Oauth => Box::new({ - let mut t = self - .store - .read_oauth_validation(&validation_request_id) - .await - .map_err(|e| util::to_secd_err(e, SecdError::Todo))?; - - let access_token = get_oauth_access_token(&t, &code) - .await - .map_err(|_| SecdError::Todo)?; - - t.access_token = Some(access_token); - t - }), - }; + t: CredentialType, + key: String, + value: Option<String>, + ) -> Result<Credential, SecdError> { + todo!() + } - if v.expired() || v.is_validated() { - return Err(SecdError::InvalidCode); - }; + pub async fn validate_credential( + &self, + t: CredentialType, + key: String, + value: Option<String>, + ) -> Result<Session, SecdError> { + todo!() + } - let mut identity = Identity { - id: Uuid::new_v4(), - data: None, - created_at: OffsetDateTime::now_utc(), - deleted_at: None, - }; + pub async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError> { + let token = hex::decode(t)?; + let mut session = Session::find( + self.store.clone(), + &SessionLens { + token_hash: Some(&util::hash(&token)), + identity_id: None, + }, + ) + .await?; + assert!(session.len() <= 1, "get session failed: multiple sessions found for a single token. This is very _very_ bad."); - match v - .find_associated_identities(self.store.clone()) - .await - .map_err(|e| util::to_secd_err(e, SecdError::IdentityIdShouldExistInvariant))? - { - Some(i) => identity.id = i.id, - _ => self.store.write_identity(&identity).await.map_err(|_| { - SecdError::InternalError("failed to write identity during session exchange".into()) - })?, - }; + if session.is_empty() { + return Err(SecdError::InvalidSession); + } else { + let mut session = session.swap_remove(0); + session.token = token; + Ok(session) + } + } - v.validate(&identity, self.store.clone()) - .await - .map_err(|e| { - util::to_secd_err( - e, - SecdError::InternalError( - "failed to update validation during session exchange".into(), - ), - ) - })?; - - // TODO: clear previous sessions if they fit the criteria - let now = OffsetDateTime::now_utc(); - let s = Session { - identity_id: identity.id, - secret: Some(Alphanumeric.sample_string(&mut rand::thread_rng(), SESSION_SIZE_BYTES)), - created_at: now, - expired_at: now - .checked_add(Duration::new(SESSION_DURATION, 0)) - .ok_or(SecdError::SessionExpiryOverflow)?, - revoked_at: None, - }; + pub async fn get_identity(&self, i: &SessionToken) -> Result<Identity, SecdError> { + let token_hash = util::hash(&hex::decode(i)?); + let mut i = Identity::find( + self.store.clone(), + &IdentityLens { + id: None, + address_type: None, + validated_address: None, + session_token_hash: Some(token_hash), + }, + ) + .await?; - self.store - .write_session(&s) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Todo))?; + assert!( + i.len() <= 1, + "The provided id refers to more than one identity. This is very _very_ bad." + ); - Ok(s) + if i.is_empty() { + return Err(SecdError::IdentityNotFound); + } else { + Ok(i.swap_remove(0)) + } } } diff --git a/crates/secd/src/command/mod.rs b/crates/secd/src/command/mod.rs index cd0d8c3..c14cf6c 100644 --- a/crates/secd/src/command/mod.rs +++ b/crates/secd/src/command/mod.rs @@ -1,42 +1,54 @@ -pub mod admin; pub mod authn; -use crate::client::{ - email, - sqldb::{PgClient, SqliteClient}, +use super::{AuthEmailMessenger, AuthStore, Secd, SecdError}; +use crate::{ + client::{ + email, + store::sql_db::{PgClient, SqliteClient}, + }, + ENV_AUTH_STORE_CONN_STRING, ENV_EMAIL_MESSENGER, ENV_EMAIL_MESSENGER_CLIENT_ID, + ENV_EMAIL_MESSENGER_CLIENT_SECRET, }; -use crate::{AuthEmail, AuthStore, Secd, SecdError}; -use log::error; -use std::sync::Arc; +use log::{error, info}; +use std::{env::var, str::FromStr, sync::Arc}; impl Secd { /// init /// /// Initialize SecD with the specified configuration, established the necessary /// constraints, persistance stores, and options. - pub async fn init( - auth_store: AuthStore, - conn_string: Option<&str>, - email_messenger: AuthEmail, - email_template_login: Option<String>, - email_template_signup: Option<String>, - ) -> Result<Self, SecdError> { + pub async fn init() -> Result<Self, SecdError> { + 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 => { + AuthStore::Sqlite { conn } => { SqliteClient::new( sqlx::sqlite::SqlitePoolOptions::new() - .connect(conn_string.unwrap_or("sqlite::memory:".into())) + .connect(&conn) .await - .map_err(|e| SecdError::InitializationFailure(e))?, + .map_err(|e| { + SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) + })?, ) .await } - AuthStore::Postgres => { + AuthStore::Postgres { conn } => { PgClient::new( sqlx::postgres::PgPoolOptions::new() - .connect(conn_string.expect("No postgres connection string provided.")) + .connect(&conn) .await - .map_err(|e| SecdError::InitializationFailure(e))?, + .map_err(|e| { + SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) + })?, ) .await } @@ -50,11 +62,7 @@ impl Secd { }; let email_sender = match email_messenger { - // TODO: initialize email and SMS templates with secd - AuthEmail::LocalStub => email::LocalEmailStubber { - email_template_login, - email_template_signup, - }, + AuthEmailMessenger::Local => email::LocalMailer {}, _ => unimplemented!(), }; |
