diff options
| author | benj <benj@rse8.com> | 2022-12-24 00:43:38 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-24 00:43:38 -0800 |
| commit | c2268c285648ef02ece04de0d9df0813c6d70ff8 (patch) | |
| tree | f84ec7ee42f97d78245f26d0c5a0c559cd35e89d /crates/secd/src/command/authn.rs | |
| parent | de6339da72af1d61ca5908b780977e2b037ce014 (diff) | |
| download | secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.gz secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.bz2 secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.lz secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.xz secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.zst secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.zip | |
refactor everything with more abstraction and a nicer interface
Diffstat (limited to 'crates/secd/src/command/authn.rs')
| -rw-r--r-- | crates/secd/src/command/authn.rs | 446 |
1 files changed, 251 insertions, 195 deletions
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)) + } } } |
