use email_address::EmailAddress; use log::debug; use rand::distributions::{Alphanumeric, DistString}; use time::Duration; use time::OffsetDateTime; use uuid::Uuid; 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, }; 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( &self, provider: &OauthProviderName, scope: Option, ) -> Result { if scope.is_some() { return Err(SecdError::NotImplemented( "Only default scopes are currently supported.".into(), )); } let p = self .store .read_oauth_provider(provider, None) .await .map_err(|_| SecdError::InternalError(INTERNAL_ERR_MSG.to_string()))?; 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))?; build_oauth_auth_url(&p, req_id) } /// create_validation_request_email /// /// Generate a request to validate the provided email. pub async fn create_validation_request_email( &self, email: &str, ) -> Result { let now = OffsetDateTime::now_utc(); let email = if EmailAddress::is_valid(email) { email } else { return Err(SecdError::InvalidEmailAddress); }; 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, }; 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) } 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) } }; self.email_messenger .send_email(email, &req_id.to_string(), &ev.code.unwrap(), mail_type) .await?; Ok(req_id) } /// 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( &self, validation_request_id: ValidationRequestId, code: ValidationSecretCode, ) -> Result { let mut v: Box = 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 }), }; if v.expired() || v.is_validated() { return Err(SecdError::InvalidCode); }; let mut identity = Identity { id: Uuid::new_v4(), data: None, created_at: OffsetDateTime::now_utc(), deleted_at: None, }; 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()) })?, }; 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, }; self.store .write_session(&s) .await .map_err(|e| util::log_err(e.into(), SecdError::Todo))?; Ok(s) } }