aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/command
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2022-12-12 17:06:57 -0800
committerbenj <benj@rse8.com>2022-12-12 17:06:57 -0800
commit0920c4d4f30a3345870d385d5c6f3e0919228b56 (patch)
treef54668d91db469b7304758893a51b590c8f9b0de /crates/secd/src/command
parent3a4de13528fc85dcbe6bc9055d97ba5cc87f5712 (diff)
downloadsecdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.gz
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.bz2
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.lz
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.xz
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.zst
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.zip
(oauth2 + email added): a mess that may or may not really work and needs to be refactored...
Diffstat (limited to 'crates/secd/src/command')
-rw-r--r--crates/secd/src/command/admin.rs57
-rw-r--r--crates/secd/src/command/authn.rs230
-rw-r--r--crates/secd/src/command/mod.rs66
3 files changed, 353 insertions, 0 deletions
diff --git a/crates/secd/src/command/admin.rs b/crates/secd/src/command/admin.rs
new file mode 100644
index 0000000..b04dbef
--- /dev/null
+++ b/crates/secd/src/command/admin.rs
@@ -0,0 +1,57 @@
+use std::str::FromStr;
+
+use time::OffsetDateTime;
+use url::Url;
+
+use crate::{OauthProviderName, Secd, SecdError};
+
+impl OauthProviderName {
+ fn base_url(&self) -> Url {
+ match self {
+ OauthProviderName::Google => {
+ Url::from_str("https://accounts.google.com/o/oauth2/v2/auth").unwrap()
+ }
+ OauthProviderName::Microsoft => {
+ Url::from_str("https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
+ .unwrap()
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn default_scope(&self) -> String {
+ match self {
+ OauthProviderName::Google => "openid%20email".into(),
+ OauthProviderName::Microsoft => "openid%20email".into(),
+ _ => unimplemented!(),
+ }
+ }
+}
+
+impl Secd {
+ pub async fn create_oauth_provider(
+ &self,
+ provider: &OauthProviderName,
+ client_id: String,
+ client_secret: String,
+ redirect_url: Url,
+ ) -> Result<(), SecdError> {
+ self.store
+ .write_oauth_provider(&crate::OauthProvider {
+ name: provider.clone(),
+ flow: Some("default".into()),
+ base_url: provider.base_url(),
+ response: crate::OauthResponseType::Code,
+ default_scope: provider.default_scope(),
+ client_id,
+ client_secret,
+ redirect_url,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ })
+ .await
+ .map_err(|_| SecdError::Todo)?;
+
+ Ok(())
+ }
+}
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)
+ }
+}
diff --git a/crates/secd/src/command/mod.rs b/crates/secd/src/command/mod.rs
new file mode 100644
index 0000000..cd0d8c3
--- /dev/null
+++ b/crates/secd/src/command/mod.rs
@@ -0,0 +1,66 @@
+pub mod admin;
+pub mod authn;
+
+use crate::client::{
+ email,
+ sqldb::{PgClient, SqliteClient},
+};
+use crate::{AuthEmail, AuthStore, Secd, SecdError};
+use log::error;
+use std::sync::Arc;
+
+impl Secd {
+ /// init
+ ///
+ /// Initialize SecD with the specified configuration, established the necessary
+ /// constraints, persistance stores, and options.
+ 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),
+ })
+ }
+}