aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/command/authn.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/secd/src/command/authn.rs230
1 files changed, 230 insertions, 0 deletions
diff --git a/crates/secd/src/command/authn.rs b/crates/secd/src/command/authn.rs
new file mode 100644
index 0000000..862d921
--- /dev/null
+++ b/crates/secd/src/command/authn.rs
@@ -0,0 +1,230 @@
+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<String>,
+ ) -> Result<OauthRedirectAuthUrl, SecdError> {
+ 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: Option<&str>,
+ ) -> Result<ValidationRequestId, SecdError> {
+ let now = OffsetDateTime::now_utc();
+
+ let email = match email {
+ Some(ea) => {
+ if EmailAddress::is_valid(ea) {
+ ea
+ } else {
+ return Err(SecdError::InvalidEmailAddress);
+ }
+ }
+ None => 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<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
+ }),
+ };
+
+ 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,
+ expires_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)
+ }
+}