diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/mod.rs | 81 | ||||
| -rw-r--r-- | src/client/sqldb.rs | 172 | ||||
| -rw-r--r-- | src/ipl/authn.rs | 1 | ||||
| -rw-r--r-- | src/ipl/authz.rs | 1 | ||||
| -rw-r--r-- | src/ipl/mod.rs | 2 | ||||
| -rw-r--r-- | src/lib.rs | 171 | ||||
| -rw-r--r-- | src/main.old | 112 | ||||
| -rw-r--r-- | src/util/mod.rs | 15 |
8 files changed, 555 insertions, 0 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..bb32e2c --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,81 @@ +pub mod sqldb; + +use thiserror::Error; +use uuid::Uuid; + +use super::Identity; + +#[derive(Error, Debug)] +pub enum StoreError { + #[error("sqlx client error")] + SqlxError(#[from] sqlx::Error), + #[error( + "More than one oauth provider identified, but no client_id was provided for disambiguation" + )] + TooManyOauthProviders, + #[error("Oath provider not registered. First register the Oauth provider before executing")] + OauthProviderNotRegistered, + #[error("An unknown error occurred")] + Unknown, +} + +#[async_trait::async_trait] +pub trait Store { + // async fn read_oauth_authorization_location( + // &self, + // provider: OauthProvider, + // client_id: Option<OauthClientId>, + // ) -> Result<String, StoreError>; + + // async fn write_oauth_authorization_request( + // &self, + // identity_id: Uuid, + // provider: OauthProvider, + // raw: String, + // state: String, + // ) -> Result<(), StoreError>; + + // async fn write_oauth_provider( + // &self, + // provider: OauthProvider, + // consent_uri: OauthConsentUri, + // client_id: OauthClientId, + // client_secret: OauthClientSecretEncrypted, + // redirect_uri: String, + // ) -> Result<(), StoreError>; + + // fn read_email_challenge(&self) -> Result<T, StoreError>; + // fn write_email_challenge(&self) -> Result<T, StoreError>; + + async fn write_email(&self, id: Uuid, email_address: &str) -> Result<(), StoreError>; + async fn write_email_validation_request( + &self, + id: Uuid, + email_address: &str, + ) -> Result<Uuid, StoreError>; + + async fn find_identity( + &self, + id: Option<&Uuid>, + email: Option<&str>, + ) -> Result<Option<Identity>, StoreError>; + async fn write_identity(&self, i: &Identity) -> Result<(), StoreError>; + async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError>; + + // fn read_sms_challenge(&self) -> Result<T, StoreError>; + // fn write_sms_challenge(&self) -> Result<T, StoreError>; +} + +// #[derive(sqlx::FromRow, Debug)] +// struct Identity { +// #[sqlx(rename = "identity_public_id")] +// id: Uuid, +// } + +// #[derive(sqlx::FromRow, Debug)] +// struct OauthProviderRecord { +// consent_uri: String, +// client_id: OauthClientId, +// client_secret_encrypted: OauthClientSecretEncrypted, +// redirect_uri: String, +// } diff --git a/src/client/sqldb.rs b/src/client/sqldb.rs new file mode 100644 index 0000000..6ad0cc1 --- /dev/null +++ b/src/client/sqldb.rs @@ -0,0 +1,172 @@ +use log::{debug, error}; +use uuid::Uuid; + +use crate::{util, Identity}; + +use super::{Store, StoreError}; + +pub struct SqliteClient { + pool: sqlx::Pool<sqlx::Sqlite>, +} + +impl SqliteClient { + pub async fn new(pool: sqlx::Pool<sqlx::Sqlite>) -> Self { + sqlx::migrate!("store/sqlite/migrations") + .run(&pool) + .await + .expect( + "Failed to execute migrations. This appears to be a secd issue. File a bug at https://www.github.com/secd-lib" + ); + + sqlx::query("pragma foreign_keys = on") + .execute(&pool) + .await + .expect( + "Failed to initialize FK pragma. File a bug at https://www.github.com/secd-lib", + ); + + SqliteClient { pool } + } +} + +impl SqliteClient { + async fn read_identity_raw_id(&self, id: &Uuid) -> Result<i64, StoreError> { + Ok(sqlx::query_as::<_, (i64,)>( + " +select identity_id from identity where identity_public_id = ?", + ) + .bind(id) + .fetch_one(&self.pool) + .await + .map_err(util::log_err)? + .0) + } + + async fn read_email_raw_id(&self, address: &str) -> Result<i64, StoreError> { + Ok(sqlx::query_as::<_, (i64,)>( + " +select email_id from email where address = ?", + ) + .bind(address) + .fetch_one(&self.pool) + .await + .map_err(util::log_err)? + .0) + } +} + +#[async_trait::async_trait] +impl Store for SqliteClient { + async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> { + let mut tx = self.pool.begin().await?; + + let identity_id = self.read_identity_raw_id(&identity_id).await?; + + let email_id: (i64,) = sqlx::query_as( + " +insert into email (address) values (?) returning email_id", + ) + .bind(email_address) + .fetch_one(&mut tx) + .await + .map_err(util::log_err)?; + + debug!("identity: {}, email: {}", identity_id, email_id.0); + + sqlx::query( + " +insert into identity_email (identity_id, email_id) values (?,?);", + ) + .bind(identity_id) + .bind(email_id.0) + .execute(&mut tx) + .await + .map_err(util::log_err)?; + + tx.commit().await?; + + Ok(()) + } + + async fn write_email_validation_request( + &self, + identity_id: Uuid, + email_address: &str, + ) -> Result<Uuid, StoreError> { + let identity_id = self.read_identity_raw_id(&identity_id).await?; + let email_id = self.read_email_raw_id(email_address).await?; + + let request_id = Uuid::new_v4(); + sqlx::query(" +insert into email_validation_request (email_validation_request_public_id, identity_email_id, is_validated) +values (?, (select identity_email_id from identity_email where identity_id = ? and email_id =?), ?)", + ) + .bind(request_id) + .bind(identity_id) + .bind(email_id) + .bind(false) + .execute(&self.pool) + .await + .map_err(util::log_err)?; + + Ok(request_id) + } + + async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> { + sqlx::query( + " +insert into identity (identity_public_id, data, created_at) values (?, ?, ?)", + ) + .bind(i.id) + .bind(i.data.clone()) + .bind(i.created_at) + .execute(&self.pool) + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; + + Ok(()) + } + + async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError> { + Ok(sqlx::query_as::<_, Identity>( + " +select identity_public_id, data, created_at from identity where identity_public_id = ?", + ) + .bind(id) + .fetch_one(&self.pool) + .await + .map_err(util::log_err)?) + } + + async fn find_identity( + &self, + id: Option<&Uuid>, + email: Option<&str>, + ) -> Result<Option<Identity>, StoreError> { + Ok( + match sqlx::query_as::<_, Identity>( + " +select identity_public_id, data, i.created_at +from identity i +join identity_email ie using (identity_id) +join email e using (email_id) +where ((? is null) or (i.identity_public_id = ?)) +and ((? is null) or (e.address = ?));", + ) + .bind(id) + .bind(id) + .bind(email) + .bind(email) + .fetch_one(&self.pool) + .await + { + Ok(i) => Some(i), + Err(sqlx::Error::RowNotFound) => None, + Err(e) => return Err(StoreError::SqlxError(e)), + }, + ) + } +} diff --git a/src/ipl/authn.rs b/src/ipl/authn.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/ipl/authn.rs @@ -0,0 +1 @@ + diff --git a/src/ipl/authz.rs b/src/ipl/authz.rs new file mode 100644 index 0000000..f2f23e4 --- /dev/null +++ b/src/ipl/authz.rs @@ -0,0 +1 @@ +// TODO: Authorization suite diff --git a/src/ipl/mod.rs b/src/ipl/mod.rs new file mode 100644 index 0000000..1946995 --- /dev/null +++ b/src/ipl/mod.rs @@ -0,0 +1,2 @@ +pub mod authn; +pub mod authz; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7856d5c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,171 @@ +mod client; +mod util; + +use std::sync::Arc; + +use client::{sqldb::SqliteClient, Store, StoreError}; +use derive_more::Display; +use email_address::EmailAddress; +use log::{error, info}; +use serde::Serialize; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Copy, Display, Clone, Debug)] +pub enum OauthProvider { + Amazon, + Apple, + Dropbox, + Facebook, + Github, + Gitlab, + Google, + Instagram, + LinkedIn, + Microsoft, + Paypal, + Reddit, + Spotify, + Strava, + Stripe, + Twitch, + Twitter, + WeChat, +} + +#[derive(Display, Debug)] +pub enum AuthStore { + Sqlite, + Postgres, + MySql, + Mongo, + Dynamo, + Redis, +} + +pub type OauthClientId = String; +pub type OauthClientSecretEncrypted = String; +pub type OauthConsentUri = String; + +pub type IdentityId = Uuid; + +////////////////////////////////////////////////// +// Resources +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Identity { + #[sqlx(rename = "identity_public_id")] + id: Uuid, + created_at: sqlx::types::time::OffsetDateTime, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option<String>, +} + +pub struct ApiKey {} + +pub struct Session {} + +pub struct ValidationRequest {} +////////////////////////////////////////////////// +#[derive(Debug, derive_more::Display, thiserror::Error)] +pub enum SecdError { + InvalidEmailAddress, + SqliteInitializationFailure(sqlx::Error), + StoreError(#[from] StoreError), + EmailValidationRequestError, + Unknown, +} + +pub struct Secd { + store: Arc<dyn Store + Send + Sync + 'static>, +} + +impl Secd { + pub async fn init( + auth_store: AuthStore, + conn_string: Option<String>, + // TODO: Turn Secd into a trait and impl separately. + // TODO: initialize email and SMS templates with secd + ) -> Result<Self, SecdError> { + let store = match auth_store { + AuthStore::Sqlite => SqliteClient::new( + sqlx::sqlite::SqlitePoolOptions::new() + .connect(conn_string.unwrap_or("sqlite::memory:".into()).as_str()) + .await + .map_err(|e| SecdError::SqliteInitializationFailure(e))?, + ), + // TODO: if AuthStore is provided, then configure the client. + _ => return Err(SecdError::Unknown), + } + .await; + + Ok(Secd { + store: Arc::new(store), + }) + } + + pub async fn create_identity(&self) -> Result<Uuid, SecdError> { + let id = Uuid::new_v4(); + self.store + .write_identity(&Identity { + id, + created_at: OffsetDateTime::now_utc(), + data: None, + }) + .await?; + + Ok(id) + } + + pub async fn create_validation_request(&self, email: Option<&str>) -> Result<(), SecdError> { + // TODO: refactor based on email, phone, or some other template? Or break up the API? + let email = match email { + Some(ea) => { + if EmailAddress::is_valid(ea) { + ea + } else { + return Err(SecdError::InvalidEmailAddress); + } + } + None => return Err(SecdError::InvalidEmailAddress), + }; + + match self.store.find_identity(None, Some(email)).await? { + Some(identity) => { + error!("TODO: implement email send with LOGIN template"); + error!("TODO: send to: {}", email); + let req_id = self + .store + .write_email_validation_request(identity.id, email) + .await?; + + // TODO: provide some dummy email handlers that are used when testing locally... + error!("TODO: when the request comes back, it needs to hit something like /iam/identity/1234/email-validation/1234?code=2345"); + error!("TODO: consequently, we may want to shorten the url by providing a quick access code and/or /iam/email-validation/1234/validate"); + } + None => { + let identity = Identity { + id: Uuid::new_v4(), + created_at: OffsetDateTime::now_utc(), + data: None, + }; + self.store.write_identity(&identity).await?; + self.store.write_email(identity.id, email).await?; + error!("TODO: implement email send with SIGN_UP template"); + self.store + .write_email_validation_request(identity.id, email) + .await?; + } + } + + error!("TODO: think about returning the identity id for which this validation request was created"); + Ok(()) + } + + pub async fn get_identity(&self, id: IdentityId) -> Result<Identity, SecdError> { + Ok(self.store.read_identity(&id).await?) + } + + pub async fn create_email_validation(email: String) -> Result<(), SecdError> { + Ok(()) + } +} diff --git a/src/main.old b/src/main.old new file mode 100644 index 0000000..e9d36c5 --- /dev/null +++ b/src/main.old @@ -0,0 +1,112 @@ +mod api; +mod client; +mod service; +mod util; + +use std::error::Error; + +use client::sqldb::PostgresClient; +use env_logger::Env; +use service::authn::Authn; +use sqlx::postgres::PgPoolOptions; + +#[async_std::main] +async fn main() -> Result<(), Box<dyn Error>> { + env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init(); + + // Load configuration + // which DB do you want to use? + // what is the connection string (e.g. location, pass, etc...)? + + let pool = PgPoolOptions::new() + .max_connections(5) + .connect("postgres://secduser:p4ssw0rd@localhost:5419/secd") + .await?; + + sqlx::migrate!("store/sql/migrations").run(&pool).await?; + + // there are a few routes + // the service itself just provides some local functions which may be wrapped in a server. + // if you want to use the server, then you start the java/python/rust/ruby/go/etc... server + // otherwise, you just bring in the java/python/rust/ruby/go/etc... client + // also...maybe a terraform template to launch a _minimal_ auth server + // with your choice of RDS, dynamo, bigquery, or even local sqlite... + + // obviously need to configure terraform things... + + // if using the server, then you need to configure a few things: + // oauth endpoint with response_type, client_id + + // scratch + let pg_client = Box::new(PostgresClient::new(pool)); + let authn = Authn { store: pg_client }; + + ////////////////////////////////////////////////// + // CREATE NEW IDENTITY // which would be saved by the client + let identity = authn.register_identity().await?; + + ////////////////////////////////////////////////// + // Register a new oauth provider with some secrets, redirect, ids, etc... + authn + .register_oauth_provider( + api::OauthProvider::Google, + format!("client_id_{}", "CLIENT_SECRET_123"), + format!("client_secret_{}", util::generate_random_url_safe(4)), + "https://iam.SOMESITE.com/goauth...provided by default or customized".to_string(), + ) + .await?; + + ////////////////////////////////////////////////// + // Start oauth challenge and return the appropriate location. + let loc = authn + .initiate_oauth_challenge(identity, api::OauthProvider::Google) + .await?; + + ////////////////////////////////////////////////// + // Complete oauth challenge and return a session token + // let session = authn + // .complete_oauth_challenge(identity, api::OauthProvider::Google, state, access_token, expires_at, raw); + + ////////////////////////////////////////////////// + // Start email challenge + // authn.initiate_email_challenge(identity, email_address); + + ////////////////////////////////////////////////// + // Complete email challenge + // let session = authn.complete_email_challenge(email_address, code); + + ////////////////////////////////////////////////// + // Start SMS challenge + // authn.initiate_sms_challenge(identity, phone_number); + + ////////////////////////////////////////////////// + // Complete SMS challenge + // let session = authn.complete_sms_challenge(phone_number, code); + + ////////////////////////////////////////////////// + // Validate credentials + // let session = authn.validate(username, passphrase); + + ////////////////////////////////////////////////// + // Revoke session + // authn.revoke_session(token); + + ////////////////////////////////////////////////// + // Create API key + // let pub, priv = authn.generate_api_key(identity, Some(expires_at)); + + ////////////////////////////////////////////////// + // Revoke API key + // authn.revoke_api_key(pub, priv); + + ////////////////////////////////////////////////// + // Revoke identity + // authn.revoke_identity(identity); + + println!("Oauth2.0 URL: {}", loc); + + Ok(()) +} + +// TODO: oauth flow +// TODO: email flow diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..c939b95 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,15 @@ +use log::error; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +pub fn log_err(e: sqlx::Error) -> sqlx::Error { + error!("{:?}", e); + e +} +pub fn generate_random_url_safe(n: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(n) + .map(char::from) + .collect() +} |
