diff options
| author | benj <benj@rse8.com> | 2022-12-30 15:57:36 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-30 15:57:36 -0800 |
| commit | 8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3 (patch) | |
| tree | 1ff85fd9fbd94a5559f9dbac755973fd58b31f28 /crates/secd/src/auth/n.rs | |
| parent | f0ea9ecd17b03605d747044874a26e1bd52c0ee1 (diff) | |
| download | secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.gz secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.bz2 secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.lz secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.xz secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.zst secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.zip | |
impl authZ write and check (depends on spicedb for now)
Diffstat (limited to 'crates/secd/src/auth/n.rs')
| -rw-r--r-- | crates/secd/src/auth/n.rs | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs new file mode 100644 index 0000000..1d3b2d5 --- /dev/null +++ b/crates/secd/src/auth/n.rs @@ -0,0 +1,287 @@ +use std::str::FromStr; + +use crate::{ + client::{ + email::{EmailValidationMessage, Sendable}, + store::{ + AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError, + }, + }, + util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, + Credential, CredentialType, Identity, 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 { + pub async fn validate_email( + &self, + 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 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 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?; + + 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) + } + pub async fn validate_sms( + &self, + 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)?; + + if validation.validated_at.is_some() { + return Err(SecdError::AddressValidationExpiredOrConsumed); + } + + 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 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"); + } + (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"); + } + } + }; + + 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); + } + + 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) + } + pub async fn create_credential( + &self, + t: CredentialType, + key: String, + value: Option<String>, + ) -> Result<Credential, SecdError> { + todo!() + } + + pub async fn validate_credential( + &self, + t: CredentialType, + key: String, + value: Option<String>, + ) -> Result<Session, SecdError> { + todo!() + } + + 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."); + + if session.is_empty() { + return Err(SecdError::InvalidSession); + } else { + let mut session = session.swap_remove(0); + session.token = token; + Ok(session) + } + } + + 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?; + + assert!( + i.len() <= 1, + "The provided id refers to more than one identity. This is very _very_ bad." + ); + + if i.is_empty() { + return Err(SecdError::IdentityNotFound); + } else { + Ok(i.swap_remove(0)) + } + } + + pub async fn revoke_session(&self, session: &mut Session) -> Result<(), SecdError> { + session.revoked_at = Some(OffsetDateTime::now_utc()); + session.write(self.store.clone()).await?; + Ok(()) + } +} |
