aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/client/mod.rs81
-rw-r--r--src/client/sqldb.rs172
-rw-r--r--src/ipl/authn.rs1
-rw-r--r--src/ipl/authz.rs1
-rw-r--r--src/ipl/mod.rs2
-rw-r--r--src/lib.rs171
-rw-r--r--src/main.old112
-rw-r--r--src/util/mod.rs15
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()
+}