aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/secd/src/lib.rs390
1 files changed, 182 insertions, 208 deletions
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<String>,
+ created_at: OffsetDateTime,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ deleted_at: Option<OffsetDateTime>,
}
#[derive(sqlx::FromRow, Debug, Serialize)]
@@ -58,6 +60,121 @@ pub struct Session {
pub revoked_at: Option<OffsetDateTime>,
}
+#[async_trait::async_trait]
+trait Validation {
+ fn expired(&self) -> bool;
+ fn is_validated(&self) -> bool;
+ async fn find_associated_identities(
+ &self,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<Option<Identity>>;
+ async fn validate(
+ &mut self,
+ i: &Identity,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> 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<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<Option<Identity>> {
+ store.find_identity(None, Some(&self.email_address)).await
+ }
+ async fn validate(
+ &mut self,
+ i: &Identity,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> 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<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<Option<Identity>> {
+ 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<dyn Store + Send + Sync>,
+ ) -> 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<IdentityId>,
#[sqlx(rename = "address")]
email_address: String,
- attempts: i32,
- code: String,
- is_validated: bool,
+ code: Option<String>,
+ is_oauth_derived: bool,
+ created_at: OffsetDateTime,
+ expired_at: OffsetDateTime,
+ validated_at: Option<OffsetDateTime>,
+ revoked_at: Option<OffsetDateTime>,
+ deleted_at: Option<OffsetDateTime>,
+}
+
+#[derive(Debug)]
+pub struct OauthValidation {
+ id: Option<Uuid>,
+ identity_id: Option<IdentityId>,
+ oauth_provider: OauthProvider,
+ access_token: Option<String>,
+ raw_response: Option<String>,
created_at: OffsetDateTime,
- expires_at: OffsetDateTime,
+ validated_at: Option<OffsetDateTime>,
revoked_at: Option<OffsetDateTime>,
+ deleted_at: Option<OffsetDateTime>,
+}
+
+#[derive(Debug, Clone)]
+pub struct OauthProvider {
+ pub name: OauthProviderName,
+ pub flow: Option<String>,
+ 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<OffsetDateTime>,
+}
+
+#[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<String>,
- email_template_signup: Option<String>,
- ) -> Result<Self, SecdError> {
- 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<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(),
- 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<Session, SecdError> {
- 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