diff options
| author | benj <benj@rse8.com> | 2022-12-12 17:06:57 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-12 17:06:57 -0800 |
| commit | 0920c4d4f30a3345870d385d5c6f3e0919228b56 (patch) | |
| tree | f54668d91db469b7304758893a51b590c8f9b0de /crates/secd/src/command | |
| parent | 3a4de13528fc85dcbe6bc9055d97ba5cc87f5712 (diff) | |
| download | secdiam-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 '')
| -rw-r--r-- | crates/secd/src/command/admin.rs | 57 | ||||
| -rw-r--r-- | crates/secd/src/command/authn.rs | 230 | ||||
| -rw-r--r-- | crates/secd/src/command/mod.rs | 66 |
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), + }) + } +} |
