From 0920c4d4f30a3345870d385d5c6f3e0919228b56 Mon Sep 17 00:00:00 2001 From: benj Date: Mon, 12 Dec 2022 17:06:57 -0800 Subject: (oauth2 + email added): a mess that may or may not really work and needs to be refactored... --- crates/secd/src/lib.rs | 390 +++++++++++++++++++++++-------------------------- 1 file changed, 182 insertions(+), 208 deletions(-) (limited to 'crates/secd/src/lib.rs') diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs index 4feda04..faa92ca 100644 --- a/crates/secd/src/lib.rs +++ b/crates/secd/src/lib.rs @@ -1,28 +1,28 @@ mod client; +mod command; mod util; use std::sync::Arc; -use client::{ - email, - sqldb::{PgClient, SqliteClient}, - EmailMessenger, EmailMessengerError, Store, StoreError, -}; +use clap::ValueEnum; +use client::{EmailMessenger, EmailMessengerError, Store}; use derive_more::Display; use email_address::EmailAddress; -use log::error; -use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use strum_macros::{EnumString, EnumVariantNames}; -use time::{Duration, OffsetDateTime}; +use time::OffsetDateTime; +use url::Url; +use util::get_oauth_identity_data; use uuid::Uuid; const SESSION_SIZE_BYTES: usize = 32; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; -const VALIDATION_ATTEMPTS_MAX: i32 = 5; const VALIDATION_CODE_SIZE: usize = 6; +const INTERNAL_ERR_MSG: &str = "It seems an invariant was borked or something non-deterministic happened. Please file a bug with secd."; + #[derive(sqlx::FromRow, Debug, Serialize)] pub struct ApiKey { pub public_key: String, @@ -38,9 +38,11 @@ pub struct Authorization { pub struct Identity { #[sqlx(rename = "identity_public_id")] id: Uuid, - created_at: OffsetDateTime, #[serde(skip_serializing_if = "Option::is_none")] data: Option, + created_at: OffsetDateTime, + #[serde(skip_serializing_if = "Option::is_none")] + deleted_at: Option, } #[derive(sqlx::FromRow, Debug, Serialize)] @@ -58,6 +60,121 @@ pub struct Session { pub revoked_at: Option, } +#[async_trait::async_trait] +trait Validation { + fn expired(&self) -> bool; + fn is_validated(&self) -> bool; + async fn find_associated_identities( + &self, + store: Arc, + ) -> anyhow::Result>; + async fn validate( + &mut self, + i: &Identity, + store: Arc, + ) -> anyhow::Result<()>; +} + +#[async_trait::async_trait] +impl Validation for EmailValidation { + fn expired(&self) -> bool { + let now = OffsetDateTime::now_utc(); + self.expired_at < now + || self.revoked_at.map(|t| t < now).unwrap_or(false) + || self.deleted_at.map(|t| t < now).unwrap_or(false) + } + fn is_validated(&self) -> bool { + self.validated_at + .map(|t| t >= OffsetDateTime::now_utc()) + .unwrap_or(false) + } + async fn find_associated_identities( + &self, + store: Arc, + ) -> anyhow::Result> { + store.find_identity(None, Some(&self.email_address)).await + } + async fn validate( + &mut self, + i: &Identity, + store: Arc, + ) -> anyhow::Result<()> { + self.identity_id = Some(i.id); + self.validated_at = Some(OffsetDateTime::now_utc()); + store.write_email_validation(&self).await?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl Validation for OauthValidation { + fn expired(&self) -> bool { + let now = OffsetDateTime::now_utc(); + self.revoked_at.map(|t| t < now).unwrap_or(false) + || self.deleted_at.map(|t| t < now).unwrap_or(false) + } + fn is_validated(&self) -> bool { + self.validated_at + .map(|t| t >= OffsetDateTime::now_utc()) + .unwrap_or(false) + } + async fn find_associated_identities( + &self, + store: Arc, + ) -> anyhow::Result> { + let oauth_identity = get_oauth_identity_data(&self).await?; + + let identity = store + .find_identity(None, oauth_identity.email.as_deref()) + .await?; + + let now = OffsetDateTime::now_utc(); + if let Some(email) = oauth_identity.email.clone() { + let identity = identity.unwrap_or(Identity { + id: Uuid::new_v4(), + data: None, + created_at: OffsetDateTime::now_utc(), + deleted_at: None, + }); + store.write_identity(&identity).await?; + store.write_email(&email).await?; + store + .write_email_validation(&EmailValidation { + id: Some(Uuid::new_v4()), + identity_id: Some(identity.id), + email_address: email, + code: None, + is_oauth_derived: true, + created_at: now, + expired_at: now, + validated_at: Some(now), + revoked_at: None, + deleted_at: None, + }) + .await?; + Ok(Some(identity)) + } else { + Ok(identity) + } + } + async fn validate( + &mut self, + i: &Identity, + store: Arc, + ) -> anyhow::Result<()> { + self.identity_id = Some(i.id); + self.validated_at = Some(OffsetDateTime::now_utc()); + store.write_oauth_validation(&self).await?; + Ok(()) + } +} + +#[derive(Debug, EnumString)] +pub enum ValidationType { + Email, + Oauth, +} + #[derive(sqlx::FromRow, Debug)] pub struct EmailValidation { #[sqlx(rename = "email_validation_public_id")] @@ -66,16 +183,53 @@ pub struct EmailValidation { identity_id: Option, #[sqlx(rename = "address")] email_address: String, - attempts: i32, - code: String, - is_validated: bool, + code: Option, + is_oauth_derived: bool, + created_at: OffsetDateTime, + expired_at: OffsetDateTime, + validated_at: Option, + revoked_at: Option, + deleted_at: Option, +} + +#[derive(Debug)] +pub struct OauthValidation { + id: Option, + identity_id: Option, + oauth_provider: OauthProvider, + access_token: Option, + raw_response: Option, created_at: OffsetDateTime, - expires_at: OffsetDateTime, + validated_at: Option, revoked_at: Option, + deleted_at: Option, +} + +#[derive(Debug, Clone)] +pub struct OauthProvider { + pub name: OauthProviderName, + pub flow: Option, + pub base_url: Url, + pub response: OauthResponseType, + pub default_scope: String, + pub client_id: String, + pub client_secret: String, + pub redirect_url: Url, + pub created_at: OffsetDateTime, + pub deleted_at: Option, +} + +#[derive(Debug, Display, Clone, Copy, ValueEnum, EnumString)] +pub enum OauthResponseType { + Code, + IdToken, + None, + Token, } -#[derive(Copy, Display, Clone, Debug)] -pub enum OauthProvider { +// TODO: feature gate ValueEnum since it's only needed for iam builds +#[derive(Copy, Display, Clone, Debug, ValueEnum, EnumString)] +pub enum OauthProviderName { Amazon, Apple, Dropbox, @@ -121,19 +275,24 @@ pub type SessionSecret = String; pub type SessionSecretHash = String; pub type ValidationRequestId = Uuid; pub type ValidationSecretCode = String; +pub type OauthRedirectAuthUrl = Url; #[derive(Debug, derive_more::Display, thiserror::Error)] pub enum SecdError { - InvalidEmailAddress, - InvalidCode, - InitializationFailure(sqlx::Error), - IdentityIdShouldExistInvariant, EmailSendError(#[from] EmailMessengerError), - EmailValidationRequestError, EmailValidationExpiryOverflow, + EmailValidationRequestError, + OauthValidationRequestError, + IdentityIdShouldExistInvariant, + InitializationFailure(sqlx::Error), + InvalidCode, + InvalidEmailAddress, + InputValidation(String), + InternalError(String), + NotImplemented(String), SessionExpiryOverflow, Unauthenticated, - Unknown, + Todo, } pub struct Secd { @@ -142,191 +301,6 @@ pub struct Secd { } impl Secd { - pub async fn init( - auth_store: AuthStore, - conn_string: Option<&str>, - email_messenger: AuthEmail, - email_template_login: Option, - email_template_signup: Option, - ) -> Result { - let store = match auth_store { - AuthStore::Sqlite => { - SqliteClient::new( - sqlx::sqlite::SqlitePoolOptions::new() - .connect(conn_string.unwrap_or("sqlite::memory:".into())) - .await - .map_err(|e| SecdError::InitializationFailure(e))?, - ) - .await - } - AuthStore::Postgres => { - PgClient::new( - sqlx::postgres::PgPoolOptions::new() - .connect(conn_string.expect("No postgres connection string provided.")) - .await - .map_err(|e| SecdError::InitializationFailure(e))?, - ) - .await - } - rest @ _ => { - error!( - "requested an AuthStore which has not yet been implemented: {:?}", - rest - ); - unimplemented!() - } - }; - - let email_sender = match email_messenger { - // TODO: initialize email and SMS templates with secd - AuthEmail::LocalStub => email::LocalEmailStubber { - email_template_login, - email_template_signup, - }, - _ => unimplemented!(), - }; - - Ok(Secd { - store, - email_messenger: Arc::new(email_sender), - }) - } - /// create_validation_request - /// - /// Generate a request to validate the provided email. - pub async fn create_validation_request( - &self, - email: Option<&str>, - ) -> Result { - 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(), - attempts: 0, - code: Alphanumeric - .sample_string(&mut rand::thread_rng(), VALIDATION_CODE_SIZE) - .to_lowercase(), - is_validated: false, - created_at: now, - expires_at: now - .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0)) - .ok_or(SecdError::EmailValidationExpiryOverflow)?, - revoked_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::Unknown))? - { - 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::Unknown))? - }; - (req_id, client::EmailType::Login) - } - None => { - let identity = Identity { - id: Uuid::new_v4(), - created_at: OffsetDateTime::now_utc(), - data: None, - }; - self.store - .write_identity(&identity) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; - self.store - .write_email(identity.id, email) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; - - 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::Unknown))? - }; - - (req_id, client::EmailType::Signup) - } - }; - - self.email_messenger - .send_email(email, &req_id.to_string(), &ev.code, 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 ev = self - .store - .find_email_validation(Some(&validation_request_id), Some(&code)) - .await - .map_err(|e| util::log_err(e.into(), SecdError::EmailValidationExpiryOverflow))?; - - if ev.is_validated - || ev.expires_at < OffsetDateTime::now_utc() - || ev.attempts >= VALIDATION_ATTEMPTS_MAX - { - return Err(SecdError::InvalidCode); - }; - - ev.is_validated = true; - ev.attempts += 1; - self.store - .write_email_validation(&ev) - .await - .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; - - // TODO: clear previous sessions if they fit the criteria - let now = OffsetDateTime::now_utc(); - let s = Session { - identity_id: ev - .identity_id - .ok_or(SecdError::IdentityIdShouldExistInvariant)?, - 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::Unknown))?; - - Ok(s) - } /// get_identity /// /// Return all information associated with the identity id. @@ -350,7 +324,7 @@ impl Secd { Ok(Authorization { session }) } Ok(_) => Err(SecdError::Unauthenticated), - Err(_e) => Err(SecdError::Unknown), + Err(_e) => Err(SecdError::Todo), } } /// revoke_session -- cgit v1.2.3