aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/command/authn.rs
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2022-12-24 00:43:38 -0800
committerbenj <benj@rse8.com>2022-12-24 00:43:38 -0800
commitc2268c285648ef02ece04de0d9df0813c6d70ff8 (patch)
treef84ec7ee42f97d78245f26d0c5a0c559cd35e89d /crates/secd/src/command/authn.rs
parentde6339da72af1d61ca5908b780977e2b037ce014 (diff)
downloadsecdiam-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.rs446
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))
+ }
}
}