aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/iam/Cargo.toml1
-rw-r--r--crates/iam/src/api.rs43
-rw-r--r--crates/iam/src/command.rs10
-rw-r--r--crates/iam/src/main.rs114
-rw-r--r--crates/secd/Cargo.toml4
-rw-r--r--crates/secd/README.md54
-rw-r--r--crates/secd/src/client/email.rs67
-rw-r--r--crates/secd/src/client/email/mod.rs68
-rw-r--r--crates/secd/src/client/mod.rs422
-rw-r--r--crates/secd/src/client/sqldb.rs632
-rw-r--r--crates/secd/src/client/store/mod.rs190
-rw-r--r--crates/secd/src/client/store/sql_db.rs526
-rw-r--r--crates/secd/src/client/types.rs3
-rw-r--r--crates/secd/src/command/admin.rs57
-rw-r--r--crates/secd/src/command/authn.rs446
-rw-r--r--crates/secd/src/command/mod.rs58
-rw-r--r--crates/secd/src/lib.rs481
-rw-r--r--crates/secd/src/util/from.rs66
-rw-r--r--crates/secd/src/util/mod.rs189
-rw-r--r--crates/secd/store/pg/migrations/20221116062550_bootstrap.sql86
-rw-r--r--crates/secd/store/pg/migrations/20221222002434_bootstrap.sql85
-rw-r--r--crates/secd/store/pg/sql/find_address.sql8
-rw-r--r--crates/secd/store/pg/sql/find_address_validation.sql18
-rw-r--r--crates/secd/store/pg/sql/find_email_validation.sql18
-rw-r--r--crates/secd/store/pg/sql/find_identity.sql26
-rw-r--r--crates/secd/store/pg/sql/find_identity_by_code.sql11
-rw-r--r--crates/secd/store/pg/sql/find_session.sql11
-rw-r--r--crates/secd/store/pg/sql/read_email_raw_id.sql1
-rw-r--r--crates/secd/store/pg/sql/read_identity.sql0
-rw-r--r--crates/secd/store/pg/sql/read_identity_raw_id.sql2
-rw-r--r--crates/secd/store/pg/sql/read_oauth_provider.sql12
-rw-r--r--crates/secd/store/pg/sql/read_oauth_validation.sql23
-rw-r--r--crates/secd/store/pg/sql/read_session.sql8
-rw-r--r--crates/secd/store/pg/sql/read_validation_type.sql7
-rw-r--r--crates/secd/store/pg/sql/write_address.sql8
-rw-r--r--crates/secd/store/pg/sql/write_address_validation.sql27
-rw-r--r--crates/secd/store/pg/sql/write_email.sql6
-rw-r--r--crates/secd/store/pg/sql/write_email_validation.sql43
-rw-r--r--crates/secd/store/pg/sql/write_identity.sql15
-rw-r--r--crates/secd/store/pg/sql/write_oauth_provider.sql25
-rw-r--r--crates/secd/store/pg/sql/write_oauth_validation.sql45
-rw-r--r--crates/secd/store/pg/sql/write_session.sql13
-rw-r--r--crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql133
-rw-r--r--crates/secd/store/sqlite/sql/find_address.sql8
-rw-r--r--crates/secd/store/sqlite/sql/find_address_validation.sql18
-rw-r--r--crates/secd/store/sqlite/sql/find_email_validation.sql18
-rw-r--r--crates/secd/store/sqlite/sql/find_identity.sql26
-rw-r--r--crates/secd/store/sqlite/sql/find_identity_by_code.sql11
-rw-r--r--crates/secd/store/sqlite/sql/find_session.sql11
-rw-r--r--crates/secd/store/sqlite/sql/read_email_raw_id.sql1
-rw-r--r--crates/secd/store/sqlite/sql/read_identity.sql0
-rw-r--r--crates/secd/store/sqlite/sql/read_identity_raw_id.sql2
-rw-r--r--crates/secd/store/sqlite/sql/read_oauth_provider.sql12
-rw-r--r--crates/secd/store/sqlite/sql/read_oauth_validation.sql23
-rw-r--r--crates/secd/store/sqlite/sql/read_session.sql8
-rw-r--r--crates/secd/store/sqlite/sql/read_validation_type.sql7
-rw-r--r--crates/secd/store/sqlite/sql/write_address.sql8
-rw-r--r--crates/secd/store/sqlite/sql/write_address_validation.sql26
-rw-r--r--crates/secd/store/sqlite/sql/write_email.sql6
-rw-r--r--crates/secd/store/sqlite/sql/write_email_validation.sql43
-rw-r--r--crates/secd/store/sqlite/sql/write_identity.sql15
-rw-r--r--crates/secd/store/sqlite/sql/write_oauth_provider.sql23
-rw-r--r--crates/secd/store/sqlite/sql/write_oauth_validation.sql45
-rw-r--r--crates/secd/store/sqlite/sql/write_session.sql13
-rw-r--r--crates/secd/tests/authn_integration.rs35
65 files changed, 1782 insertions, 2639 deletions
diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml
index ba642c3..966f341 100644
--- a/crates/iam/Cargo.toml
+++ b/crates/iam/Cargo.toml
@@ -8,6 +8,7 @@ anyhow = "1.0"
async-std = { version = "1.12.0", features = [ "attributes" ] }
clap = { version = "4.0.29", features = ["derive"] }
colored = "2.0.0"
+email_address = "0.2"
env_logger = "0.9"
home = "0.5.4"
log = "0.4"
diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs
index 841aa9e..ace3199 100644
--- a/crates/iam/src/api.rs
+++ b/crates/iam/src/api.rs
@@ -1,7 +1,7 @@
use crate::ISSUE_TRACKER_LOC;
use clap::{Parser, Subcommand, ValueEnum};
use colored::*;
-use secd::{IdentityId, OauthProviderName};
+use secd::IdentityId;
use serde::{Deserialize, Serialize};
use thiserror;
use url::Url;
@@ -183,7 +183,6 @@ pub enum AdminObject {
},
/// A selected Oauth2.0 provider capable of authenticating identities
Oauth2Provider {
- provider: OauthProviderName,
client_id: String,
secret: String,
redirect_url: Url,
@@ -310,7 +309,7 @@ pub enum CreateObject {
secret_code: String,
},
#[command(
- about = "An action which initiates an identity validation",
+ about = "An action which initiates an address validation",
long_about = "Validation\n\nA validation requires that the identity authenticate in some way, either by providing IAM managed credentials, an external gated mechanism (e.g. email, phone, or hardware key), or through a secondary authentication provider (oauth, saml, ldap, kerberos)."
)]
Validation {
@@ -319,7 +318,21 @@ pub enum CreateObject {
method: ValidationMethod,
/// The identity against which to associate this validation. A new identity will be created if no identity is provided.
#[arg(long, short)]
- identity: Option<Uuid>,
+ identity_id: Option<Uuid>,
+ },
+ #[command(
+ about = "An action which completes an address validation",
+ long_about = "Validation Completion\n\nA validation completion depends on an existing address validation, which is validated based on the provided validation id and secret token or secret code"
+ )]
+ ValidationCompletion {
+ /// The validation id against which to complete the validation.
+ validation_id: Uuid,
+ /// The secret token for the validation. A token or code must be provided.
+ #[arg(long, short)]
+ token: Option<String>,
+ /// The secret code for the validation. A code or token must be provided.
+ #[arg(long, short)]
+ code: Option<String>,
},
}
@@ -343,26 +356,12 @@ pub enum ValidationMethod {
/// Email address which will receive the validation
address: String,
},
- /// A hardware security key to associate with an identity
- HardwareKey,
- /// A kerberos ticket to associated with an identity
- Kerberos,
- /// An oauth2 provider to authenticate (and authorize) an identity
- Oauth2 {
- provider: OauthProviderName,
- /// An optional scope to use for authorization
- scope: Option<String>,
- /// An optional existing identity to link to this validation request
- identity: Option<IdentityId>,
- },
- /// A phone which an identity may authenticate via SMS or voice
+ /// A phone which an identity may authenticate via SMS or Voice
Phone {
/// Whether to use a voice code. Otherwise, uses SMS
#[arg(long, short, action)]
use_voice: bool,
},
- /// A saml provider to authenticate an identity
- Saml,
}
#[derive(Subcommand)]
@@ -379,8 +378,8 @@ pub enum GetObject {
id: Option<Uuid>,
},
Identity {
- /// Unique identity id
- id: Uuid,
+ /// Any session corresponding to this identity.
+ session_token: String,
},
Permission {
/// Unique permission name
@@ -497,7 +496,7 @@ pub struct ConfigProfile {
pub name: String,
pub store: secd::AuthStore,
pub store_conn: String,
- pub emailer: secd::AuthEmail,
+ pub emailer: secd::AuthEmailMessenger,
pub email_template_login: Option<String>,
pub email_template_signup: Option<String>,
}
diff --git a/crates/iam/src/command.rs b/crates/iam/src/command.rs
index 379e7fb..56734b1 100644
--- a/crates/iam/src/command.rs
+++ b/crates/iam/src/command.rs
@@ -5,7 +5,7 @@ use crate::{
};
use colored::*;
use rand::distributions::{Alphanumeric, DistString};
-use secd::{AuthEmail, AuthStore};
+use secd::{AuthEmailMessenger, AuthStore};
use std::{
fs::{self, File},
io::{self, stdin, stdout, Read, Write},
@@ -48,13 +48,13 @@ pub async fn admin_init(is_interactive: bool) -> Result<()> {
let mut cfg = api::Config {
profile: vec![api::ConfigProfile {
name: "default".to_string(),
- store: AuthStore::Sqlite,
+ store: AuthStore::Sqlite { conn: "".into() },
store_conn: format!(
"sqlite://{}/{}.sql?mode=rwc",
config_dir.clone().display().to_string(),
Alphanumeric.sample_string(&mut rand::thread_rng(), 5),
),
- emailer: secd::AuthEmail::LocalStub,
+ emailer: secd::AuthEmailMessenger::Local,
email_template_login: Some(login_template.display().to_string()),
email_template_signup: Some(signup_template.display().to_string()),
}],
@@ -104,7 +104,7 @@ pub async fn admin_init(is_interactive: bool) -> Result<()> {
write!(
stdout(),
"Email provider {:?}: ",
- AuthEmail::VARIANTS
+ AuthEmailMessenger::VARIANTS
.iter()
.map(|s| s.to_lowercase())
.collect::<Vec<String>>()
@@ -112,7 +112,7 @@ pub async fn admin_init(is_interactive: bool) -> Result<()> {
stdout().flush()?;
input.clear();
stdin().read_line(&mut input)?;
- match AuthEmail::from_str(&input.trim()) {
+ match AuthEmailMessenger::from_str(&input.trim()) {
Ok(s) => {
cfg.profile[0].emailer = s;
break;
diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs
index 4f6316a..ce72072 100644
--- a/crates/iam/src/main.rs
+++ b/crates/iam/src/main.rs
@@ -10,7 +10,8 @@ use api::{
use clap::Parser;
use command::dev_oauth2_listen;
use env_logger::Env;
-use secd::{Secd, SecdError};
+use secd::{Secd, SecdError, ENV_AUTH_STORE_CONN_STRING};
+use std::str::FromStr;
use util::{error_detail, Result};
use uuid::Uuid;
@@ -49,16 +50,15 @@ async fn exec() -> Result<Option<String>> {
}
rest @ _ => {
- let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?;
- let secd = Secd::init(
- cfg.store,
- Some(&cfg.store_conn),
- cfg.emailer,
- cfg.email_template_login,
- cfg.email_template_signup,
- )
- .await
- .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?;
+ // let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?;
+ std::env::set_var(
+ ENV_AUTH_STORE_CONN_STRING,
+ "sqlite:///tmp/store.db?mode=rwc",
+ // "postgresql://secduser:p4ssw0rd@localhost:5412/secd",
+ );
+ let secd = Secd::init()
+ .await
+ .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?;
match rest {
Command::Admin { action } => admin(&secd, action).await?,
@@ -69,13 +69,13 @@ async fn exec() -> Result<Option<String>> {
"4a696b66-6231-4a2f-811c-4448a41473d2",
"Code path should be unreachable",
))),
- Command::Link { object, unlink } => link(&secd, object, unlink).await?,
+ Command::Link { object, unlink } => todo!(),
Command::Ls {
object,
name,
before,
after,
- } => list(&secd, object, name, before, after).await?,
+ } => todo!(),
Command::Repl => {
unimplemented!()
}
@@ -90,19 +90,7 @@ async fn admin(secd: &Secd, cmd: AdminAction) -> Result<Option<String>> {
println!("do backend stuff!");
None
}
- AdminAction::Create { object } => match object {
- AdminObject::Oauth2Provider {
- provider,
- client_id,
- secret,
- redirect_url,
- } => {
- secd.create_oauth_provider(&provider, client_id, secret, redirect_url)
- .await?;
- None
- }
- rest @ _ => unimplemented!(),
- },
+ AdminAction::Create { object } => todo!(),
AdminAction::Seal => {
println!("do seal");
None
@@ -148,54 +136,31 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
CreateObject::Session {
validation_id,
secret_code,
- } => {
- let session = secd
- .exchange_code_for_session(validation_id, secret_code)
- .await
- .map_err(|e| match e {
- SecdError::InvalidCode => CliError::InvalidCode,
- _ => CliError::InternalError(error_detail(
- "17e5c226-5d7d-44a2-b3b5-be3ee958c252",
- "An unknown error while exchanging a session",
- )),
- })?;
- serde_json::to_string(&session).ok()
- }
- CreateObject::Validation { method, identity } => match method {
- ValidationMethod::Email { address } => serde_json::to_string(&Validation {
- validation_id: secd.create_validation_request_email(&address).await?,
- note: Some("<secret code> sent to client".into()),
- oauth_auth_url: None,
- })
- .ok(),
+ } => todo!(),
+ CreateObject::Validation {
+ method,
+ identity_id,
+ } => match method {
+ ValidationMethod::Email { address } => {
+ let validation = secd.validate_email(&address, identity_id).await?;
- ValidationMethod::Oauth2 {
- provider,
- scope,
- identity,
- } => {
- let redirect = secd
- .create_validation_request_oauth(&provider, scope)
- .await?
- .to_string();
- let validation_id = redirect
- .split("state=")
- .collect::<Vec<&str>>()
- .last()
- .map(|i| Uuid::parse_str(i).ok())
- .flatten()
- .unwrap();
- serde_json::to_string(&Validation {
- validation_id,
- note: Some(
- "<secret code> is retrieved by completing oauth flow in the browser".into(),
- ),
- oauth_auth_url: Some(redirect),
- })
- .ok()
+ Some(serde_json::ser::to_string(&validation)?.to_string())
}
_ => unimplemented!(),
},
+ CreateObject::ValidationCompletion {
+ validation_id,
+ token,
+ code,
+ } => {
+ if token.is_none() && code.is_none() {
+ bail!("A token or code must be specified")
+ }
+ let session = secd
+ .complete_address_validation(&validation_id, token, code)
+ .await?;
+ Some(serde_json::ser::to_string(&session)?.to_string())
+ }
})
}
@@ -215,10 +180,10 @@ async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> {
println!("get object group");
None
}
- GetObject::Identity { id } => {
- println!("get object identity");
- None
+ GetObject::Identity { session_token } => {
+ Some(serde_json::ser::to_string(&secd.get_identity(&session_token).await?)?.to_string())
}
+
GetObject::Permission { name, id } => {
println!("get object permission");
None
@@ -236,8 +201,7 @@ async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> {
None
}
GetObject::Session { secret } => {
- println!("get object session");
- None
+ Some(serde_json::ser::to_string(&secd.get_session(&secret).await?)?.to_string())
}
GetObject::Validation { id } => {
println!("get object validation");
diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml
index 069e41e..350cfd1 100644
--- a/crates/secd/Cargo.toml
+++ b/crates/secd/Cargo.toml
@@ -10,13 +10,17 @@ base64 = "0.13.1"
clap = { version = "4.0.29", features = ["derive"] }
derive_more = "0.99"
email_address = "0.2"
+hex = "0.4"
lazy_static = "1.4"
+lettre = "0.10.1"
log = "0.4"
openssl = "0.10.42"
rand = "0.8"
reqwest = { version = "0.11.13", features = ["json"] }
serde = "1"
serde_json = { version = "1.0", features = ["raw_value"] }
+serde_with = { version = "2.1", features = ["hex"] }
+sha2 = "0.10.6"
strum = "0.24.1"
strum_macros = "0.24"
sqlx = { path = "../../../sqlx", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] }
diff --git a/crates/secd/README.md b/crates/secd/README.md
new file mode 100644
index 0000000..5786d0c
--- /dev/null
+++ b/crates/secd/README.md
@@ -0,0 +1,54 @@
+// maybe motif instead of thread?
+
+// Email Address validation example
+thread = start_thread(EmailAddressValidation, "b@g.com");
+thread = advance_thread(AddressValidation, token, code);
+session = complete_thread(thread.id);
+
+// Sms validation example
+thread = start_thread(SmsAddressValidation, "12133447460");
+thread = advance_thread(SmsAddressValidation, token, code);
+
+// New passphrase
+credential = create_credential(Passphrase, "b@g.com", "p4ssw0rd");
+thread = start_thread(Passphrase, "b@g.com", "p4ssw0rd");
+session = complete_thread(thread.id);
+
+// New Totp
+credential = create_credential(Totp);
+thread = start_thread(Totp, code);
+session = complete_thread(thread.id);
+
+// New OneTimeCodes
+credential = create_credential(OneTimeCode);
+thread = start_thread(OneTimeCodes, code);
+session = complete_thread(thread.id);
+
+// MFA example which requires totp after email
+thread = start_thread(Passphrase, "b@g.com", "p4ssw0rd");
+Thread { Proof: { credential: [totp] } }
+thread = advance_thread(Totp, code);
+session = complete_thread(thread.id);
+
+
+// REST entities
+Identity
+Credential
+Address
+Motif
+Session
+
+// example
+POST /api/auth/email-validation
+motif = start_motif(EmailAddress, "b@g.com", None)
+--> an email has been sent with this motif.id + code and stuff
+user clicks on email
+GET /api/auth/email-validation/complete?motif_id=1234
+session = complete_thread(motif_id)
+
+under the hood, it looks up the thread_id, sees that it belongs to an email validation, validates the email, creates a new identity if it's not already attached, creates a session and returns that session.
+
+GET /oidc/provider?state=123444 -- state validated by client
+POST /api/auth/oidc { data ... }
+motif = start_motif(Oidc, access_token, data)
+session = complete_motif(motif.id)
diff --git a/crates/secd/src/client/email.rs b/crates/secd/src/client/email.rs
deleted file mode 100644
index 2712037..0000000
--- a/crates/secd/src/client/email.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-use std::{path::PathBuf, str::FromStr};
-
-use email_address::EmailAddress;
-use time::OffsetDateTime;
-
-use super::{
- EmailMessenger, EmailMessengerError, EmailType, EMAIL_TEMPLATE_DEFAULT_LOGIN,
- EMAIL_TEMPLATE_DEFAULT_SIGNUP,
-};
-
-pub(crate) struct LocalEmailStubber {
- pub(crate) email_template_login: Option<String>,
- pub(crate) email_template_signup: Option<String>,
-}
-
-#[async_trait::async_trait]
-impl EmailMessenger for LocalEmailStubber {
- // TODO: this module really shouldn't be called client, it should be called services... the client is sqlx/mailgun/sns wrapper or whatever...
- async fn send_email(
- &self,
- email_address: &str,
- validation_id: &str,
- secret_code: &str,
- t: EmailType,
- ) -> Result<(), EmailMessengerError> {
- let login_template = self
- .email_template_login
- .clone()
- .unwrap_or(EMAIL_TEMPLATE_DEFAULT_LOGIN.to_string());
- let signup_template = self
- .email_template_signup
- .clone()
- .unwrap_or(EMAIL_TEMPLATE_DEFAULT_SIGNUP.to_string());
-
- let replace_template = |s: &str| {
- s.replace(
- "%secd_link%",
- &format!("{}?code={}", validation_id, secret_code),
- )
- .replace("%secd_email_address%", email_address)
- .replace("%secd_code%", secret_code)
- };
-
- if !EmailAddress::is_valid(email_address) {
- return Err(EmailMessengerError::InvalidEmailAddress);
- }
-
- let body = match t {
- EmailType::Login => replace_template(&login_template),
- EmailType::Signup => replace_template(&signup_template),
- };
-
- // TODO: write to the system mailbox instead?
- std::fs::write(
- PathBuf::from_str(&format!(
- "/tmp/{}_{}.localmail",
- OffsetDateTime::now_utc(),
- validation_id
- ))
- .map_err(|_| EmailMessengerError::Unknown)?,
- body,
- )
- .map_err(|_| EmailMessengerError::FailedToSendEmail)?;
-
- Ok(())
- }
-}
diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs
new file mode 100644
index 0000000..915d18c
--- /dev/null
+++ b/crates/secd/src/client/email/mod.rs
@@ -0,0 +1,68 @@
+use email_address::EmailAddress;
+use lettre::Transport;
+use log::error;
+use std::collections::HashMap;
+
+#[derive(Debug, thiserror::Error, derive_more::Display)]
+pub enum EmailMessengerError {
+ FailedToSendEmail,
+}
+
+pub struct EmailValidationMessage {
+ pub recipient: EmailAddress,
+ pub subject: String,
+ pub body: String,
+}
+
+#[async_trait::async_trait]
+pub(crate) trait EmailMessenger {
+ async fn send_email(
+ &self,
+ email_address: &EmailAddress,
+ template: &str,
+ template_vars: HashMap<&str, &str>,
+ ) -> Result<(), EmailMessengerError>;
+}
+
+pub(crate) struct LocalMailer {}
+
+#[async_trait::async_trait]
+impl EmailMessenger for LocalMailer {
+ async fn send_email(
+ &self,
+ email_address: &EmailAddress,
+ template: &str,
+ template_vars: HashMap<&str, &str>,
+ ) -> Result<(), EmailMessengerError> {
+ todo!()
+ }
+}
+
+#[async_trait::async_trait]
+pub(crate) trait Sendable {
+ async fn send(&self) -> Result<(), EmailMessengerError>;
+}
+
+#[async_trait::async_trait]
+impl Sendable for EmailValidationMessage {
+ // TODO: We need to break this up as before, especially so we can feature
+ // gate unwanted things like Lettre...
+ async fn send(&self) -> Result<(), EmailMessengerError> {
+ // TODO: Get these things from the template...
+ let email = lettre::Message::builder()
+ .from("BranchControl <iam@branchcontrol.com>".parse().unwrap())
+ .reply_to("BranchControl <iam@branchcontrol.com>".parse().unwrap())
+ .to(self.recipient.to_string().parse().unwrap())
+ .subject(self.subject.clone())
+ .body(self.body.clone())
+ .unwrap();
+
+ let mailer = lettre::SmtpTransport::unencrypted_localhost();
+
+ mailer.send(&email).map_err(|e| {
+ error!("failed to send email {:?}", e);
+ EmailMessengerError::FailedToSendEmail
+ })?;
+ Ok(())
+ }
+}
diff --git a/crates/secd/src/client/mod.rs b/crates/secd/src/client/mod.rs
index 38426ef..e5272fd 100644
--- a/crates/secd/src/client/mod.rs
+++ b/crates/secd/src/client/mod.rs
@@ -1,422 +1,2 @@
pub(crate) mod email;
-pub(crate) mod sqldb;
-pub(crate) mod types;
-
-use std::{collections::HashMap, str::FromStr};
-
-use super::Identity;
-use crate::{
- EmailValidation, OauthProvider, OauthProviderName, OauthResponseType, OauthValidation, Session,
- SessionSecret, ValidationRequestId, ValidationType,
-};
-
-use email_address::EmailAddress;
-use lazy_static::lazy_static;
-use sqlx::{
- database::HasValueRef, sqlite::SqliteRow, ColumnIndex, Database, Decode, FromRow, Row, Sqlite,
- Type,
-};
-use thiserror::Error;
-use time::OffsetDateTime;
-use url::Url;
-use uuid::Uuid;
-
-pub enum EmailType {
- Login,
- Signup,
-}
-
-#[derive(Error, Debug, derive_more::Display)]
-pub enum EmailMessengerError {
- InvalidEmailAddress,
- FailedToSendEmail,
- Unknown,
-}
-
-#[async_trait::async_trait]
-pub trait EmailMessenger {
- async fn send_email(
- &self,
- email_address: &str,
- validation_id: &str,
- secret_code: &str,
- t: EmailType,
- ) -> Result<(), EmailMessengerError>;
-}
-
-#[derive(Error, Debug, derive_more::Display)]
-pub enum StoreError {
- SqlxError(#[from] sqlx::Error),
- CodeAppearsMoreThanOnce,
- CodeDoesNotExist(String),
- IdentityIdMustExistInvariant,
- TooManyValidations,
- TooManyIdentitiesFound,
- NoEmailValidationFound,
- OauthProviderDoesNotExist(OauthProviderName),
- OauthValidationDoesNotExist(ValidationRequestId),
- Other(String),
-}
-
-const EMAIL_TEMPLATE_DEFAULT_LOGIN: &str = "You requested a login link. Please click the following link %secd_code% to login as %secd_email_address%";
-const EMAIL_TEMPLATE_DEFAULT_SIGNUP: &str = "You requested a sign up. Please click the following link %secd_code% to complete your sign up and validate %secd_email_address%";
-
-const ERR_MSG_MIGRATION_FAILED: &str = "Failed to execute migrations. This appears to be a secd issue. File a bug at https://www.github.com/secd-lib";
-
-const SQLITE: &str = "sqlite";
-const PGSQL: &str = "pgsql";
-
-const WRITE_IDENTITY: &str = "write_identity";
-const WRITE_EMAIL_VALIDATION: &str = "write_email_validation";
-const FIND_EMAIL_VALIDATION: &str = "find_email_validation";
-const READ_VALIDATION_TYPE: &str = "read_validation_type";
-
-const WRITE_EMAIL: &str = "write_email";
-
-const READ_IDENTITY: &str = "read_identity";
-const FIND_IDENTITY: &str = "find_identity";
-const FIND_IDENTITY_BY_CODE: &str = "find_identity_by_code";
-
-const READ_IDENTITY_RAW_ID: &str = "read_identity_raw_id";
-const READ_EMAIL_RAW_ID: &str = "read_email_raw_id";
-
-const WRITE_SESSION: &str = "write_session";
-const READ_SESSION: &str = "read_session";
-
-const WRITE_OAUTH_PROVIDER: &str = "write_oauth_provider";
-const READ_OAUTH_PROVIDER: &str = "read_oauth_provider";
-const WRITE_OAUTH_VALIDATION: &str = "write_oauth_validation";
-const READ_OAUTH_VALIDATION: &str = "read_oauth_validation";
-
-lazy_static! {
- static ref SQLS: HashMap<&'static str, HashMap<&'static str, &'static str>> = {
- let sqlite_sqls: HashMap<&'static str, &'static str> = [
- (
- WRITE_IDENTITY,
- include_str!("../../store/sqlite/sql/write_identity.sql"),
- ),
- (
- WRITE_EMAIL_VALIDATION,
- include_str!("../../store/sqlite/sql/write_email_validation.sql"),
- ),
- (
- WRITE_EMAIL,
- include_str!("../../store/sqlite/sql/write_email.sql"),
- ),
- (
- READ_IDENTITY,
- include_str!("../../store/sqlite/sql/read_identity.sql"),
- ),
- (
- FIND_IDENTITY,
- include_str!("../../store/sqlite/sql/find_identity.sql"),
- ),
- (
- FIND_IDENTITY_BY_CODE,
- include_str!("../../store/sqlite/sql/find_identity_by_code.sql"),
- ),
- (
- READ_IDENTITY_RAW_ID,
- include_str!("../../store/sqlite/sql/read_identity_raw_id.sql"),
- ),
- (
- READ_EMAIL_RAW_ID,
- include_str!("../../store/sqlite/sql/read_email_raw_id.sql"),
- ),
- (
- WRITE_SESSION,
- include_str!("../../store/sqlite/sql/write_session.sql"),
- ),
- (
- READ_SESSION,
- include_str!("../../store/sqlite/sql/read_session.sql"),
- ),
- (
- FIND_EMAIL_VALIDATION,
- include_str!("../../store/sqlite/sql/find_email_validation.sql"),
- ),
- (
- WRITE_OAUTH_PROVIDER,
- include_str!("../../store/sqlite/sql/write_oauth_provider.sql"),
- ),
- (
- READ_OAUTH_PROVIDER,
- include_str!("../../store/sqlite/sql/read_oauth_provider.sql"),
- ),
- (
- READ_OAUTH_VALIDATION,
- include_str!("../../store/sqlite/sql/read_oauth_validation.sql"),
- ),
- (
- WRITE_OAUTH_VALIDATION,
- include_str!("../../store/sqlite/sql/write_oauth_validation.sql"),
- ),
- (
- READ_VALIDATION_TYPE,
- include_str!("../../store/sqlite/sql/read_validation_type.sql"),
- ),
- ]
- .iter()
- .cloned()
- .collect();
-
- let pg_sqls: HashMap<&'static str, &'static str> = [
- (
- WRITE_IDENTITY,
- include_str!("../../store/pg/sql/write_identity.sql"),
- ),
- (
- WRITE_EMAIL_VALIDATION,
- include_str!("../../store/pg/sql/write_email_validation.sql"),
- ),
- (
- WRITE_EMAIL,
- include_str!("../../store/pg/sql/write_email.sql"),
- ),
- (
- READ_IDENTITY,
- include_str!("../../store/pg/sql/read_identity.sql"),
- ),
- (
- FIND_IDENTITY,
- include_str!("../../store/pg/sql/find_identity.sql"),
- ),
- (
- FIND_IDENTITY_BY_CODE,
- include_str!("../../store/pg/sql/find_identity_by_code.sql"),
- ),
- (
- READ_IDENTITY_RAW_ID,
- include_str!("../../store/pg/sql/read_identity_raw_id.sql"),
- ),
- (
- READ_EMAIL_RAW_ID,
- include_str!("../../store/pg/sql/read_email_raw_id.sql"),
- ),
- (
- WRITE_SESSION,
- include_str!("../../store/pg/sql/write_session.sql"),
- ),
- (
- READ_SESSION,
- include_str!("../../store/pg/sql/read_session.sql"),
- ),
- (
- FIND_EMAIL_VALIDATION,
- include_str!("../../store/pg/sql/find_email_validation.sql"),
- ),
- (
- WRITE_OAUTH_PROVIDER,
- include_str!("../../store/pg/sql/write_oauth_provider.sql"),
- ),
- (
- READ_OAUTH_PROVIDER,
- include_str!("../../store/pg/sql/read_oauth_provider.sql"),
- ),
- (
- READ_OAUTH_VALIDATION,
- include_str!("../../store/pg/sql/read_oauth_validation.sql"),
- ),
- (
- WRITE_OAUTH_VALIDATION,
- include_str!("../../store/pg/sql/write_oauth_validation.sql"),
- ),
- (
- READ_VALIDATION_TYPE,
- include_str!("../../store/pg/sql/read_validation_type.sql"),
- ),
- ]
- .iter()
- .cloned()
- .collect();
-
- let sqls: HashMap<&'static str, HashMap<&'static str, &'static str>> =
- [(SQLITE, sqlite_sqls), (PGSQL, pg_sqls)]
- .iter()
- .cloned()
- .collect();
- sqls
- };
-}
-
-impl<'a, R: Row> FromRow<'a, R> for OauthValidation
-where
- &'a str: ColumnIndex<R>,
- OauthProviderName: Decode<'a, R::Database> + Type<R::Database>,
- OauthResponseType: Decode<'a, R::Database> + Type<R::Database>,
- OffsetDateTime: Decode<'a, R::Database> + Type<R::Database>,
- String: Decode<'a, R::Database> + Type<R::Database>,
- Uuid: Decode<'a, R::Database> + Type<R::Database>,
-{
- fn from_row(row: &'a R) -> Result<Self, sqlx::Error> {
- let id: Option<Uuid> = row.try_get("oauth_validation_public_id")?;
- let identity_id: Option<Uuid> = row.try_get("identity_public_id")?;
- let access_token: Option<String> = row.try_get("access_token")?;
- let raw_response: Option<String> = row.try_get("raw_response")?;
- let created_at: Option<OffsetDateTime> = row.try_get("created_at")?;
- let validated_at: Option<OffsetDateTime> = row.try_get("validated_at")?;
- let revoked_at: Option<OffsetDateTime> = row.try_get("revoked_at")?;
- let deleted_at: Option<OffsetDateTime> = row.try_get("deleted_at")?;
-
- let op_name: Option<OauthProviderName> = row.try_get("oauth_provider_name")?;
- let op_flow: Option<String> = row.try_get("oauth_provider_flow")?;
- let op_base_url: Option<String> = row.try_get("oauth_provider_base_url")?;
- let op_response_type: Option<OauthResponseType> =
- row.try_get("oauth_provider_response_type")?;
- let op_default_scope: Option<String> = row.try_get("oauth_provider_default_scope")?;
- let op_client_id: Option<String> = row.try_get("oauth_provider_client_id")?;
- let op_client_secret: Option<String> = row.try_get("oauth_provider_client_secret")?;
- let op_redirect_url: Option<String> = row.try_get("oauth_provider_redirect_url")?;
- let op_created_at: Option<OffsetDateTime> = row.try_get("oauth_provider_created_at")?;
- let op_deleted_at: Option<OffsetDateTime> = row.try_get("oauth_provider_deleted_at")?;
-
- let op_base_url = op_base_url
- .map(|s| Url::from_str(&s).ok())
- .flatten()
- .ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_base_url".into(),
- source: "secd".into(),
- })?;
-
- let op_redirect_url = op_redirect_url
- .map(|s| Url::from_str(&s).ok())
- .flatten()
- .ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_redirect_url".into(),
- source: "secd".into(),
- })?;
-
- Ok(OauthValidation {
- id,
- identity_id,
- access_token,
- raw_response,
- created_at: created_at.ok_or(sqlx::Error::ColumnDecode {
- index: "created_at".into(),
- source: "secd".into(),
- })?,
- validated_at,
- revoked_at,
- deleted_at,
- oauth_provider: OauthProvider {
- name: op_name.unwrap(),
- flow: op_flow,
- base_url: op_base_url,
- response: op_response_type.ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_response_type".into(),
- source: "secd".into(),
- })?,
- default_scope: op_default_scope.ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_default_scope".into(),
- source: "secd".into(),
- })?,
- client_id: op_client_id.ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_client_id".into(),
- source: "secd".into(),
- })?,
- client_secret: op_client_secret.ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_client_secret".into(),
- source: "secd".into(),
- })?,
- redirect_url: op_redirect_url,
- created_at: op_created_at.ok_or(sqlx::Error::ColumnDecode {
- index: "oauth_provider_created_at".into(),
- source: "secd".into(),
- })?,
- deleted_at: op_deleted_at,
- },
- })
- }
-}
-
-impl<'a, D: Database> Decode<'a, D> for OauthProviderName
-where
- &'a str: Decode<'a, D>,
-{
- fn decode(
- value: <D as HasValueRef<'a>>::ValueRef,
- ) -> Result<Self, Box<dyn ::std::error::Error + 'static + Send + Sync>> {
- let v = <&str as Decode<D>>::decode(value)?;
- <OauthProviderName as clap::ValueEnum>::from_str(v, true)
- .map_err(|_| "OauthProviderName should exist and decode to a program value.".into())
- }
-}
-
-impl<D: Database> Type<D> for OauthProviderName
-where
- str: Type<D>,
-{
- fn type_info() -> D::TypeInfo {
- <&str as Type<D>>::type_info()
- }
-}
-
-impl<'a, D: Database> Decode<'a, D> for OauthResponseType
-where
- &'a str: Decode<'a, D>,
-{
- fn decode(
- value: <D as HasValueRef<'a>>::ValueRef,
- ) -> Result<Self, Box<dyn ::std::error::Error + 'static + Send + Sync>> {
- let v = <&str as Decode<D>>::decode(value)?;
- <OauthResponseType as clap::ValueEnum>::from_str(v, true)
- .map_err(|_| "OauthResponseType should exist and decode to a program value.".into())
- }
-}
-
-impl<D: Database> Type<D> for OauthResponseType
-where
- str: Type<D>,
-{
- fn type_info() -> D::TypeInfo {
- <&str as Type<D>>::type_info()
- }
-}
-
-#[async_trait::async_trait]
-pub trait Store {
- async fn write_email(&self, email_address: &str) -> Result<(), StoreError>;
-
- async fn find_email_validation(
- &self,
- validation_id: Option<&Uuid>,
- code: Option<&str>,
- ) -> Result<EmailValidation, StoreError>;
- async fn write_email_validation(
- &self,
- ev: &EmailValidation,
- // TODO: Make this write an EmailValidation
- ) -> anyhow::Result<Uuid>;
-
- async fn find_identity(
- &self,
- identity_id: Option<&Uuid>,
- email: Option<&str>,
- ) -> anyhow::Result<Option<Identity>>;
- async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError>;
- async fn write_identity(&self, i: &Identity) -> Result<(), StoreError>;
- async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError>;
-
- async fn write_session(&self, session: &Session) -> Result<(), StoreError>;
- async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError>;
-
- async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError>;
- async fn read_oauth_provider(
- &self,
- provider: &OauthProviderName,
- flow: Option<String>,
- ) -> Result<OauthProvider, StoreError>;
- async fn write_oauth_validation(
- &self,
- validation: &OauthValidation,
- ) -> anyhow::Result<ValidationRequestId>;
- async fn read_oauth_validation(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<OauthValidation>;
-
- async fn find_validation_type(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<ValidationType>;
-}
+pub(crate) mod store;
diff --git a/crates/secd/src/client/sqldb.rs b/crates/secd/src/client/sqldb.rs
deleted file mode 100644
index 6751ef6..0000000
--- a/crates/secd/src/client/sqldb.rs
+++ /dev/null
@@ -1,632 +0,0 @@
-use std::{str::FromStr, sync::Arc};
-
-use super::{
- EmailValidation, Identity, OauthProvider, OauthProviderName, OauthResponseType, Session,
- SessionSecret, Store, StoreError, ERR_MSG_MIGRATION_FAILED, FIND_EMAIL_VALIDATION,
- FIND_IDENTITY, FIND_IDENTITY_BY_CODE, PGSQL, READ_EMAIL_RAW_ID, READ_IDENTITY_RAW_ID,
- READ_OAUTH_PROVIDER, READ_OAUTH_VALIDATION, READ_SESSION, READ_VALIDATION_TYPE, SQLITE, SQLS,
- WRITE_EMAIL, WRITE_EMAIL_VALIDATION, WRITE_IDENTITY, WRITE_OAUTH_PROVIDER,
- WRITE_OAUTH_VALIDATION, WRITE_SESSION,
-};
-use crate::{util, OauthValidation, ValidationRequestId, ValidationType};
-use anyhow::bail;
-use log::{debug, error};
-use openssl::sha::Sha256;
-use sqlx::{
- self, database::HasArguments, ColumnIndex, Database, Decode, Encode, Executor, IntoArguments,
- Pool, Postgres, Sqlite, Transaction, Type,
-};
-use time::OffsetDateTime;
-use url::Url;
-use uuid::Uuid;
-
-fn get_sqls(root: &str, file: &str) -> Vec<String> {
- SQLS.get(root)
- .unwrap()
- .get(file)
- .unwrap()
- .split("--")
- .map(|p| p.to_string())
- .collect()
-}
-
-fn hash_secret(secret: &str) -> Vec<u8> {
- let mut hasher = Sha256::new();
- hasher.update(secret.as_bytes());
- hasher.finish().to_vec()
-}
-
-struct SqlClient<D>
-where
- D: sqlx::Database,
-{
- pool: sqlx::Pool<D>,
- sqls_root: String,
-}
-
-impl<D> SqlClient<D>
-where
- D: sqlx::Database,
- for<'c> <D as HasArguments<'c>>::Arguments: IntoArguments<'c, D>,
- for<'c> i64: Decode<'c, D> + Type<D>,
- for<'c> &'c str: Decode<'c, D> + Type<D>,
- for<'c> &'c str: Encode<'c, D> + Type<D>,
- for<'c> usize: ColumnIndex<<D as Database>::Row>,
- for<'c> Uuid: Decode<'c, D> + Type<D>,
- for<'c> Uuid: Encode<'c, D> + Type<D>,
- for<'c> &'c Pool<D>: Executor<'c, Database = D>,
-{
- async fn read_identity_raw_id(&self, id: &Uuid) -> Result<i64, StoreError> {
- let sqls = get_sqls(&self.sqls_root, READ_IDENTITY_RAW_ID);
-
- Ok(sqlx::query_as::<_, (i64,)>(&sqls[0])
- .bind(id)
- .fetch_one(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?
- .0)
- }
-
- async fn read_email_raw_id(&self, address: &str) -> Result<i64, StoreError> {
- let sqls = get_sqls(&self.sqls_root, READ_EMAIL_RAW_ID);
-
- Ok(sqlx::query_as::<_, (i64,)>(&sqls[0])
- .bind(address)
- .fetch_one(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?
- .0)
- }
-}
-
-#[async_trait::async_trait]
-impl<D> Store for SqlClient<D>
-where
- D: sqlx::Database,
- for<'c> <D as HasArguments<'c>>::Arguments: IntoArguments<'c, D>,
- for<'c> bool: Decode<'c, D> + Type<D>,
- for<'c> bool: Encode<'c, D> + Type<D>,
- for<'c> i64: Decode<'c, D> + Type<D>,
- for<'c> i64: Encode<'c, D> + Type<D>,
- for<'c> i32: Decode<'c, D> + Type<D>,
- for<'c> i32: Encode<'c, D> + Type<D>,
- for<'c> OffsetDateTime: Decode<'c, D> + Type<D>,
- for<'c> OffsetDateTime: Encode<'c, D> + Type<D>,
- for<'c> &'c str: ColumnIndex<<D as Database>::Row>,
- for<'c> &'c str: Decode<'c, D> + Type<D>,
- for<'c> &'c str: Encode<'c, D> + Type<D>,
- for<'c> Option<&'c str>: Decode<'c, D> + Type<D>,
- for<'c> Option<&'c str>: Encode<'c, D> + Type<D>,
- for<'c> String: Decode<'c, D> + Type<D>,
- for<'c> String: Encode<'c, D> + Type<D>,
- for<'c> Option<String>: Decode<'c, D> + Type<D>,
- for<'c> Option<String>: Encode<'c, D> + Type<D>,
- for<'c> OauthProviderName: Decode<'c, D> + Type<D>,
- for<'c> OauthResponseType: Decode<'c, D> + Type<D>,
- for<'c> usize: ColumnIndex<<D as Database>::Row>,
- for<'c> Uuid: Decode<'c, D> + Type<D>,
- for<'c> Uuid: Encode<'c, D> + Type<D>,
- for<'c> &'c [u8]: Encode<'c, D> + Type<D>,
- for<'c> Option<&'c Uuid>: Encode<'c, D> + Type<D>,
- for<'c> Option<&'c Vec<u8>>: Encode<'c, D> + Type<D>,
- for<'c> Option<OffsetDateTime>: Decode<'c, D> + Type<D>,
- for<'c> Option<OffsetDateTime>: Encode<'c, D> + Type<D>,
- for<'c> &'c Pool<D>: Executor<'c, Database = D>,
- for<'c> &'c mut Transaction<'c, D>: Executor<'c, Database = D>,
-{
- async fn write_email(&self, email_address: &str) -> Result<(), StoreError> {
- let sqls = get_sqls(&self.sqls_root, WRITE_EMAIL);
-
- sqlx::query(&sqls[0])
- .bind(email_address)
- .execute(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- Ok(())
- }
-
- async fn find_email_validation(
- &self,
- validation_id: Option<&Uuid>,
- code: Option<&str>,
- ) -> Result<EmailValidation, StoreError> {
- let sqls = get_sqls(&self.sqls_root, FIND_EMAIL_VALIDATION);
- let mut rows = sqlx::query_as::<_, EmailValidation>(&sqls[0])
- .bind(validation_id)
- .bind(code)
- .fetch_all(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- match rows.len() {
- 0 => Err(StoreError::NoEmailValidationFound),
- 1 => Ok(rows.swap_remove(0)),
- _ => Err(StoreError::TooManyValidations),
- }
- }
-
- async fn write_email_validation(&self, ev: &EmailValidation) -> anyhow::Result<Uuid> {
- let sqls = get_sqls(&self.sqls_root, WRITE_EMAIL_VALIDATION);
-
- let email_id = self.read_email_raw_id(&ev.email_address).await?;
- let validation_id = ev.id.unwrap_or(Uuid::new_v4());
- sqlx::query(&sqls[0])
- .bind(validation_id)
- .bind(email_id)
- .bind(&ev.code)
- .bind(ev.is_oauth_derived)
- .bind(ev.created_at)
- .bind(ev.validated_at)
- .bind(ev.expired_at)
- .execute(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- if ev.identity_id.is_some() || ev.revoked_at.is_some() || ev.deleted_at.is_some() {
- sqlx::query(&sqls[1])
- .bind(ev.identity_id.as_ref())
- .bind(validation_id)
- .bind(ev.revoked_at)
- .bind(ev.deleted_at)
- .execute(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
- }
-
- Ok(validation_id)
- }
-
- async fn find_identity(
- &self,
- id: Option<&Uuid>,
- email: Option<&str>,
- ) -> anyhow::Result<Option<Identity>> {
- let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY);
- Ok(
- match sqlx::query_as::<_, Identity>(&sqls[0])
- .bind(id)
- .bind(email)
- .fetch_all(&self.pool)
- .await
- {
- Ok(mut is) => match is.len() {
- // if only 1 found, then that's fine
- // if multiple are fond, then if they all have the same id, that's okay
- 1 => {
- let i = is.swap_remove(0);
- match i.deleted_at {
- Some(t) if t > OffsetDateTime::now_utc() => Some(i),
- None => Some(i),
- _ => None,
- }
- }
- 0 => None,
- _ => {
- match is
- .iter()
- .filter(|&i| i.id != is[0].id)
- .collect::<Vec<&Identity>>()
- .len()
- {
- 0 => Some(is.swap_remove(0)),
- _ => bail!(StoreError::TooManyIdentitiesFound),
- }
- }
- },
- Err(sqlx::Error::RowNotFound) => None,
- Err(e) => bail!(StoreError::SqlxError(e)),
- },
- )
- }
-
- async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> {
- let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY_BY_CODE);
-
- let rows = sqlx::query_as::<_, (i32,)>(&sqls[0])
- .bind(code)
- .fetch_all(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- if rows.len() == 0 {
- return Err(StoreError::CodeDoesNotExist(code.to_string()));
- }
-
- if rows.len() != 1 {
- return Err(StoreError::CodeAppearsMoreThanOnce);
- }
-
- let identity_email_id = rows.get(0).unwrap().0;
-
- // TODO: IF we expand beyond email codes, then we'll need to join against a bunch of identity tables.
- // but since a single code was found, only one of them should pop...
- Ok(sqlx::query_as::<_, Identity>(&sqls[1])
- .bind(identity_email_id)
- .fetch_one(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?)
- }
-
- async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> {
- let sqls = get_sqls(&self.sqls_root, WRITE_IDENTITY);
- sqlx::query(&sqls[0])
- .bind(i.id)
- .bind(i.data.clone())
- .bind(i.created_at)
- .execute(&self.pool)
- .await
- .map_err(|e| {
- error!("write_identity_failure");
- error!("{:?}", e);
- e
- })?;
-
- Ok(())
- }
- async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError> {
- let identity = 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_sqlx)?;
-
- Ok(identity)
- }
-
- async fn write_session(&self, session: &Session) -> Result<(), StoreError> {
- let sqls = get_sqls(&self.sqls_root, WRITE_SESSION);
-
- let secret_hash = session.secret.as_ref().map(|s| hash_secret(s));
-
- sqlx::query(&sqls[0])
- .bind(&session.identity_id)
- .bind(secret_hash.as_ref())
- .bind(session.created_at)
- .bind(session.expired_at)
- .bind(session.revoked_at)
- .execute(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- Ok(())
- }
- async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> {
- let sqls = get_sqls(&self.sqls_root, READ_SESSION);
-
- let secret_hash = hash_secret(secret);
- let mut session = sqlx::query_as::<_, Session>(&sqls[0])
- .bind(&secret_hash[..])
- .fetch_one(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- // This should do nothing other than updated touched_at, and then
- // clear the plaintext secret
- session.secret = Some(secret.to_string());
- self.write_session(&session).await?;
- session.secret = None;
-
- Ok(session)
- }
-
- async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError> {
- let sqls = get_sqls(&self.sqls_root, WRITE_OAUTH_PROVIDER);
- sqlx::query(&sqls[0])
- .bind(&provider.name.to_string())
- .bind(&provider.flow)
- .bind(&provider.base_url.to_string())
- .bind(&provider.response.to_string())
- .bind(&provider.default_scope)
- .bind(&provider.client_id)
- // TODO: encrypt secret before writing
- .bind(&provider.client_secret)
- .bind(&provider.redirect_url.to_string())
- .bind(provider.created_at)
- .bind(provider.deleted_at)
- .execute(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
- Ok(())
- }
-
- async fn read_oauth_provider(
- &self,
- provider: &OauthProviderName,
- flow: Option<String>,
- ) -> Result<OauthProvider, StoreError> {
- let sqls = get_sqls(&self.sqls_root, READ_OAUTH_PROVIDER);
- let flow = flow.unwrap_or("default".into());
- debug!("provider: {:?}, flow: {:?}", provider, flow);
- // TODO: Write the generic FromRow impl for OauthProvider...
- let res = sqlx::query_as::<
- _,
- (
- String,
- String,
- String,
- String,
- String,
- String,
- String,
- OffsetDateTime,
- Option<OffsetDateTime>,
- ),
- >(&sqls[0])
- .bind(&provider.to_string())
- .bind(&flow)
- .fetch_one(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- debug!("res: {:?}", res);
-
- Ok(OauthProvider {
- name: provider.clone(),
- flow: Some(res.0),
- base_url: Url::from_str(&res.1)
- .map_err(|_| StoreError::OauthProviderDoesNotExist(*provider))?,
- response: OauthResponseType::from_str(&res.2)
- .map_err(|_| StoreError::OauthProviderDoesNotExist(*provider))?,
- default_scope: res.3,
- client_id: res.4,
- client_secret: res.5,
- redirect_url: Url::from_str(&res.6)
- .map_err(|_| StoreError::OauthProviderDoesNotExist(*provider))?,
- created_at: res.7,
- deleted_at: res.8,
- })
- }
- async fn write_oauth_validation(
- &self,
- v: &OauthValidation,
- ) -> anyhow::Result<ValidationRequestId> {
- let sqls = get_sqls(&self.sqls_root, WRITE_OAUTH_VALIDATION);
-
- let validation_id = v.id.unwrap_or(Uuid::new_v4());
- sqlx::query(&sqls[0])
- .bind(validation_id)
- .bind(v.oauth_provider.name.to_string())
- .bind(v.oauth_provider.flow.clone())
- .bind(v.access_token.clone())
- .bind(v.raw_response.clone())
- .bind(v.created_at)
- .bind(v.validated_at)
- .execute(&self.pool)
- .await?;
-
- if v.identity_id.is_some() || v.revoked_at.is_some() || v.deleted_at.is_some() {
- sqlx::query(&sqls[1])
- .bind(v.identity_id.as_ref())
- .bind(validation_id)
- .bind(v.revoked_at)
- .bind(v.deleted_at)
- .execute(&self.pool)
- .await?;
- }
-
- Ok(validation_id)
- }
- async fn read_oauth_validation(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<OauthValidation> {
- let sqls = get_sqls(&self.sqls_root, READ_OAUTH_VALIDATION);
-
- let mut es = sqlx::query_as::<_, OauthValidation>(&sqls[0])
- .bind(validation_id)
- .fetch_all(&self.pool)
- .await?;
-
- if es.len() != 1 {
- bail!(StoreError::OauthValidationDoesNotExist(
- validation_id.clone()
- ));
- }
-
- Ok(es.swap_remove(0))
- }
- async fn find_validation_type(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<ValidationType> {
- let sqls = get_sqls(&self.sqls_root, READ_VALIDATION_TYPE);
-
- let mut es = sqlx::query_as::<_, (String,)>(&sqls[0])
- .bind(validation_id)
- .fetch_all(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?;
-
- match es.len() {
- 1 => Ok(ValidationType::from_str(&es.swap_remove(0).0)?),
- _ => bail!(StoreError::Other(
- "expected a single validation but recieved 0 or multiple validations".into()
- )),
- }
- }
-}
-
-pub struct PgClient {
- sql: SqlClient<Postgres>,
-}
-
-impl PgClient {
- pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/pg/migrations")
- .run(&pool)
- .await
- .expect(ERR_MSG_MIGRATION_FAILED);
-
- Arc::new(PgClient {
- sql: SqlClient {
- pool,
- sqls_root: PGSQL.to_string(),
- },
- })
- }
-}
-
-#[async_trait::async_trait]
-impl Store for PgClient {
- async fn write_email(&self, email_address: &str) -> Result<(), StoreError> {
- self.sql.write_email(email_address).await
- }
- async fn find_email_validation(
- &self,
- validation_id: Option<&Uuid>,
- code: Option<&str>,
- ) -> Result<EmailValidation, StoreError> {
- self.sql.find_email_validation(validation_id, code).await
- }
- async fn write_email_validation(&self, ev: &EmailValidation) -> anyhow::Result<Uuid> {
- self.sql.write_email_validation(ev).await
- }
- async fn find_identity(
- &self,
- identity_id: Option<&Uuid>,
- email: Option<&str>,
- ) -> anyhow::Result<Option<Identity>> {
- self.sql.find_identity(identity_id, email).await
- }
- async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> {
- self.sql.find_identity_by_code(code).await
- }
- async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> {
- self.sql.write_identity(i).await
- }
- async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError> {
- self.sql.read_identity(identity_id).await
- }
- async fn write_session(&self, session: &Session) -> Result<(), StoreError> {
- self.sql.write_session(session).await
- }
- async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> {
- self.sql.read_session(secret).await
- }
- async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError> {
- self.sql.write_oauth_provider(provider).await
- }
- async fn read_oauth_provider(
- &self,
- provider: &OauthProviderName,
- flow: Option<String>,
- ) -> Result<OauthProvider, StoreError> {
- self.sql.read_oauth_provider(provider, flow).await
- }
- async fn write_oauth_validation(
- &self,
- validation: &OauthValidation,
- ) -> anyhow::Result<ValidationRequestId> {
- self.sql.write_oauth_validation(validation).await
- }
- async fn read_oauth_validation(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<OauthValidation> {
- self.sql.read_oauth_validation(validation_id).await
- }
- async fn find_validation_type(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<ValidationType> {
- self.sql.find_validation_type(validation_id).await
- }
-}
-
-pub struct SqliteClient {
- sql: SqlClient<Sqlite>,
-}
-
-impl SqliteClient {
- pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/sqlite/migrations")
- .run(&pool)
- .await
- .expect(ERR_MSG_MIGRATION_FAILED);
-
- 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",
- );
-
- Arc::new(SqliteClient {
- sql: SqlClient {
- pool,
- sqls_root: SQLITE.to_string(),
- },
- })
- }
-}
-
-#[async_trait::async_trait]
-impl Store for SqliteClient {
- async fn write_email(&self, email_address: &str) -> Result<(), StoreError> {
- self.sql.write_email(email_address).await
- }
- async fn find_email_validation(
- &self,
- validation_id: Option<&Uuid>,
- code: Option<&str>,
- ) -> Result<EmailValidation, StoreError> {
- self.sql.find_email_validation(validation_id, code).await
- }
- async fn write_email_validation(&self, ev: &EmailValidation) -> anyhow::Result<Uuid> {
- self.sql.write_email_validation(ev).await
- }
- async fn find_identity(
- &self,
- identity_id: Option<&Uuid>,
- email: Option<&str>,
- ) -> anyhow::Result<Option<Identity>> {
- self.sql.find_identity(identity_id, email).await
- }
- async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> {
- self.sql.find_identity_by_code(code).await
- }
- async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> {
- self.sql.write_identity(i).await
- }
- async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError> {
- self.sql.read_identity(identity_id).await
- }
- async fn write_session(&self, session: &Session) -> Result<(), StoreError> {
- self.sql.write_session(session).await
- }
- async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> {
- self.sql.read_session(secret).await
- }
- async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError> {
- self.sql.write_oauth_provider(provider).await
- }
- async fn read_oauth_provider(
- &self,
- provider: &OauthProviderName,
- flow: Option<String>,
- ) -> Result<OauthProvider, StoreError> {
- self.sql.read_oauth_provider(provider, flow).await
- }
- async fn write_oauth_validation(
- &self,
- validation: &OauthValidation,
- ) -> anyhow::Result<ValidationRequestId> {
- self.sql.write_oauth_validation(validation).await
- }
- async fn read_oauth_validation(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<OauthValidation> {
- self.sql.read_oauth_validation(validation_id).await
- }
- async fn find_validation_type(
- &self,
- validation_id: &ValidationRequestId,
- ) -> anyhow::Result<ValidationType> {
- self.sql.find_validation_type(validation_id).await
- }
-}
diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs
new file mode 100644
index 0000000..b93fd84
--- /dev/null
+++ b/crates/secd/src/client/store/mod.rs
@@ -0,0 +1,190 @@
+pub(crate) mod sql_db;
+
+use email_address::EmailAddress;
+use sqlx::{Postgres, Sqlite};
+use std::sync::Arc;
+use time::OffsetDateTime;
+use uuid::Uuid;
+
+use crate::{
+ util, Address, AddressType, AddressValidation, Identity, IdentityId, PhoneNumber, Session,
+ SessionToken,
+};
+
+use self::sql_db::SqlClient;
+
+#[derive(Debug, thiserror::Error, derive_more::Display)]
+pub enum StoreError {
+ SqlClientError(#[from] sqlx::Error),
+ StoreValueCannotBeParsedInvariant,
+ IdempotentCheckAlreadyExists,
+}
+
+#[async_trait::async_trait(?Send)]
+pub trait Store {
+ fn get_type(&self) -> StoreType;
+}
+
+pub enum StoreType {
+ Postgres { c: Arc<SqlClient<Postgres>> },
+ Sqlite { c: Arc<SqlClient<Sqlite>> },
+}
+
+#[async_trait::async_trait(?Send)]
+pub(crate) trait Storable<'a> {
+ type Item;
+ type Lens;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError>;
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError>;
+}
+
+pub(crate) trait Lens {}
+
+pub(crate) struct AddressLens<'a> {
+ pub id: Option<&'a Uuid>,
+ pub t: Option<&'a AddressType>,
+}
+impl<'a> Lens for AddressLens<'a> {}
+
+pub(crate) struct AddressValidationLens<'a> {
+ pub id: Option<&'a Uuid>,
+}
+impl<'a> Lens for AddressValidationLens<'a> {}
+
+pub(crate) struct IdentityLens<'a> {
+ pub id: Option<&'a Uuid>,
+ pub address_type: Option<&'a AddressType>,
+ pub validated_address: Option<bool>,
+ pub session_token_hash: Option<Vec<u8>>,
+}
+impl<'a> Lens for IdentityLens<'a> {}
+
+pub(crate) struct SessionLens<'a> {
+ pub token_hash: Option<&'a Vec<u8>>,
+ pub identity_id: Option<&'a IdentityId>,
+}
+impl<'a> Lens for SessionLens<'a> {}
+
+#[async_trait::async_trait(?Send)]
+impl<'a> Storable<'a> for Address {
+ type Item = Address;
+ type Lens = AddressLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ match store.get_type() {
+ StoreType::Postgres { c } => c.write_address(self).await?,
+ StoreType::Sqlite { c } => c.write_address(self).await?,
+ }
+ Ok(())
+ }
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError> {
+ let typ = lens.t.map(|at| at.to_string().clone());
+ let typ = typ.as_deref();
+
+ let val = lens.t.and_then(|at| at.get_value());
+ let val = val.as_deref();
+
+ Ok(match store.get_type() {
+ StoreType::Postgres { c } => c.find_address(lens.id, typ, val).await?,
+ StoreType::Sqlite { c } => c.find_address(lens.id, typ, val).await?,
+ })
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl<'a> Storable<'a> for AddressValidation {
+ type Item = AddressValidation;
+ type Lens = AddressValidationLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ match store.get_type() {
+ StoreType::Sqlite { c } => c.write_address_validation(self).await?,
+ StoreType::Postgres { c } => c.write_address_validation(self).await?,
+ }
+ Ok(())
+ }
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError> {
+ Ok(match store.get_type() {
+ StoreType::Postgres { c } => c.find_address_validation(lens.id).await?,
+ StoreType::Sqlite { c } => c.find_address_validation(lens.id).await?,
+ })
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl<'a> Storable<'a> for Identity {
+ type Item = Identity;
+ type Lens = IdentityLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ match store.get_type() {
+ StoreType::Postgres { c } => c.write_identity(self).await?,
+ StoreType::Sqlite { c } => c.write_identity(self).await?,
+ }
+ Ok(())
+ }
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError> {
+ let val = lens.address_type.and_then(|at| at.get_value());
+ let val = val.as_deref();
+
+ Ok(match store.get_type() {
+ StoreType::Postgres { c } => {
+ c.find_identity(
+ lens.id,
+ val,
+ lens.validated_address,
+ &lens.session_token_hash,
+ )
+ .await?
+ }
+ StoreType::Sqlite { c } => {
+ c.find_identity(
+ lens.id,
+ val,
+ lens.validated_address,
+ &lens.session_token_hash,
+ )
+ .await?
+ }
+ })
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl<'a> Storable<'a> for Session {
+ type Item = Session;
+ type Lens = SessionLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ let token_hash = util::hash(&self.token);
+ match store.get_type() {
+ StoreType::Postgres { c } => c.write_session(self, &token_hash).await?,
+ StoreType::Sqlite { c } => c.write_session(self, &token_hash).await?,
+ }
+ Ok(())
+ }
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError> {
+ let token = lens.token_hash.map(|t| t.clone()).unwrap_or(vec![]);
+
+ Ok(match store.get_type() {
+ StoreType::Postgres { c } => c.find_session(token, lens.identity_id).await?,
+ StoreType::Sqlite { c } => c.find_session(token, lens.identity_id).await?,
+ })
+ }
+}
diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs
new file mode 100644
index 0000000..6d84301
--- /dev/null
+++ b/crates/secd/src/client/store/sql_db.rs
@@ -0,0 +1,526 @@
+use std::{str::FromStr, sync::Arc};
+
+use email_address::EmailAddress;
+use serde_json::value::RawValue;
+use sqlx::{
+ database::HasArguments, types::Json, ColumnIndex, Database, Decode, Encode, Executor,
+ IntoArguments, Pool, Transaction, Type,
+};
+use time::OffsetDateTime;
+use uuid::Uuid;
+
+use crate::{
+ Address, AddressType, AddressValidation, AddressValidationMethod, Identity, Session,
+ SessionToken,
+};
+
+use lazy_static::lazy_static;
+use sqlx::{Postgres, Sqlite};
+use std::collections::HashMap;
+
+use super::{
+ AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, Store, StoreError,
+ StoreType,
+};
+
+const SQLITE: &str = "sqlite";
+const PGSQL: &str = "pgsql";
+
+const WRITE_ADDRESS: &str = "write_address";
+const FIND_ADDRESS: &str = "find_address";
+const WRITE_ADDRESS_VALIDATION: &str = "write_address_validation";
+const FIND_ADDRESS_VALIDATION: &str = "find_address_validation";
+const WRITE_IDENTITY: &str = "write_identity";
+const FIND_IDENTITY: &str = "find_identity";
+const WRITE_SESSION: &str = "write_session";
+const FIND_SESSION: &str = "find_session";
+
+const ERR_MSG_MIGRATION_FAILED: &str = "Failed to apply secd migrations to a sql db. File a bug at https://www.github.com/branchcontrol/secdiam";
+
+lazy_static! {
+ static ref SQLS: HashMap<&'static str, HashMap<&'static str, &'static str>> = {
+ let sqlite_sqls: HashMap<&'static str, &'static str> = [
+ (
+ WRITE_ADDRESS,
+ include_str!("../../../store/sqlite/sql/write_address.sql"),
+ ),
+ (
+ FIND_ADDRESS,
+ include_str!("../../../store/sqlite/sql/find_address.sql"),
+ ),
+ (
+ WRITE_ADDRESS_VALIDATION,
+ include_str!("../../../store/sqlite/sql/write_address_validation.sql"),
+ ),
+ (
+ FIND_ADDRESS_VALIDATION,
+ include_str!("../../../store/sqlite/sql/find_address_validation.sql"),
+ ),
+ (
+ WRITE_IDENTITY,
+ include_str!("../../../store/sqlite/sql/write_identity.sql"),
+ ),
+ (
+ FIND_IDENTITY,
+ include_str!("../../../store/sqlite/sql/find_identity.sql"),
+ ),
+ (
+ WRITE_SESSION,
+ include_str!("../../../store/sqlite/sql/write_session.sql"),
+ ),
+ (
+ FIND_SESSION,
+ include_str!("../../../store/sqlite/sql/find_session.sql"),
+ ),
+ ]
+ .iter()
+ .cloned()
+ .collect();
+
+ let pg_sqls: HashMap<&'static str, &'static str> = [
+ (
+ WRITE_ADDRESS,
+ include_str!("../../../store/pg/sql/write_address.sql"),
+ ),
+ (
+ FIND_ADDRESS,
+ include_str!("../../../store/pg/sql/find_address.sql"),
+ ),
+ (
+ WRITE_ADDRESS_VALIDATION,
+ include_str!("../../../store/pg/sql/write_address_validation.sql"),
+ ),
+ (
+ FIND_ADDRESS_VALIDATION,
+ include_str!("../../../store/pg/sql/find_address_validation.sql"),
+ ),
+ (
+ WRITE_IDENTITY,
+ include_str!("../../../store/pg/sql/write_identity.sql"),
+ ),
+ (
+ FIND_IDENTITY,
+ include_str!("../../../store/pg/sql/find_identity.sql"),
+ ),
+ (
+ WRITE_SESSION,
+ include_str!("../../../store/pg/sql/write_session.sql"),
+ ),
+ (
+ FIND_SESSION,
+ include_str!("../../../store/pg/sql/find_session.sql"),
+ ),
+ ]
+ .iter()
+ .cloned()
+ .collect();
+
+ let sqls: HashMap<&'static str, HashMap<&'static str, &'static str>> =
+ [(SQLITE, sqlite_sqls), (PGSQL, pg_sqls)]
+ .iter()
+ .cloned()
+ .collect();
+ sqls
+ };
+}
+
+pub trait SqlxResultExt<T> {
+ fn extend_err(self) -> Result<T, StoreError>;
+}
+
+impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> {
+ fn extend_err(self) -> Result<T, StoreError> {
+ if let Err(sqlx::Error::Database(dbe)) = &self {
+ if dbe.code() == Some("23505".into()) {
+ return Err(StoreError::IdempotentCheckAlreadyExists);
+ }
+ }
+ self.map_err(|e| StoreError::SqlClientError(e))
+ }
+}
+
+pub struct SqlClient<D>
+where
+ D: sqlx::Database,
+{
+ pool: sqlx::Pool<D>,
+ sqls_root: String,
+}
+
+pub struct PgClient {
+ sql: Arc<SqlClient<Postgres>>,
+}
+impl Store for PgClient {
+ fn get_type(&self) -> StoreType {
+ StoreType::Postgres {
+ c: self.sql.clone(),
+ }
+ }
+}
+
+impl PgClient {
+ pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
+ sqlx::migrate!("store/pg/migrations")
+ .run(&pool)
+ .await
+ .expect(ERR_MSG_MIGRATION_FAILED);
+
+ Arc::new(PgClient {
+ sql: Arc::new(SqlClient {
+ pool,
+ sqls_root: PGSQL.to_string(),
+ }),
+ })
+ }
+}
+
+pub struct SqliteClient {
+ sql: Arc<SqlClient<Sqlite>>,
+}
+impl Store for SqliteClient {
+ fn get_type(&self) -> StoreType {
+ StoreType::Sqlite {
+ c: self.sql.clone(),
+ }
+ }
+}
+
+impl SqliteClient {
+ pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
+ sqlx::migrate!("store/sqlite/migrations")
+ .run(&pool)
+ .await
+ .expect(ERR_MSG_MIGRATION_FAILED);
+
+ 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",
+ );
+
+ Arc::new(SqliteClient {
+ sql: Arc::new(SqlClient {
+ pool,
+ sqls_root: SQLITE.to_string(),
+ }),
+ })
+ }
+}
+
+impl<D> SqlClient<D>
+where
+ D: sqlx::Database,
+ for<'c> &'c Pool<D>: Executor<'c, Database = D>,
+ for<'c> &'c mut Transaction<'c, D>: Executor<'c, Database = D>,
+ for<'c> <D as HasArguments<'c>>::Arguments: IntoArguments<'c, D>,
+ for<'c> bool: Decode<'c, D> + Type<D>,
+ for<'c> bool: Encode<'c, D> + Type<D>,
+ for<'c> Option<bool>: Decode<'c, D> + Type<D>,
+ for<'c> Option<bool>: Encode<'c, D> + Type<D>,
+ for<'c> i64: Decode<'c, D> + Type<D>,
+ for<'c> i64: Encode<'c, D> + Type<D>,
+ for<'c> i32: Decode<'c, D> + Type<D>,
+ for<'c> i32: Encode<'c, D> + Type<D>,
+ for<'c> OffsetDateTime: Decode<'c, D> + Type<D>,
+ for<'c> OffsetDateTime: Encode<'c, D> + Type<D>,
+ for<'c> &'c str: ColumnIndex<<D as Database>::Row>,
+ for<'c> &'c str: Decode<'c, D> + Type<D>,
+ for<'c> &'c str: Encode<'c, D> + Type<D>,
+ for<'c> Option<&'c str>: Decode<'c, D> + Type<D>,
+ for<'c> Option<&'c str>: Encode<'c, D> + Type<D>,
+ for<'c> String: Decode<'c, D> + Type<D>,
+ for<'c> String: Encode<'c, D> + Type<D>,
+ for<'c> Option<String>: Decode<'c, D> + Type<D>,
+ for<'c> Option<String>: Encode<'c, D> + Type<D>,
+ for<'c> usize: ColumnIndex<<D as Database>::Row>,
+ for<'c> Uuid: Decode<'c, D> + Type<D>,
+ for<'c> Uuid: Encode<'c, D> + Type<D>,
+ for<'c> &'c [u8]: Encode<'c, D> + Type<D>,
+ for<'c> Option<&'c Uuid>: Encode<'c, D> + Type<D>,
+ for<'c> Vec<u8>: Encode<'c, D> + Type<D>,
+ for<'c> Vec<u8>: Decode<'c, D> + Type<D>,
+ for<'c> Option<Vec<u8>>: Encode<'c, D> + Type<D>,
+ for<'c> Option<Vec<u8>>: Decode<'c, D> + Type<D>,
+ for<'c> Option<OffsetDateTime>: Decode<'c, D> + Type<D>,
+ for<'c> Option<OffsetDateTime>: Encode<'c, D> + Type<D>,
+{
+ pub async fn write_address(&self, a: &Address) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_ADDRESS);
+ sqlx::query(&sqls[0])
+ .bind(a.id)
+ .bind(a.t.to_string())
+ .bind(match &a.t {
+ AddressType::Email { email_address } => {
+ email_address.as_ref().map(ToString::to_string)
+ }
+ AddressType::Sms { phone_number } => phone_number.clone(),
+ })
+ .bind(a.created_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+
+ Ok(())
+ }
+
+ pub async fn find_address(
+ &self,
+ id: Option<&Uuid>,
+ typ: Option<&str>,
+ val: Option<&str>,
+ ) -> Result<Vec<Address>, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, FIND_ADDRESS);
+ let res = sqlx::query_as::<_, (Uuid, String, String, OffsetDateTime)>(&sqls[0])
+ .bind(id)
+ .bind(typ)
+ .bind(val)
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ let mut addresses = vec![];
+ for (id, typ, val, created_at) in res.into_iter() {
+ let t = match AddressType::from_str(&typ)
+ .map_err(|_| StoreError::StoreValueCannotBeParsedInvariant)?
+ {
+ AddressType::Email { .. } => AddressType::Email {
+ email_address: Some(
+ EmailAddress::from_str(&val)
+ .map_err(|_| StoreError::StoreValueCannotBeParsedInvariant)?,
+ ),
+ },
+ AddressType::Sms { .. } => AddressType::Sms {
+ phone_number: Some(val.clone()),
+ },
+ };
+
+ addresses.push(Address { id, t, created_at });
+ }
+
+ Ok(addresses)
+ }
+
+ pub async fn write_address_validation(&self, v: &AddressValidation) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_ADDRESS_VALIDATION);
+ sqlx::query(&sqls[0])
+ .bind(v.id)
+ .bind(v.identity_id.as_ref())
+ .bind(v.address.id)
+ .bind(v.method.to_string())
+ .bind(v.hashed_token.clone())
+ .bind(v.hashed_code.clone())
+ .bind(v.attempts)
+ .bind(v.created_at)
+ .bind(v.expires_at)
+ .bind(v.revoked_at)
+ .bind(v.validated_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+
+ Ok(())
+ }
+
+ pub async fn find_address_validation(
+ &self,
+ id: Option<&Uuid>,
+ ) -> Result<Vec<AddressValidation>, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, FIND_ADDRESS_VALIDATION);
+ let rs = sqlx::query_as::<
+ _,
+ (
+ Uuid,
+ Option<Uuid>,
+ Uuid,
+ String,
+ String,
+ OffsetDateTime,
+ String,
+ Vec<u8>,
+ Vec<u8>,
+ i32,
+ OffsetDateTime,
+ OffsetDateTime,
+ Option<OffsetDateTime>,
+ Option<OffsetDateTime>,
+ ),
+ >(&sqls[0])
+ .bind(id)
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ let mut res = vec![];
+ for (
+ id,
+ identity_id,
+ address_id,
+ address_typ,
+ address_val,
+ address_created_at,
+ method,
+ hashed_token,
+ hashed_code,
+ attempts,
+ created_at,
+ expires_at,
+ revoked_at,
+ validated_at,
+ ) in rs.into_iter()
+ {
+ let t = match AddressType::from_str(&address_typ)
+ .map_err(|_| StoreError::StoreValueCannotBeParsedInvariant)?
+ {
+ AddressType::Email { .. } => AddressType::Email {
+ email_address: Some(
+ EmailAddress::from_str(&address_val)
+ .map_err(|_| StoreError::StoreValueCannotBeParsedInvariant)?,
+ ),
+ },
+ AddressType::Sms { .. } => AddressType::Sms {
+ phone_number: Some(address_val.clone()),
+ },
+ };
+
+ res.push(AddressValidation {
+ id,
+ identity_id,
+ address: Address {
+ id: address_id,
+ t,
+ created_at: address_created_at,
+ },
+ method: AddressValidationMethod::from_str(&method)
+ .map_err(|_| StoreError::StoreValueCannotBeParsedInvariant)?,
+ created_at,
+ expires_at,
+ revoked_at,
+ validated_at,
+ attempts,
+ hashed_token,
+ hashed_code,
+ });
+ }
+
+ Ok(res)
+ }
+
+ pub async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_IDENTITY);
+ sqlx::query(&sqls[0])
+ .bind(i.id)
+ // TODO: validate this is actually Json somewhere way up the chain (when being deserialized)
+ .bind(i.metadata.clone().unwrap_or("{}".into()))
+ .bind(i.created_at)
+ .bind(OffsetDateTime::now_utc())
+ .bind(i.deleted_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+
+ Ok(())
+ }
+
+ pub async fn find_identity(
+ &self,
+ id: Option<&Uuid>,
+ address_value: Option<&str>,
+ address_is_validated: Option<bool>,
+ session_token_hash: &Option<Vec<u8>>,
+ ) -> Result<Vec<Identity>, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY);
+ println!("{:?}", id);
+ println!("{:?}", address_value);
+ println!("{:?}", address_is_validated);
+ println!("{:?}", session_token_hash);
+ let rs = sqlx::query_as::<
+ _,
+ (
+ Uuid,
+ Option<String>,
+ OffsetDateTime,
+ OffsetDateTime,
+ Option<OffsetDateTime>,
+ ),
+ >(&sqls[0])
+ .bind(id)
+ .bind(address_value)
+ .bind(address_is_validated)
+ .bind(session_token_hash)
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ let mut res = vec![];
+ for (id, metadata, created_at, updated_at, deleted_at) in rs.into_iter() {
+ res.push(Identity {
+ id,
+ address_validations: vec![],
+ credentials: vec![],
+ rules: vec![],
+ metadata,
+ created_at,
+ deleted_at,
+ })
+ }
+
+ Ok(res)
+ }
+
+ pub async fn write_session(&self, s: &Session, token_hash: &[u8]) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_SESSION);
+ sqlx::query(&sqls[0])
+ .bind(s.identity_id)
+ .bind(token_hash)
+ .bind(s.created_at)
+ .bind(s.expired_at)
+ .bind(s.revoked_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+
+ Ok(())
+ }
+
+ pub async fn find_session(
+ &self,
+ token: Vec<u8>,
+ identity_id: Option<&Uuid>,
+ ) -> Result<Vec<Session>, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, FIND_SESSION);
+ let rs =
+ sqlx::query_as::<_, (Uuid, OffsetDateTime, OffsetDateTime, Option<OffsetDateTime>)>(
+ &sqls[0],
+ )
+ .bind(token)
+ .bind(identity_id)
+ .bind(OffsetDateTime::now_utc())
+ .bind(OffsetDateTime::now_utc())
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ let mut res = vec![];
+ for (identity_id, created_at, expired_at, revoked_at) in rs.into_iter() {
+ res.push(Session {
+ identity_id,
+ token: vec![],
+ created_at,
+ expired_at,
+ revoked_at,
+ });
+ }
+ Ok(res)
+ }
+}
+
+fn get_sqls(root: &str, file: &str) -> Vec<String> {
+ SQLS.get(root)
+ .unwrap()
+ .get(file)
+ .unwrap()
+ .split("--")
+ .map(|p| p.to_string())
+ .collect()
+}
diff --git a/crates/secd/src/client/types.rs b/crates/secd/src/client/types.rs
deleted file mode 100644
index bacade4..0000000
--- a/crates/secd/src/client/types.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub(crate) struct Email {
- address: String,
-}
diff --git a/crates/secd/src/command/admin.rs b/crates/secd/src/command/admin.rs
deleted file mode 100644
index b04dbef..0000000
--- a/crates/secd/src/command/admin.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-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
index 9c2babe..5590e8c 100644
--- a/crates/secd/src/command/authn.rs
+++ b/crates/secd/src/command/authn.rs
@@ -1,225 +1,281 @@
-use email_address::EmailAddress;
-use log::debug;
-use rand::distributions::{Alphanumeric, DistString};
-use time::Duration;
-use time::OffsetDateTime;
-use uuid::Uuid;
+use std::str::FromStr;
-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,
+ client::{
+ email::{EmailValidationMessage, Sendable},
+ store::{
+ AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError,
+ },
+ },
+ util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod,
+ Credential, CredentialType, Identity, IdentityId, PhoneNumber, Secd, SecdError, Session,
+ SessionToken, ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS,
+ ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION,
};
+use email_address::EmailAddress;
+use log::warn;
+use rand::Rng;
+use time::{Duration, OffsetDateTime};
+use uuid::Uuid;
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(
+ pub async fn validate_email(
&self,
- provider: &OauthProviderName,
- scope: Option<String>,
- ) -> Result<OauthRedirectAuthUrl, SecdError> {
- if scope.is_some() {
- return Err(SecdError::NotImplemented(
- "Only default scopes are currently supported.".into(),
- ));
+ email_address: &str,
+ identity_id: Option<Uuid>,
+ ) -> Result<AddressValidation, SecdError> {
+ let email_address = EmailAddress::from_str(email_address)?;
+ // record address (idempotent operation)
+ let mut address = Address {
+ id: Uuid::new_v4(),
+ t: AddressType::Email {
+ email_address: Some(email_address.clone()),
+ },
+ created_at: OffsetDateTime::now_utc(),
+ };
+
+ if let Err(StoreError::IdempotentCheckAlreadyExists) =
+ address.write(self.store.clone()).await
+ {
+ address = Address::find(
+ self.store.clone(),
+ &AddressLens {
+ id: None,
+ t: Some(&AddressType::Email {
+ email_address: Some(email_address.clone()),
+ }),
+ },
+ )
+ .await?
+ .into_iter()
+ .next()
+ .ok_or(SecdError::AddressValidationFailed)?;
}
- let p = self
- .store
- .read_oauth_provider(provider, None)
- .await
- .map_err(|_| SecdError::InternalError(INTERNAL_ERR_MSG.to_string()))?;
+ let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>());
+ let code: String = vec![0; ADDRESSS_VALIDATION_CODE_SIZE as usize]
+ .into_iter()
+ .map(|_| char::from_digit(rand::thread_rng().gen_range(0..=9), 10).unwrap())
+ .collect();
- 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))?;
+ let mut validation = AddressValidation {
+ id: Uuid::new_v4(),
+ identity_id,
+ address,
+ method: AddressValidationMethod::Email,
+ created_at: OffsetDateTime::now_utc(),
+ expires_at: OffsetDateTime::now_utc()
+ .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0))
+ .ok_or(SecdError::Todo)?,
+ revoked_at: None,
+ validated_at: None,
+ attempts: 0,
+ hashed_token: util::hash(&secret.as_bytes()),
+ hashed_code: util::hash(&code.as_bytes()),
+ };
+
+ validation.write(self.store.clone()).await?;
- build_oauth_auth_url(&p, req_id)
+ let msg =EmailValidationMessage {
+ recipient: email_address.clone(),
+ subject: "Confirm Your Email".into(),
+ body: format!("This is an email validation message. Click this link [{:?}?s={}] or use the code [{}]", validation.id, secret, code),
+ };
+
+ match msg.send().await {
+ Ok(_) => { /* TODO: Write down the message*/ }
+ Err(e) => {
+ validation.revoked_at = Some(OffsetDateTime::now_utc());
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::EmailMessengerError(e));
+ }
+ }
+
+ Ok(validation)
}
- /// create_validation_request_email
- ///
- /// Generate a request to validate the provided email.
- pub async fn create_validation_request_email(
+ pub async fn validate_sms(
&self,
- email: &str,
- ) -> Result<ValidationRequestId, SecdError> {
- let now = OffsetDateTime::now_utc();
+ phone_number: &PhoneNumber,
+ ) -> Result<AddressValidation, SecdError> {
+ todo!()
+ }
+ pub async fn complete_address_validation(
+ &self,
+ validation_id: &AddressValidationId,
+ plaintext_token: Option<String>,
+ plaintext_code: Option<String>,
+ ) -> Result<Session, SecdError> {
+ let mut validation = AddressValidation::find(
+ self.store.clone(),
+ &AddressValidationLens {
+ id: Some(validation_id),
+ },
+ )
+ .await?
+ .into_iter()
+ .next()
+ .ok_or(SecdError::AddressValidationFailed)?;
- let email = if EmailAddress::is_valid(email) {
- email
- } else {
- return Err(SecdError::InvalidEmailAddress);
- };
+ if validation.validated_at.is_some() {
+ return Err(SecdError::AddressValidationExpiredOrConsumed);
+ }
- 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,
- };
+ validation.attempts += 1;
+ if validation.attempts > ADDRESS_VALIDATION_ALLOWS_ATTEMPTS as i32 {
+ warn!(
+ "validation failed: Too many validation attempts were tried for validation {:?}",
+ validation.id
+ );
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::AddressValidationExpiredOrConsumed);
+ }
- 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)
+ let hashed_token = plaintext_token.map(|s| util::hash(s.as_bytes()));
+ let hashed_code = plaintext_code.map(|c| util::hash(c.as_bytes()));
+
+ let mut warn_msg = None;
+ match (hashed_token, hashed_code) {
+ (None, None) => {
+ warn_msg = Some("neither token nor hash was provided during the address validation session exchange");
}
- 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)
+ (Some(t), None) => {
+ if validation.hashed_token != t {
+ warn_msg =
+ Some("the provided token does not match the address validation token");
+ }
+ }
+ (None, Some(c)) => {
+ if validation.hashed_code != c {
+ warn_msg = Some("the provided code does not match the address validation code");
+ }
+ }
+ (Some(t), Some(c)) => {
+ if validation.hashed_token != t || validation.hashed_code != c {
+ warn_msg = Some("the provided token and code must both match the address validation token and code");
+ }
}
};
- self.email_messenger
- .send_email(email, &req_id.to_string(), &ev.code.unwrap(), mail_type)
- .await?;
+ if let Some(msg) = warn_msg {
+ warn!("validation failed: {}", msg);
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::AddressValidationSessionExchangeFailed);
+ }
+
+ let identity = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: None,
+ address_type: Some(&validation.address.t),
+ validated_address: Some(true),
+ session_token_hash: None,
+ },
+ )
+ .await?;
+
+ if !ADDRESS_VALIDATION_IDENTITY_SURJECTION && identity.len() > 1 {
+ warn!("validation failed: identity validation surjection disallowed");
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::TooManyIdentities);
+ }
+
+ let mut identity = identity.into_iter().next();
+ if identity.is_none() {
+ let i = Identity {
+ id: Uuid::new_v4(),
+ address_validations: vec![],
+ credentials: vec![],
+ rules: vec![],
+ metadata: None,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ };
+ i.write(self.store.clone()).await?;
+ identity = Some(i);
+ }
- Ok(req_id)
+ assert!(identity.is_some());
+
+ // If the validation was attached to another identity, unless surjection is allowed, it cannot be recorded.
+ if !ADDRESS_VALIDATION_IDENTITY_SURJECTION
+ && validation.identity_id.is_some()
+ && identity.as_ref().map(|i| i.id) != validation.identity_id
+ {
+ warn!("validation failed: identity validation surjection is disallowed, but found existing identity for another account");
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::TooManyIdentities);
+ }
+
+ validation.identity_id = identity.map(|i| i.id);
+ validation.validated_at = Some(OffsetDateTime::now_utc());
+ validation.write(self.store.clone()).await?;
+
+ let session = Session::new(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?;
+ session.write(self.store.clone()).await?;
+
+ Ok(session)
}
- /// 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(
+ pub async fn create_credential(
&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
- }),
- };
+ t: CredentialType,
+ key: String,
+ value: Option<String>,
+ ) -> Result<Credential, SecdError> {
+ todo!()
+ }
- if v.expired() || v.is_validated() {
- return Err(SecdError::InvalidCode);
- };
+ pub async fn validate_credential(
+ &self,
+ t: CredentialType,
+ key: String,
+ value: Option<String>,
+ ) -> Result<Session, SecdError> {
+ todo!()
+ }
- let mut identity = Identity {
- id: Uuid::new_v4(),
- data: None,
- created_at: OffsetDateTime::now_utc(),
- deleted_at: None,
- };
+ pub async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError> {
+ let token = hex::decode(t)?;
+ let mut session = Session::find(
+ self.store.clone(),
+ &SessionLens {
+ token_hash: Some(&util::hash(&token)),
+ identity_id: None,
+ },
+ )
+ .await?;
+ assert!(session.len() <= 1, "get session failed: multiple sessions found for a single token. This is very _very_ bad.");
- 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())
- })?,
- };
+ if session.is_empty() {
+ return Err(SecdError::InvalidSession);
+ } else {
+ let mut session = session.swap_remove(0);
+ session.token = token;
+ Ok(session)
+ }
+ }
- 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,
- expired_at: now
- .checked_add(Duration::new(SESSION_DURATION, 0))
- .ok_or(SecdError::SessionExpiryOverflow)?,
- revoked_at: None,
- };
+ pub async fn get_identity(&self, i: &SessionToken) -> Result<Identity, SecdError> {
+ let token_hash = util::hash(&hex::decode(i)?);
+ let mut i = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: None,
+ address_type: None,
+ validated_address: None,
+ session_token_hash: Some(token_hash),
+ },
+ )
+ .await?;
- self.store
- .write_session(&s)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Todo))?;
+ assert!(
+ i.len() <= 1,
+ "The provided id refers to more than one identity. This is very _very_ bad."
+ );
- Ok(s)
+ if i.is_empty() {
+ return Err(SecdError::IdentityNotFound);
+ } else {
+ Ok(i.swap_remove(0))
+ }
}
}
diff --git a/crates/secd/src/command/mod.rs b/crates/secd/src/command/mod.rs
index cd0d8c3..c14cf6c 100644
--- a/crates/secd/src/command/mod.rs
+++ b/crates/secd/src/command/mod.rs
@@ -1,42 +1,54 @@
-pub mod admin;
pub mod authn;
-use crate::client::{
- email,
- sqldb::{PgClient, SqliteClient},
+use super::{AuthEmailMessenger, AuthStore, Secd, SecdError};
+use crate::{
+ client::{
+ email,
+ store::sql_db::{PgClient, SqliteClient},
+ },
+ ENV_AUTH_STORE_CONN_STRING, ENV_EMAIL_MESSENGER, ENV_EMAIL_MESSENGER_CLIENT_ID,
+ ENV_EMAIL_MESSENGER_CLIENT_SECRET,
};
-use crate::{AuthEmail, AuthStore, Secd, SecdError};
-use log::error;
-use std::sync::Arc;
+use log::{error, info};
+use std::{env::var, str::FromStr, 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> {
+ pub async fn init() -> Result<Self, SecdError> {
+ let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok());
+ let email_messenger = AuthEmailMessenger::from_str(
+ &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()),
+ )
+ .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5");
+ let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok();
+ let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok();
+
+ info!("starting client with auth_store: {:?}", auth_store);
+ info!("starting client with email_messenger: {:?}", auth_store);
+
let store = match auth_store {
- AuthStore::Sqlite => {
+ AuthStore::Sqlite { conn } => {
SqliteClient::new(
sqlx::sqlite::SqlitePoolOptions::new()
- .connect(conn_string.unwrap_or("sqlite::memory:".into()))
+ .connect(&conn)
.await
- .map_err(|e| SecdError::InitializationFailure(e))?,
+ .map_err(|e| {
+ SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e))
+ })?,
)
.await
}
- AuthStore::Postgres => {
+ AuthStore::Postgres { conn } => {
PgClient::new(
sqlx::postgres::PgPoolOptions::new()
- .connect(conn_string.expect("No postgres connection string provided."))
+ .connect(&conn)
.await
- .map_err(|e| SecdError::InitializationFailure(e))?,
+ .map_err(|e| {
+ SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e))
+ })?,
)
.await
}
@@ -50,11 +62,7 @@ impl Secd {
};
let email_sender = match email_messenger {
- // TODO: initialize email and SMS templates with secd
- AuthEmail::LocalStub => email::LocalEmailStubber {
- email_template_login,
- email_template_signup,
- },
+ AuthEmailMessenger::Local => email::LocalMailer {},
_ => unimplemented!(),
};
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 17186c8..c84f7cf 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -2,382 +2,185 @@ mod client;
mod command;
mod util;
-use std::sync::Arc;
-
-use clap::ValueEnum;
-use client::{EmailMessenger, EmailMessengerError, Store};
-use derive_more::Display;
+use client::{
+ email::{EmailMessenger, EmailMessengerError},
+ store::{Store, StoreError},
+};
use email_address::EmailAddress;
use serde::{Deserialize, Serialize};
-use sqlx::FromRow;
-use strum_macros::{EnumString, EnumVariantNames};
+use serde_with::{serde_as, DisplayFromStr};
+use std::sync::Arc;
+use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
use url::Url;
-use util::get_oauth_identity_data;
use uuid::Uuid;
+pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING";
+pub const ENV_EMAIL_MESSENGER: &str = "SECD_EMAIL_MESSENGER";
+pub const ENV_EMAIL_MESSENGER_CLIENT_ID: &str = "SECD_EMAIL_MESSENGER_CLIENT_ID";
+pub const ENV_EMAIL_MESSENGER_CLIENT_SECRET: &str = "SECD_EMAIL_MESSENGER_CLIENT_SECRET";
+
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_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,
- pub private_key: String,
-}
-
-#[derive(sqlx::FromRow, Debug, Serialize)]
-pub struct Authorization {
- session: Session,
-}
-
-#[derive(sqlx::FromRow, Debug, Serialize)]
-pub struct Identity {
- #[sqlx(rename = "identity_public_id")]
- id: Uuid,
- #[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)]
-pub struct Session {
- #[sqlx(rename = "identity_public_id")]
- pub identity_id: IdentityId,
- #[serde(skip_serializing_if = "Option::is_none")]
- #[sqlx(default)]
- pub secret: Option<SessionSecret>,
- #[serde(with = "time::serde::timestamp")]
- pub created_at: OffsetDateTime,
- #[serde(with = "time::serde::timestamp")]
- pub expired_at: OffsetDateTime,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub revoked_at: Option<OffsetDateTime>,
-}
+const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6;
+const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5;
+const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true;
-#[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<()>;
-}
+pub type AddressId = Uuid;
+pub type AddressValidationId = Uuid;
+pub type CredentialId = Uuid;
+pub type IdentityId = Uuid;
+pub type MotifId = Uuid;
+pub type PhoneNumber = String;
+pub type RefId = Uuid;
+pub type SessionToken = String;
-#[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(())
- }
-}
+#[derive(Debug, derive_more::Display, thiserror::Error)]
+pub enum SecdError {
+ AddressValidationFailed,
+ AddressValidationSessionExchangeFailed,
+ AddressValidationExpiredOrConsumed,
-#[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?;
+ TooManyIdentities,
+ IdentityNotFound,
- let identity = store
- .find_identity(None, oauth_identity.email.as_deref())
- .await?;
+ EmailMessengerError(#[from] EmailMessengerError),
+ InvalidEmaillAddress(#[from] email_address::Error),
- 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(())
- }
-}
+ FailedToProvideSessionIdentity(String),
+ InvalidSession,
-#[derive(Debug, EnumString)]
-pub enum ValidationType {
- Email,
- Oauth,
-}
-
-#[derive(sqlx::FromRow, Debug)]
-pub struct EmailValidation {
- #[sqlx(rename = "email_validation_public_id")]
- id: Option<Uuid>,
- #[sqlx(rename = "identity_public_id")]
- identity_id: Option<IdentityId>,
- #[sqlx(rename = "address")]
- email_address: String,
- 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>,
-}
+ StoreError(#[from] StoreError),
+ StoreInitFailure(String),
-#[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,
- 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,
+ FailedToDecodeInput(#[from] hex::FromHexError),
+ Todo,
}
-// 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,
- Facebook,
- Github,
- Gitlab,
- Google,
- Instagram,
- LinkedIn,
- Microsoft,
- Paypal,
- Reddit,
- Spotify,
- Strava,
- Stripe,
- Twitch,
- Twitter,
- WeChat,
+pub struct Secd {
+ store: Arc<dyn Store + Send + Sync + 'static>,
+ email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>,
}
#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)]
#[strum(ascii_case_insensitive)]
pub enum AuthStore {
- Sqlite,
- Postgres,
- MySql,
- Mongo,
- Dynamo,
- Redis,
+ Sqlite { conn: String },
+ Postgres { conn: String },
+ Redis { conn: String },
}
#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)]
#[strum(ascii_case_insensitive)]
-pub enum AuthEmail {
- LocalStub,
+pub enum AuthEmailMessenger {
+ Local,
Ses,
Mailgun,
Sendgrid,
}
-pub type IdentityId = Uuid;
-pub type SessionSecret = String;
-pub type SessionSecretHash = String;
-pub type ValidationRequestId = Uuid;
-pub type ValidationSecretCode = String;
-pub type OauthRedirectAuthUrl = Url;
+#[serde_with::skip_serializing_none]
+#[derive(Debug, Serialize)]
+pub struct Address {
+ id: AddressId,
+ t: AddressType,
+ #[serde(with = "time::serde::timestamp")]
+ created_at: OffsetDateTime,
+}
-#[derive(Debug, derive_more::Display, thiserror::Error)]
-pub enum SecdError {
- EmailSendError(#[from] EmailMessengerError),
- EmailValidationExpiryOverflow,
- EmailValidationRequestError,
- OauthValidationRequestError,
- IdentityIdShouldExistInvariant,
- InitializationFailure(sqlx::Error),
- InvalidCode,
- InvalidEmailAddress,
- InputValidation(String),
- InternalError(String),
- NotImplemented(String),
- SessionExpiryOverflow,
- Unauthenticated,
- Todo,
+#[serde_as]
+#[serde_with::skip_serializing_none]
+#[derive(Debug, Serialize)]
+pub struct AddressValidation {
+ pub id: AddressValidationId,
+ pub identity_id: Option<IdentityId>,
+ pub address: Address,
+ pub method: AddressValidationMethod,
+ #[serde(with = "time::serde::timestamp")]
+ pub created_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp")]
+ pub expires_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub revoked_at: Option<OffsetDateTime>,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub validated_at: Option<OffsetDateTime>,
+ pub attempts: i32,
+ #[serde_as(as = "serde_with::hex::Hex")]
+ hashed_token: Vec<u8>,
+ #[serde_as(as = "serde_with::hex::Hex")]
+ hashed_code: Vec<u8>,
}
-pub struct Secd {
- store: Arc<dyn Store + Send + Sync + 'static>,
- email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>,
+#[derive(Debug, Display, Serialize, EnumString)]
+pub enum AddressValidationMethod {
+ Email,
+ Sms,
+ Oauth,
}
-impl Secd {
- /// get_identity
- ///
- /// Return all information associated with the identity id.
- pub async fn get_identity(&self, identity: IdentityId) -> Result<Identity, SecdError> {
- unimplemented!()
- }
- /// get_authorization
- ///
- /// Return the authorization for this session. If the session is
- /// invalid, expired or otherwise unauthenticated, an error will
- /// be returned.
- pub async fn get_authorization(
- &self,
- secret: SessionSecret,
- ) -> Result<Authorization, SecdError> {
- match self.store.read_session(&secret).await {
- Ok(session)
- if session.expired_at > OffsetDateTime::now_utc()
- || session.revoked_at > Some(OffsetDateTime::now_utc()) =>
- {
- Ok(Authorization { session })
- }
- Ok(_) => Err(SecdError::Unauthenticated),
- Err(_e) => Err(SecdError::Todo),
- }
- }
- /// revoke_session
- ///
- /// Revokes a session such that it may no longer be used to authenticate
- /// the associated identity.
- pub async fn revoke_session(&self, secret_hash: SessionSecretHash) -> Result<(), SecdError> {
- unimplemented!()
- }
- /// revoke_identity
- ///
- /// Soft delete an identity such that all associated resources are
- /// deleted as well.
- ///
- /// NOTE: This operation cannot be undone. Although it may not be undone
- /// a separate call to delete_identity is required to cleanup necessary
- /// resources.
- ///
- /// You may configure secd to periodically clean all revoked
- /// identities and associated resources with AUTOCLEAN_REVOKED.
- pub async fn revoke_identity(&self, identity_id: IdentityId) -> Result<(), SecdError> {
- unimplemented!()
- }
- /// delete_identity
- ///
- /// Delete an identity and all associated resources (e.g. session,
- /// authorization structures, etc...). This is a hard delete and permanently
- /// removes all stored information.
- ///
- /// NOTE: An identity _must_ be revoked before it can be deleted. Otherwise,
- /// secd will return an error.
- pub async fn delete_identity(&self, identity_id: IdentityId) -> Result<(), SecdError> {
- unimplemented!()
- }
+#[derive(Debug, Display, Serialize, EnumString)]
+pub enum AddressType {
+ Email { email_address: Option<EmailAddress> },
+ Sms { phone_number: Option<PhoneNumber> },
+}
- // register service
- // register service_action(service_id, action)
- // list services
- // list service actions
+#[derive(Debug, Serialize)]
+pub struct Credential {
+ pub id: CredentialId,
+ pub identity_id: IdentityId,
+ pub t: CredentialType,
+}
+
+#[serde_as]
+#[derive(Debug, Serialize)]
+pub enum CredentialType {
+ Passphrase {
+ key: String,
+ value: String,
+ },
+ Oicd {
+ value: String,
+ },
+ OneTimeCodes {
+ codes: Vec<String>,
+ },
+ Totp {
+ #[serde_as(as = "DisplayFromStr")]
+ url: Url,
+ code: String,
+ },
+ WebAuthn {
+ value: String,
+ },
+}
+
+#[serde_with::skip_serializing_none]
+#[derive(Debug, Serialize)]
+pub struct Identity {
+ pub id: IdentityId,
+ pub address_validations: Vec<AddressValidation>,
+ pub credentials: Vec<Credential>,
+ pub rules: Vec<String>, // TODO: rules for (e.g. mfa reqs)
+ pub metadata: Option<String>,
+ #[serde(with = "time::serde::timestamp")]
+ pub created_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub deleted_at: Option<OffsetDateTime>,
+}
- // create permission
- // create group (name, identities)
- // create role (name, permissios)
- // list group
- // list role
- // list permission
- // describe group
- // describe role
- // describe permission
- // add_identity_to_group
- // remove_identity_from_group
- // add_permission_to_role
- // remove_permission_from_role
- // attach_role_to_group
- // attach_permission_to_group (just creates single role and attaches it)
+#[serde_as]
+#[serde_with::skip_serializing_none]
+#[derive(Debug, Serialize)]
+pub struct Session {
+ pub identity_id: IdentityId,
+ #[serde_as(as = "serde_with::hex::Hex")]
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub token: Vec<u8>,
+ #[serde(with = "time::serde::timestamp")]
+ pub created_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp")]
+ pub expired_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub revoked_at: Option<OffsetDateTime>,
}
diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs
new file mode 100644
index 0000000..bab8a25
--- /dev/null
+++ b/crates/secd/src/util/from.rs
@@ -0,0 +1,66 @@
+use std::str::FromStr;
+
+use crate::AuthStore;
+
+impl From<Option<String>> for AuthStore {
+ fn from(s: Option<String>) -> Self {
+ let conn = s.clone().unwrap_or("sqlite::memory:".into());
+ match conn.split(":").next() {
+ Some("postgresql") | Some("postgres") => AuthStore::Postgres { conn },
+ Some("sqlite") => AuthStore::Sqlite { conn },
+ _ => panic!(
+ "AuthStore: Invalid database connection string provided. Found: {:?}",
+ s
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn auth_store_from_string() {
+ let in_conn = None;
+ let a = AuthStore::from(in_conn.clone());
+ match AuthStore::from(in_conn.clone()) {
+ AuthStore::Sqlite { conn } => assert_eq!(conn, "sqlite::memory:"),
+ r @ _ => assert!(
+ false,
+ "should have parsed None as in-memory sqlite. Found: {:?}",
+ r
+ ),
+ }
+
+ let postgresql_conn = Some("postgresql://testuser:p4ssw0rd@1.2.3.4:5432/test-db".into());
+ match AuthStore::from(postgresql_conn.clone()) {
+ AuthStore::Postgres { conn } => assert_eq!(conn, postgresql_conn.unwrap()),
+ r @ _ => assert!(false, "should have parsed as postgres. Found: {:?}", r),
+ }
+
+ let postgres_conn = Some("postgres://testuser:p4ssw0rd@1.2.3.4:5432/test-db".into());
+ match AuthStore::from(postgres_conn.clone()) {
+ AuthStore::Postgres { conn } => assert_eq!(conn, postgres_conn.unwrap()),
+ r @ _ => assert!(false, "should have parsed as postgres. Found: {:?}", r),
+ }
+
+ let sqlite_conn = Some("sqlite:///path/to/db.sql".into());
+ let a = AuthStore::from(sqlite_conn.clone());
+ match AuthStore::from(sqlite_conn.clone()) {
+ AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_conn.unwrap()),
+ r @ _ => assert!(false, "should have parsed as sqlite. Found: {:?}", r),
+ }
+
+ let sqlite_mem_conn = Some("sqlite:memory:".into());
+ let a = AuthStore::from(sqlite_mem_conn.clone());
+ match AuthStore::from(sqlite_mem_conn.clone()) {
+ AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_mem_conn.unwrap()),
+ r @ _ => assert!(
+ false,
+ "should have parsed as in-memoy sqlite. Found: {:?}",
+ r
+ ),
+ }
+ }
+}
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index bb177cb..6677c2f 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -1,38 +1,12 @@
-use std::str::FromStr;
+pub(crate) mod from;
-use anyhow::{bail, Context};
-use log::error;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
-use reqwest::header;
-use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use time::OffsetDateTime;
use url::Url;
-use crate::{
- OauthProvider, OauthProviderName, OauthValidation, SecdError, ValidationRequestId,
- INTERNAL_ERR_MSG,
-};
-
-pub(crate) fn log_err(e: Box<dyn std::error::Error>, new_e: SecdError) -> SecdError {
- error!("{:?}", e);
- new_e
-}
-pub(crate) fn to_secd_err(e: anyhow::Error, new_e: SecdError) -> SecdError {
- error!("{:?}", e);
- new_e
-}
-
-pub(crate) fn log_err_sqlx(e: sqlx::Error) -> sqlx::Error {
- error!("{:?}", e);
- e
-}
-pub(crate) fn generate_random_url_safe(n: usize) -> String {
- thread_rng()
- .sample_iter(&Alphanumeric)
- .take(n)
- .map(char::from)
- .collect()
-}
+use crate::{AddressType, IdentityId, SecdError, Session, SESSION_DURATION, SESSION_SIZE_BYTES};
pub(crate) fn remove_trailing_slash(url: &mut Url) -> String {
let mut u = url.to_string();
@@ -44,134 +18,37 @@ pub(crate) fn remove_trailing_slash(url: &mut Url) -> String {
u
}
-pub(crate) fn build_oauth_auth_url(
- p: &OauthProvider,
- validation_id: ValidationRequestId,
-) -> Result<Url, SecdError> {
- let redirect_url = remove_trailing_slash(&mut p.redirect_url.clone());
-
- Ok(Url::from_str(&format!(
- "{}?client_id={}&response_type={}&redirect_uri={}&scope={}&state={}",
- p.base_url,
- p.client_id,
- p.response.to_string().to_lowercase(),
- redirect_url,
- p.default_scope,
- validation_id.to_string()
- ))
- .map_err(|_| SecdError::InternalError(INTERNAL_ERR_MSG.into()))?)
-}
-
-pub(crate) async fn get_oauth_identity_data(
- validation: &OauthValidation,
-) -> anyhow::Result<OauthAccessIdentity> {
- let provider = validation.oauth_provider.name;
- let token = validation
- .access_token
- .clone()
- .ok_or(SecdError::InternalError(
- "no access token provided with which to build oauth data url".into(),
- ))?;
-
- let url = Url::from_str(&format!(
- "{}{}",
- match provider {
- OauthProviderName::Google =>
- "https://www.googleapis.com/oauth2/v2/userinfo?access_token=",
- _ => unimplemented!(),
- },
- token
- ))?;
-
- let resp = reqwest::get(url).await?.json::<serde_json::Value>().await?;
- let identity = match provider {
- OauthProviderName::Google => OauthAccessIdentity {
- email: resp
- .get("email")
- .and_then(|v| v.as_str().map(|s| s.to_string())),
- email_is_verified: resp.get("verified_email").and_then(|v| v.as_bool()),
- picture_url: resp
- .get("picture")
- .and_then(|v| Url::from_str(&v.to_string()).ok()),
- },
- _ => unimplemented!(),
- };
-
- Ok(identity)
-}
-
-#[derive(Debug, Serialize)]
-pub(crate) struct OauthAccessTokenGoogleRequest {
- grant_type: String,
- code: String,
- client_id: String,
- client_secret: String,
- redirect_uri: String,
-}
-
-#[derive(Debug, Deserialize)]
-pub(crate) struct OauthAccessTokenGoogleResponse {
- access_token: String,
- expires_in: i32,
- token_type: String,
- scope: String,
- id_token: String,
+pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
+ let mut hasher = Sha256::new();
+ hasher.update(i);
+ hasher.finalize().to_vec()
}
-#[derive(Debug)]
-pub(crate) struct OauthAccessIdentity {
- pub(crate) email: Option<String>,
- pub(crate) email_is_verified: Option<bool>,
- pub(crate) picture_url: Option<Url>,
-}
-
-type AccessTokenRequestData = String;
-
-pub(crate) async fn get_oauth_access_token(
- validation: &OauthValidation,
- secret_code: &String,
-) -> anyhow::Result<String> {
- let provider = validation.oauth_provider.name;
-
- let url = Url::from_str(match provider {
- OauthProviderName::Google => "https://accounts.google.com/o/oauth2/token",
- _ => unimplemented!(),
- })?;
-
- let request_data = serde_json::to_string(&match provider {
- OauthProviderName::Google => OauthAccessTokenGoogleRequest {
- grant_type: "authorization_code".to_string(),
- code: secret_code.to_string(),
- client_id: validation.oauth_provider.client_id.clone(),
- client_secret: validation.oauth_provider.client_secret.clone(),
- redirect_uri: remove_trailing_slash(
- &mut validation.oauth_provider.redirect_url.clone(),
- ),
- },
- _ => unimplemented!(),
- })?;
-
- let r = reqwest::Client::new()
- .post(url)
- .body(request_data)
- .header(header::CONTENT_TYPE, "application/json")
- .send()
- .await
- .context(format!(
- "Failed to successfully POST a new access token for: {}",
- provider
- ))?;
-
- let access_token = match provider {
- OauthProviderName::Google => {
- let resp: OauthAccessTokenGoogleResponse = r.json().await.context(format!(
- "Failed to parse access token response for: {}",
- provider
- ))?;
- resp.access_token
+impl AddressType {
+ pub fn get_value(&self) -> Option<String> {
+ match &self {
+ AddressType::Email { email_address } => {
+ email_address.as_ref().map(|a| a.to_string().clone())
+ }
+ AddressType::Sms { phone_number } => phone_number.as_ref().cloned(),
}
- _ => unimplemented!(),
- };
+ }
+}
- Ok(access_token)
+impl Session {
+ pub(crate) fn new(identity_id: IdentityId) -> Result<Self, SecdError> {
+ let token = (0..SESSION_SIZE_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>();
+ let now = OffsetDateTime::now_utc();
+ Ok(Session {
+ identity_id,
+ token,
+ created_at: now,
+ expired_at: now
+ .checked_add(time::Duration::new(SESSION_DURATION, 0))
+ .ok_or(SecdError::Todo)?,
+ revoked_at: None,
+ })
+ }
}
diff --git a/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql b/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql
deleted file mode 100644
index 3d4d84c..0000000
--- a/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql
+++ /dev/null
@@ -1,86 +0,0 @@
-create extension if not exists pgcrypto;
-create extension if not exists citext;
-create schema if not exists secd;
-
-create table if not exists secd.identity (
- identity_id bigserial primary key
- , identity_public_id uuid
- , data text
- , created_at timestamptz not null
- , deleted_at timestamptz
- , unique(identity_public_id)
-);
-
-create table if not exists secd.session (
- session_id bigserial primary key
- , identity_id bigint not null references secd.identity(identity_id)
- , secret_hash bytea not null
- , created_at timestamptz not null
- , expired_at timestamptz
- , revoked_at timestamptz
- , unique(secret_hash)
-);
-
-create table if not exists secd.oauth_provider (
- oauth_provider_id serial primary key
- , name text not null
- , flow text not null
- , base_url text not null
- , response_type text not null
- , default_scope text
- , client_id text not null
- , client_secret text not null
- , redirect_url text not null
- , created_at timestamptz not null
- , deleted_at timestamptz
- , unique (name, flow)
-);
-
-create table if not exists secd.oauth_validation (
- oauth_validation_id bigserial primary key
- , oauth_validation_public_id uuid not null
- , oauth_provider_id integer not null references secd.oauth_provider(oauth_provider_id)
- , access_token text
- , raw_response text
- , created_at timestamptz not null
- , validated_at timestamptz
- , unique (oauth_validation_public_id)
-);
-
-create table if not exists secd.identity_oauth_validation (
- identity_oauth_validation_id bigserial primary key
- -- A validation does not require an identity to initiate
- , identity_id bigint references secd.identity(identity_id)
- , oauth_validation_id bigint not null references secd.oauth_validation(oauth_validation_id)
- , revoked_at timestamptz
- , deleted_at timestamptz
- , unique(identity_id, oauth_validation_id)
-);
-
-create table if not exists secd.email (
- email_id bigserial primary key
- , address text not null
- , unique(address)
-);
-
-create table if not exists secd.email_validation (
- email_validation_id bigserial primary key
- , email_validation_public_id uuid not null
- , email_id bigint not null references secd.email(email_id)
- , code text
- , is_oauth_derived boolean not null
- , created_at timestamptz not null
- , validated_at timestamptz
- , expired_at timestamptz
- , unique(email_validation_public_id)
-);
-
-create table if not exists secd.identity_email_validation (
- identity_email_validation_id bigserial primary key
- -- A validation does not require an identity to initiate
- , identity_id bigint references secd.identity(identity_id)
- , email_validation_id bigint not null references secd.email_validation(email_validation_id)
- , revoked_at timestamptz
- , deleted_at timestamptz
- , unique(identity_id, email_validation_id)
-);
diff --git a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
new file mode 100644
index 0000000..2b89957
--- /dev/null
+++ b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
@@ -0,0 +1,85 @@
+create extension if not exists pgcrypto;
+create extension if not exists citext;
+create schema if not exists secd;
+
+create table if not exists secd.realm (
+ realm_id bigserial primary key
+ , created_at timestamptz not null
+);
+
+create table if not exists secd.realm_data (
+ realm_data_id bigserial primary key
+ , realm_id bigint not null references secd.realm(realm_id)
+ , email_provider jsonb not null
+ , sms_provider jsonb not null
+ , created_at timestamptz not null
+ , deleted_at timestamptz
+);
+
+create table if not exists secd.identity (
+ identity_id bigserial primary key
+ , identity_public_id uuid not null
+ , data jsonb -- some things are dervied, others are not
+ , created_at timestamptz not null
+ , updated_at timestamptz not null
+ , deleted_at timestamptz
+ , unique(identity_public_id)
+);
+
+create table if not exists secd.credential (
+ credential_id bigserial primary key
+ , credential_public_id uuid not null
+ , identity_id bigint not null references secd.identity(identity_id)
+ , type text not null-- e.g. password, oidc, totop, lookup_secret, webauthn, ...
+ , data jsonb not null
+ , version integer not null
+ , created_at timestamptz not null
+ , revoked_at timestamptz
+ , deleted_at timestamptz
+);
+
+create table if not exists secd.address (
+ address_id bigserial primary key
+ , address_public_id uuid not null
+ , type text not null
+ , value text not null
+ , created_at timestamptz not null
+ , unique(value, type)
+);
+
+create table if not exists secd.address_validation (
+ address_validation_id bigserial primary key
+ , address_validation_public_id uuid not null
+ , identity_id bigint references secd.identity(identity_id)
+ , address_id bigint not null references secd.address(address_id)
+ , method text not null -- e.g. email, sms, voice, oidc
+ , token_hash bytea
+ , code_hash bytea
+ , attempts integer not null
+ , created_at timestamptz not null
+ , expires_at timestamptz not null
+ , revoked_at timestamptz
+ , validated_at timestamptz
+ , unique(address_validation_public_id)
+);
+
+create table if not exists secd.session (
+ session_id bigserial primary key
+ , identity_id bigint not null references secd.identity(identity_id)
+ , token_hash bytea not null
+ , created_at timestamptz not null
+ , expired_at timestamptz not null
+ , revoked_at timestamptz
+ , unique(token_hash)
+);
+
+create table if not exists secd.message (
+ message_id bigserial primary key
+ , address_id bigint not null references secd.address(address_id)
+ , subject text
+ , body text
+ , template text not null
+ , template_vars jsonb not null
+ , created_at timestamptz not null
+ , sent_at timestamptz
+);
diff --git a/crates/secd/store/pg/sql/find_address.sql b/crates/secd/store/pg/sql/find_address.sql
new file mode 100644
index 0000000..5eaafbb
--- /dev/null
+++ b/crates/secd/store/pg/sql/find_address.sql
@@ -0,0 +1,8 @@
+select address_public_id
+ , type
+ , value
+ , created_at
+from secd.address
+where (($1::uuid is null) or (address_public_id = $1))
+and (($2::text is null) or (type = $2))
+and (($3::text is null) or (value = $3));
diff --git a/crates/secd/store/pg/sql/find_address_validation.sql b/crates/secd/store/pg/sql/find_address_validation.sql
new file mode 100644
index 0000000..3874994
--- /dev/null
+++ b/crates/secd/store/pg/sql/find_address_validation.sql
@@ -0,0 +1,18 @@
+select av.address_validation_public_id
+ , i.identity_public_id
+ , a.address_public_id
+ , a.type
+ , a.value
+ , a.created_at
+ , av.method
+ , av.token_hash
+ , av.code_hash
+ , av.attempts
+ , av.created_at
+ , av.expires_at
+ , av.revoked_at
+ , av.validated_at
+from secd.address_validation av
+join secd.address a using(address_id)
+left join secd.identity i using(identity_id)
+where (($1::uuid is null) or (address_validation_public_id = $1));
diff --git a/crates/secd/store/pg/sql/find_email_validation.sql b/crates/secd/store/pg/sql/find_email_validation.sql
deleted file mode 100644
index 1eb3e43..0000000
--- a/crates/secd/store/pg/sql/find_email_validation.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-select
- ev.email_validation_public_id
- , i.identity_public_id
- , e.address
- , ev.code
- , ev.is_oauth_derived
- , ev.created_at
- , ev.expired_at
- , ev.validated_at
- , iev.revoked_at
- , iev.deleted_at
-from secd.email_validation ev
-join secd.email e using (email_id)
-left join secd.identity_email_validation iev using (email_validation_id)
-left join secd.identity i using (identity_id)
-where (($1 is null) or (email_validation_public_id = $1))
-and (($2 is null) or (code = $2));
---
diff --git a/crates/secd/store/pg/sql/find_identity.sql b/crates/secd/store/pg/sql/find_identity.sql
index 135ff9a..37105cb 100644
--- a/crates/secd/store/pg/sql/find_identity.sql
+++ b/crates/secd/store/pg/sql/find_identity.sql
@@ -1,11 +1,15 @@
-select
- identity_public_id
- , data
- , i.created_at
- , i.deleted_at
-from secd.identity i
-join secd.identity_email_validation iev using (identity_id)
-join secd.email_validation ev using (email_validation_id)
-join secd.email e using (email_id)
-where (($1 is null) or (i.identity_public_id = $1))
-and (($2 is null) or (e.address = $2));
+select distinct
+ identity_public_id
+ , data::text
+ , i.created_at
+ , i.updated_at
+ , i.deleted_at
+from secd.identity i
+left join secd.address_validation av using (identity_id)
+left join secd.address a using (address_id)
+left join secd.session s using (identity_id)
+where (($1::uuid is null) or (i.identity_public_id = $1))
+and (($2::text is null) or (a.value = $2))
+and (($3::bool is null) or (($3::bool is true) and (av.validated_at is not null)))
+and (($4::bytea is null) or (s.token_hash = $4))
+and i.deleted_at is null;
diff --git a/crates/secd/store/pg/sql/find_identity_by_code.sql b/crates/secd/store/pg/sql/find_identity_by_code.sql
deleted file mode 100644
index e5a0970..0000000
--- a/crates/secd/store/pg/sql/find_identity_by_code.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-select identity_email_validation_id
-from secd.email_validation
-where email_validation_public_id = $1::uuid
---
-select
- identity_public_id
- , data
- , i.created_at
-from secd.identity i
-left join secd.identity_email ie using (identity_id)
-where ie.identity_email_validation_id = $1;
diff --git a/crates/secd/store/pg/sql/find_session.sql b/crates/secd/store/pg/sql/find_session.sql
new file mode 100644
index 0000000..ca58480
--- /dev/null
+++ b/crates/secd/store/pg/sql/find_session.sql
@@ -0,0 +1,11 @@
+select distinct
+ i.identity_public_id
+ , s.created_at
+ , s.expired_at
+ , s.revoked_at
+from secd.session s
+join secd.identity i using (identity_id)
+where (($1::bytea is null) or (s.token_hash = $1))
+and (($2::uuid is null) or (i.identity_public_id = $2))
+and (($3::timestamptz is null) or (s.expired_at > $3))
+and ((revoked_at is null) or ($4::timestamptz is null) or (s.revoked_at > $4));
diff --git a/crates/secd/store/pg/sql/read_email_raw_id.sql b/crates/secd/store/pg/sql/read_email_raw_id.sql
deleted file mode 100644
index 6604fb0..0000000
--- a/crates/secd/store/pg/sql/read_email_raw_id.sql
+++ /dev/null
@@ -1 +0,0 @@
-select email_id from secd.email where address = $1
diff --git a/crates/secd/store/pg/sql/read_identity.sql b/crates/secd/store/pg/sql/read_identity.sql
deleted file mode 100644
index e69de29..0000000
--- a/crates/secd/store/pg/sql/read_identity.sql
+++ /dev/null
diff --git a/crates/secd/store/pg/sql/read_identity_raw_id.sql b/crates/secd/store/pg/sql/read_identity_raw_id.sql
deleted file mode 100644
index 5b5d95c..0000000
--- a/crates/secd/store/pg/sql/read_identity_raw_id.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-select identity_id from secd.identity where identity_public_id = $1;
---
diff --git a/crates/secd/store/pg/sql/read_oauth_provider.sql b/crates/secd/store/pg/sql/read_oauth_provider.sql
deleted file mode 100644
index edaa114..0000000
--- a/crates/secd/store/pg/sql/read_oauth_provider.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-select flow
- , base_url
- , response_type
- , default_scope
- , client_id
- , client_secret
- , redirect_url
- , created_at
- , deleted_at
-from secd.oauth_provider
-where name = $1
-and flow = $2;
diff --git a/crates/secd/store/pg/sql/read_oauth_validation.sql b/crates/secd/store/pg/sql/read_oauth_validation.sql
deleted file mode 100644
index d8361ea..0000000
--- a/crates/secd/store/pg/sql/read_oauth_validation.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-select oauth_validation_public_id
- , i.identity_public_id
- , ov.access_token
- , ov.raw_response
- , ov.created_at
- , ov.validated_at
- , iov.revoked_at
- , iov.deleted_at
- , op.name as oauth_provider_name
- , op.flow as oauth_provider_flow
- , op.base_url as oauth_provider_base_url
- , op.response_type as oauth_provider_response_type
- , op.default_scope as oauth_provider_default_scope
- , op.client_id as oauth_provider_client_id
- , op.client_secret as oauth_provider_client_secret
- , op.redirect_url as oauth_provider_redirect_url
- , op.created_at as oauth_provider_created_at
- , op.deleted_at as oauth_provider_deleted_at
-from secd.oauth_validation ov
-join secd.oauth_provider op using(oauth_provider_id)
-left join secd.identity_oauth_validation iov using(oauth_validation_id)
-left join secd.identity i using(identity_id)
-where oauth_validation_public_id = $1;
diff --git a/crates/secd/store/pg/sql/read_session.sql b/crates/secd/store/pg/sql/read_session.sql
deleted file mode 100644
index b1f98d4..0000000
--- a/crates/secd/store/pg/sql/read_session.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-select
- i.identity_public_id
- , s.created_at
- , s.expired_at
- , s.revoked_at
-from secd.session s
-join secd.identity i using (identity_id)
-where secret_hash = $1;
diff --git a/crates/secd/store/pg/sql/read_validation_type.sql b/crates/secd/store/pg/sql/read_validation_type.sql
deleted file mode 100644
index 2eceb98..0000000
--- a/crates/secd/store/pg/sql/read_validation_type.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-select 'Email'
-from secd.email_validation
-where email_validation_public_id = $1
-union
-select 'Oauth'
-from secd.oauth_validation
-where oauth_validation_public_id = $1;
diff --git a/crates/secd/store/pg/sql/write_address.sql b/crates/secd/store/pg/sql/write_address.sql
new file mode 100644
index 0000000..da1bf3a
--- /dev/null
+++ b/crates/secd/store/pg/sql/write_address.sql
@@ -0,0 +1,8 @@
+insert into secd.address (
+ address_public_id
+ , type
+ , value
+ , created_at
+) values (
+ $1, $2, $3, $4
+);
diff --git a/crates/secd/store/pg/sql/write_address_validation.sql b/crates/secd/store/pg/sql/write_address_validation.sql
new file mode 100644
index 0000000..3be830e
--- /dev/null
+++ b/crates/secd/store/pg/sql/write_address_validation.sql
@@ -0,0 +1,27 @@
+insert into secd.address_validation (
+ address_validation_public_id
+ , identity_id
+ , address_id
+ , "method"
+ , token_hash
+ , code_hash
+ , attempts
+ , created_at
+ , expires_at
+ , revoked_at
+ , validated_at
+) values(
+ $1
+ , (
+ select identity_id from secd.identity where identity_public_id = $2
+ )
+ , (
+ select address_id from secd.address where address_public_id = $3
+ )
+ , $4, $5, $6, $7, $8, $9, $10, $11
+) on conflict (address_validation_public_id) do update
+ set identity_id = excluded.identity_id
+ , attempts = excluded.attempts
+ , revoked_at = excluded.revoked_at
+ , validated_at = excluded.validated_at
+returning (xmax = 0);
diff --git a/crates/secd/store/pg/sql/write_email.sql b/crates/secd/store/pg/sql/write_email.sql
deleted file mode 100644
index 06a1dc5..0000000
--- a/crates/secd/store/pg/sql/write_email.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-insert into secd.email (
- address
-) values (
- $1
-) on conflict (address) do nothing
-returning email_id;
diff --git a/crates/secd/store/pg/sql/write_email_validation.sql b/crates/secd/store/pg/sql/write_email_validation.sql
deleted file mode 100644
index ff25b87..0000000
--- a/crates/secd/store/pg/sql/write_email_validation.sql
+++ /dev/null
@@ -1,43 +0,0 @@
-insert into secd.email_validation
- (
- email_validation_public_id
- , email_id
- , code
- , is_oauth_derived
- , created_at
- , validated_at
- , expired_at
- )
-values (
- $1
- , $2
- , $3
- , $4
- , $5
- , $6
- , $7
-) on conflict (email_validation_public_id) do update
- set validated_at = excluded.validated_at
- , expired_at = excluded.expired_at;
---
-insert into secd.identity_email_validation (
- identity_id
- , email_validation_id
- , revoked_at
- , deleted_at
-) values (
- (
- select identity_id
- from secd.identity
- where identity_public_id = $1
- )
- , (
- select email_validation_id
- from secd.email_validation
- where email_validation_public_id = $2
- )
- , $3
- , $4
-) on conflict (identity_id, email_validation_id) do update
- set revoked_at = excluded.revoked_at
- , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql
index 94a51fe..67662a6 100644
--- a/crates/secd/store/pg/sql/write_identity.sql
+++ b/crates/secd/store/pg/sql/write_identity.sql
@@ -1,11 +1,12 @@
insert into secd.identity (
- identity_public_id,
- data,
- created_at
+ identity_public_id
+ , data
+ , created_at
+ , updated_at
+ , deleted_at
) values (
- $1,
- $2,
- $3
-) on conflict(identity_public_id) do update
+ $1, $2::jsonb, $3, $4, $5
+) on conflict (identity_public_id) do update
set data = excluded.data
+ , updated_at = excluded.updated_at
, deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_oauth_provider.sql b/crates/secd/store/pg/sql/write_oauth_provider.sql
deleted file mode 100644
index ba69857..0000000
--- a/crates/secd/store/pg/sql/write_oauth_provider.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-insert into secd.oauth_provider (
- oauth_provider_id
- , name
- , flow
- , base_url
- , response_type
- , default_scope
- , client_id
- , client_secret
- , redirect_url
- , created_at
- , deleted_at
-) values (
- default
- , $1
- , $2
- , $3
- , $4
- , $5
- , $6
- , $7
- , $8
- , $9
- , $10
-) on conflict (name, flow) do nothing;
diff --git a/crates/secd/store/pg/sql/write_oauth_validation.sql b/crates/secd/store/pg/sql/write_oauth_validation.sql
deleted file mode 100644
index 11f2578..0000000
--- a/crates/secd/store/pg/sql/write_oauth_validation.sql
+++ /dev/null
@@ -1,45 +0,0 @@
-insert into secd.oauth_validation (
- oauth_validation_public_id
- , oauth_provider_id
- , access_token
- , raw_response
- , created_at
- , validated_at
-) values (
- $1
- , (
- select oauth_provider_id
- from secd.oauth_provider
- where name = $2
- and flow = $3
- )
- , $4
- , $5
- , $6
- , $7
-) on conflict (oauth_validation_public_id) do update
- set access_token = excluded.access_token
- , validated_at = excluded.validated_at
- , raw_response = excluded.raw_response;
---
-insert into secd.identity_oauth_validation (
- identity_id
- , oauth_validation_id
- , revoked_at
- , deleted_at
-) values (
- (
- select identity_id
- from secd.identity
- where identity_public_id = $1
- )
- , (
- select oauth_validation_id
- from secd.oauth_validation
- where oauth_validation_public_id = $2
- )
- , $3
- , $4
-) on conflict (identity_id, oauth_validation_id) do update
- set revoked_at = excluded.revoked_at
- , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_session.sql b/crates/secd/store/pg/sql/write_session.sql
index 1b238c6..18dc1f1 100644
--- a/crates/secd/store/pg/sql/write_session.sql
+++ b/crates/secd/store/pg/sql/write_session.sql
@@ -1,15 +1,10 @@
insert into secd.session (
identity_id
- , secret_hash
+ , token_hash
, created_at
, expired_at
, revoked_at
) values (
- (select identity_id from secd.identity where identity_public_id = $1)
- , $2
- , $3
- , $4
- , $5
-) on conflict (secret_hash) do update
- set revoked_at = excluded.revoked_at;
---
+ (select identity_id from secd.identity where identity_public_id = $1)
+ , $2, $3, $4, $5
+);
diff --git a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
index a8784f5..299f282 100644
--- a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
+++ b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
@@ -1,82 +1,81 @@
-create table if not exists identity (
- identity_id integer primary key autoincrement
- , identity_public_id uuid
- , data text
- , created_at timestamptz not null
- , deleted_at timestamptz
- , unique(identity_public_id)
+create table if not exists realm (
+ realm_id integer primary key
+ , created_at integer not null
);
-create table if not exists session (
- session_id integer primary key autoincrement
- , identity_id bigint not null references identity(identity_id)
- , secret_hash bytea not null
- , created_at timestamptz not null
- , expired_at timestamptz
- , revoked_at timestamptz
- , unique(secret_hash)
+create table if not exists realm_data (
+ realm_data_id integer primary key
+ , realm_id integer not null references realm(realm_id)
+ , email_provider text not null
+ , sms_provider text not null
+ , created_at integer not null
+ , deleted_at integer
);
-create table if not exists oauth_provider (
- oauth_provider_id integer primary key autoincrement
- , name text not null
- , flow text not null
- , base_url text not null
- , response_type text not null
- , default_scope text
- , client_id text not null
- , client_secret text not null
- , redirect_url text not null
- , created_at timestamptz not null
- , deleted_at timestamptz
- , unique (name, flow)
+create table if not exists identity (
+ identity_id integer primary key
+ , identity_public_id uuid not null
+ , data text -- some things are dervied, others are not
+ , created_at integer not null
+ , updated_at integer not null
+ , deleted_at integer
+ , unique(identity_public_id)
);
-create table if not exists oauth_validation (
- oauth_validation_id integer primary key autoincrement
- , oauth_validation_public_id uuid not null
- , oauth_provider_id integer not null references oauth_provider(oauth_provider_id)
- , access_token text
- , raw_response text
- , created_at timestamptz not null
- , validated_at timestamptz
- , unique (oauth_validation_public_id)
+create table if not exists credential (
+ credential_id integer primary key
+ , credential_public_id uuid not null
+ , identity_id integer not null references identity(identity_id)
+ , type text not null-- e.g. password, oidc, totop, lookup_secret, webauthn, ...
+ , data text not null
+ , version integer not null
+ , created_at integer not null
+ , revoked_at integer
+ , deleted_at integer
);
-create table if not exists identity_oauth_validation (
- identity_oauth_validation_id integer primary key autoincrement
- -- A validation does not require an identity to initiate
- , identity_id bigint references identity(identity_id)
- , oauth_validation_id bigint not null references oauth_validation(oauth_validation_id)
- , revoked_at timestamptz
- , deleted_at timestamptz
- , unique(identity_id, oauth_validation_id)
+create table if not exists address (
+ address_id integer primary key
+ , address_public_id uuid not null
+ , type text not null
+ , value text not null
+ , created_at integer not null
+ , unique(value, type)
);
-create table if not exists email (
- email_id integer primary key autoincrement
- , address text not null
- , unique(address)
+create table if not exists address_validation (
+ address_validation_id integer primary key
+ , address_validation_public_id uuid not null
+ , identity_id integer references identity(identity_id)
+ , address_id integer not null references address(address_id)
+ , method text not null -- e.g. email, sms, voice, oidc
+ , token_hash blob
+ , code_hash blob
+ , attempts integer not null
+ , created_at integer not null
+ , expires_at integer not null
+ , revoked_at integer
+ , validated_at integer
+ , unique(address_validation_public_id)
);
-create table if not exists email_validation (
- email_validation_id integer primary key autoincrement
- , email_validation_public_id uuid not null
- , email_id bigint not null references email(email_id)
- , code text
- , is_oauth_derived boolean not null
- , created_at timestamptz not null
- , validated_at timestamptz
- , expired_at timestamptz
- , unique(email_validation_public_id)
+create table if not exists session (
+ session_id integer primary key
+ , identity_id integer not null references identity(identity_id)
+ , token_hash blob not null
+ , created_at integer not null
+ , expired_at integer not null
+ , revoked_at integer
+ , unique(token_hash)
);
-create table if not exists identity_email_validation (
- identity_email_validation_id integer primary key autoincrement
- -- A validation does not require an identity to initiate
- , identity_id bigint references identity(identity_id)
- , email_validation_id bigint not null references email_validation(email_validation_id)
- , revoked_at timestamptz
- , deleted_at timestamptz
- , unique(identity_id, email_validation_id)
+create table if not exists message (
+ message_id integer primary key
+ , address_id integer not null references address(address_id)
+ , subject text
+ , body text
+ , template text not null
+ , template_vars text not null
+ , created_at integer not null
+ , sent_at integer
);
diff --git a/crates/secd/store/sqlite/sql/find_address.sql b/crates/secd/store/sqlite/sql/find_address.sql
new file mode 100644
index 0000000..da1df81
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/find_address.sql
@@ -0,0 +1,8 @@
+select address_public_id
+ , type
+ , value
+ , created_at
+from address
+where (($1 is null) or (address_public_id = $1))
+and (($2 is null) or (type = $2))
+and (($3 is null) or (value = $3));
diff --git a/crates/secd/store/sqlite/sql/find_address_validation.sql b/crates/secd/store/sqlite/sql/find_address_validation.sql
new file mode 100644
index 0000000..81d2cdf
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/find_address_validation.sql
@@ -0,0 +1,18 @@
+select av.address_validation_public_id
+ , i.identity_public_id
+ , a.address_public_id
+ , a.type
+ , a.value
+ , a.created_at
+ , av.method
+ , av.token_hash
+ , av.code_hash
+ , av.attempts
+ , av.created_at
+ , av.expires_at
+ , av.revoked_at
+ , av.validated_at
+from address_validation av
+join address a using(address_id)
+left join identity i using(identity_id)
+where (($1 is null) or (address_validation_public_id = $1));
diff --git a/crates/secd/store/sqlite/sql/find_email_validation.sql b/crates/secd/store/sqlite/sql/find_email_validation.sql
deleted file mode 100644
index d7f311c..0000000
--- a/crates/secd/store/sqlite/sql/find_email_validation.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-select
- ev.email_validation_public_id
- , i.identity_public_id
- , e.address
- , ev.code
- , ev.is_oauth_derived
- , ev.created_at
- , ev.expired_at
- , ev.validated_at
- , iev.revoked_at
- , iev.deleted_at
-from email_validation ev
-join email e using (email_id)
-left join identity_email_validation iev using (email_validation_id)
-left join identity i using (identity_id)
-where ((?1 is null) or (email_validation_public_id = ?1))
-and ((?2 is null) or (code = ?2));
---
diff --git a/crates/secd/store/sqlite/sql/find_identity.sql b/crates/secd/store/sqlite/sql/find_identity.sql
index f94e7b1..1528407 100644
--- a/crates/secd/store/sqlite/sql/find_identity.sql
+++ b/crates/secd/store/sqlite/sql/find_identity.sql
@@ -1,11 +1,15 @@
-select
- identity_public_id
- , data
- , i.created_at
- , i.deleted_at
-from identity i
-join identity_email_validation iev using (identity_id)
-join email_validation ev using (email_validation_id)
-join email e using (email_id)
-where ((?1 is null) or (i.identity_public_id = ?1))
-and ((?2 is null) or (e.address = ?2));
+select distinct
+ identity_public_id
+ , data
+ , i.created_at
+ , i.updated_at
+ , i.deleted_at
+from identity i
+left join address_validation av using (identity_id)
+left join address a using (address_id)
+left join session s using (identity_id)
+where (($1 is null) or (i.identity_public_id = $1))
+and (($2 is null) or (a.value = $2))
+and (($3 is null) or (($3 is true) and (av.validated_at is not null)))
+and (($4 is null) or (s.token_hash = $4))
+and i.deleted_at is null;
diff --git a/crates/secd/store/sqlite/sql/find_identity_by_code.sql b/crates/secd/store/sqlite/sql/find_identity_by_code.sql
deleted file mode 100644
index b70a13a..0000000
--- a/crates/secd/store/sqlite/sql/find_identity_by_code.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-select identity_email_validation_id
-from email_validation
-where email_validation_public_id = $1::uuid
---
-select
- identity_public_id
- , data
- , i.created_at
-from identity i
-left join identity_email ie using (identity_id)
-where ie.identity_email_validation_id = ?1;
diff --git a/crates/secd/store/sqlite/sql/find_session.sql b/crates/secd/store/sqlite/sql/find_session.sql
new file mode 100644
index 0000000..31640dd
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/find_session.sql
@@ -0,0 +1,11 @@
+select distinct
+ i.identity_public_id
+ , s.created_at
+ , s.expired_at
+ , s.revoked_at
+from session s
+join identity i using (identity_id)
+where (($1 is null) or (s.token_hash = $1))
+and (($2 is null) or (i.identity_public_id = $2))
+and (($3 is null) or (s.expired_at > $3))
+and ((revoked_at is null) or ($4 is null) or (s.revoked_at > $4));
diff --git a/crates/secd/store/sqlite/sql/read_email_raw_id.sql b/crates/secd/store/sqlite/sql/read_email_raw_id.sql
deleted file mode 100644
index a65c717..0000000
--- a/crates/secd/store/sqlite/sql/read_email_raw_id.sql
+++ /dev/null
@@ -1 +0,0 @@
-select email_id from email where address = ?1
diff --git a/crates/secd/store/sqlite/sql/read_identity.sql b/crates/secd/store/sqlite/sql/read_identity.sql
deleted file mode 100644
index e69de29..0000000
--- a/crates/secd/store/sqlite/sql/read_identity.sql
+++ /dev/null
diff --git a/crates/secd/store/sqlite/sql/read_identity_raw_id.sql b/crates/secd/store/sqlite/sql/read_identity_raw_id.sql
deleted file mode 100644
index 2bdb718..0000000
--- a/crates/secd/store/sqlite/sql/read_identity_raw_id.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-select identity_id from identity where identity_public_id = ?1;
---
diff --git a/crates/secd/store/sqlite/sql/read_oauth_provider.sql b/crates/secd/store/sqlite/sql/read_oauth_provider.sql
deleted file mode 100644
index 5c33cf0..0000000
--- a/crates/secd/store/sqlite/sql/read_oauth_provider.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-select flow
- , base_url
- , response_type
- , default_scope
- , client_id
- , client_secret
- , redirect_url
- , created_at
- , deleted_at
-from oauth_provider
-where name = ?1
-and flow = ?2;
diff --git a/crates/secd/store/sqlite/sql/read_oauth_validation.sql b/crates/secd/store/sqlite/sql/read_oauth_validation.sql
deleted file mode 100644
index 75f5a94..0000000
--- a/crates/secd/store/sqlite/sql/read_oauth_validation.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-select oauth_validation_public_id
- , i.identity_public_id
- , ov.access_token
- , ov.raw_response
- , ov.created_at
- , ov.validated_at
- , iov.revoked_at
- , iov.deleted_at
- , op.name as oauth_provider_name
- , op.flow as oauth_provider_flow
- , op.base_url as oauth_provider_base_url
- , op.response_type as oauth_provider_response_type
- , op.default_scope as oauth_provider_default_scope
- , op.client_id as oauth_provider_client_id
- , op.client_secret as oauth_provider_client_secret
- , op.redirect_url as oauth_provider_redirect_url
- , op.created_at as oauth_provider_created_at
- , op.deleted_at as oauth_provider_deleted_at
-from oauth_validation ov
-join oauth_provider op using(oauth_provider_id)
-left join identity_oauth_validation iov using(oauth_validation_id)
-left join identity i using(identity_id)
-where oauth_validation_public_id = ?1;
diff --git a/crates/secd/store/sqlite/sql/read_session.sql b/crates/secd/store/sqlite/sql/read_session.sql
deleted file mode 100644
index c415c4c..0000000
--- a/crates/secd/store/sqlite/sql/read_session.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-select
- i.identity_public_id
- , s.created_at
- , s.expired_at
- , s.revoked_at
-from session s
-join identity i using (identity_id)
-where secret_hash = ?1;
diff --git a/crates/secd/store/sqlite/sql/read_validation_type.sql b/crates/secd/store/sqlite/sql/read_validation_type.sql
deleted file mode 100644
index cc02ead..0000000
--- a/crates/secd/store/sqlite/sql/read_validation_type.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-select 'Email'
-from email_validation
-where email_validation_public_id = ?1
-union
-select 'Oauth'
-from oauth_validation
-where oauth_validation_public_id = ?1;
diff --git a/crates/secd/store/sqlite/sql/write_address.sql b/crates/secd/store/sqlite/sql/write_address.sql
new file mode 100644
index 0000000..56dab0c
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/write_address.sql
@@ -0,0 +1,8 @@
+insert into address (
+ address_public_id
+ , type
+ , value
+ , created_at
+) values (
+ $1, $2, $3, $4
+);
diff --git a/crates/secd/store/sqlite/sql/write_address_validation.sql b/crates/secd/store/sqlite/sql/write_address_validation.sql
new file mode 100644
index 0000000..67ce916
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/write_address_validation.sql
@@ -0,0 +1,26 @@
+insert into address_validation (
+ address_validation_public_id
+ , identity_id
+ , address_id
+ , "method"
+ , token_hash
+ , code_hash
+ , attempts
+ , created_at
+ , expires_at
+ , revoked_at
+ , validated_at
+) values(
+ $1
+ , (
+ select identity_id from identity where identity_public_id = $2
+ )
+ , (
+ select address_id from address where address_public_id = $3
+ )
+ , $4, $5, $6, $7, $8, $9, $10, $11
+) on conflict (address_validation_public_id) do update
+ set identity_id = excluded.identity_id
+ , attempts = excluded.attempts
+ , revoked_at = excluded.revoked_at
+ , validated_at = excluded.validated_at;
diff --git a/crates/secd/store/sqlite/sql/write_email.sql b/crates/secd/store/sqlite/sql/write_email.sql
deleted file mode 100644
index a64aed4..0000000
--- a/crates/secd/store/sqlite/sql/write_email.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-insert into email (
- address
-) values (
- $1
-) on conflict (address) do nothing
-returning email_id;
diff --git a/crates/secd/store/sqlite/sql/write_email_validation.sql b/crates/secd/store/sqlite/sql/write_email_validation.sql
deleted file mode 100644
index d839310..0000000
--- a/crates/secd/store/sqlite/sql/write_email_validation.sql
+++ /dev/null
@@ -1,43 +0,0 @@
-insert into email_validation
- (
- email_validation_public_id
- , email_id
- , code
- , is_oauth_derived
- , created_at
- , validated_at
- , expired_at
- )
-values (
- ?1
- , ?2
- , ?3
- , ?4
- , ?5
- , ?6
- , ?7
-) on conflict (email_validation_public_id) do update
- set validated_at = excluded.validated_at
- , expired_at = excluded.expired_at;
---
-insert into identity_email_validation (
- identity_id
- , email_validation_id
- , revoked_at
- , deleted_at
-) values (
- (
- select identity_id
- from identity
- where identity_public_id = ?1
- )
- , (
- select email_validation_id
- from email_validation
- where email_validation_public_id = ?2
- )
- , ?3
- , ?4
-) on conflict (identity_id, email_validation_id) do update
- set revoked_at = excluded.revoked_at
- , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/sqlite/sql/write_identity.sql b/crates/secd/store/sqlite/sql/write_identity.sql
index 8cf46c5..aa59358 100644
--- a/crates/secd/store/sqlite/sql/write_identity.sql
+++ b/crates/secd/store/sqlite/sql/write_identity.sql
@@ -1,11 +1,12 @@
insert into identity (
- identity_public_id,
- data,
- created_at
+ identity_public_id
+ , data
+ , created_at
+ , updated_at
+ , deleted_at
) values (
- ?1,
- ?2,
- ?3
-) on conflict(identity_public_id) do update
+ $1, $2, $3, $4, $5
+) on conflict (identity_public_id) do update
set data = excluded.data
+ , updated_at = excluded.updated_at
, deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/sqlite/sql/write_oauth_provider.sql b/crates/secd/store/sqlite/sql/write_oauth_provider.sql
deleted file mode 100644
index 421caf7..0000000
--- a/crates/secd/store/sqlite/sql/write_oauth_provider.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-insert into oauth_provider (
- name
- , flow
- , base_url
- , response_type
- , default_scope
- , client_id
- , client_secret
- , redirect_url
- , created_at
- , deleted_at
-) values (
- ?1
- , ?2
- , ?3
- , ?4
- , ?5
- , ?6
- , ?7
- , ?8
- , ?9
- , ?10
-) on conflict (name, flow) do nothing;
diff --git a/crates/secd/store/sqlite/sql/write_oauth_validation.sql b/crates/secd/store/sqlite/sql/write_oauth_validation.sql
deleted file mode 100644
index ccb11aa..0000000
--- a/crates/secd/store/sqlite/sql/write_oauth_validation.sql
+++ /dev/null
@@ -1,45 +0,0 @@
-insert into oauth_validation (
- oauth_validation_public_id
- , oauth_provider_id
- , access_token
- , raw_response
- , created_at
- , validated_at
-) values (
- ?1
- , (
- select oauth_provider_id
- from oauth_provider
- where name = ?2
- and flow = ?3
- )
- , ?4
- , ?5
- , ?6
- , ?7
-) on conflict (oauth_validation_public_id) do update
- set access_token = excluded.access_token
- , validated_at = excluded.validated_at
- , raw_response = excluded.raw_response;
---
-insert into identity_oauth_validation (
- identity_id
- , oauth_validation_id
- , revoked_at
- , deleted_at
-) values (
- (
- select identity_id
- from identity
- where identity_public_id = ?1
- )
- , (
- select oauth_validation_id
- from oauth_validation
- where oauth_validation_public_id = ?2
- )
- , ?3
- , ?4
-) on conflict (identity_id, oauth_validation_id) do update
- set revoked_at = excluded.revoked_at
- , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/sqlite/sql/write_session.sql b/crates/secd/store/sqlite/sql/write_session.sql
index 480af54..4679912 100644
--- a/crates/secd/store/sqlite/sql/write_session.sql
+++ b/crates/secd/store/sqlite/sql/write_session.sql
@@ -1,15 +1,10 @@
insert into session (
identity_id
- , secret_hash
+ , token_hash
, created_at
, expired_at
, revoked_at
) values (
- (select identity_id from identity where identity_public_id = ?1)
- , ?2
- , ?3
- , ?4
- , ?5
-) on conflict (secret_hash) do update
- set revoked_at = excluded.revoked_at;
---
+ (select identity_id from identity where identity_public_id = $1)
+ , $2, $3, $4, $5
+);
diff --git a/crates/secd/tests/authn_integration.rs b/crates/secd/tests/authn_integration.rs
deleted file mode 100644
index d823d5a..0000000
--- a/crates/secd/tests/authn_integration.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-#[cfg(test)]
-mod test {
- use std::error::Error;
-
- use secd::{AuthEmail, AuthStore, Secd};
-
- #[tokio::test]
- async fn email_authentication_int() -> Result<(), Box<dyn Error>> {
- let secd = Secd::init(AuthStore::Sqlite, None, AuthEmail::LocalStub, None, None).await?;
- let v_id = secd.create_validation_request_email("b@g.com").await?;
-
- // TODO: in memory mailbox backed by sqlite which just throws them in temporarily...
- // and then I can grab it?
-
- // Things to test
- // 1. after exchanging the session, I cannot get it again
- // 1. a validation can only be used once
- // 1. a session can be used to retrieve identity information
- assert_eq!(1, 2);
- Ok(())
- }
-
- #[tokio::test]
- async fn oauth_authentication_int() -> Result<(), Box<dyn Error>> {
- let secd = Secd::init(AuthStore::Sqlite, None, AuthEmail::LocalStub, None, None).await?;
-
- // Things to test
- // 1. after exchanging the session, I cannot get it again
- // 1. a validation can only be used once
- // 1. a session can be used to retrieve identity information
- // 1. an oauth session links with an existing emails session
- assert_eq!(1, 2);
- Ok(())
- }
-}