From ed34a5251f13bbded0aa15719887db4924b351eb Mon Sep 17 00:00:00 2001 From: benj Date: Mon, 22 May 2023 15:47:06 -0700 Subject: update credential API to include sessions This change updates the credential API to include sessions as just another credential type. It adds the ApiToken type and enables revocation of credentials. Updates were also made to the Identity API which now includes a list of new credentials added to an Identity. This change also migrates off the hacky ENV configuration paradigm and includes a new config.toml file specified by the SECD_CONFIG_PATH env var. No default is currently provided. Clippy updates and code cleanup. --- crates/secd/src/lib.rs | 170 +++++++++++++++++++++++-------------------------- 1 file changed, 81 insertions(+), 89 deletions(-) (limited to 'crates/secd/src/lib.rs') diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs index 54759a5..eb5d33d 100644 --- a/crates/secd/src/lib.rs +++ b/crates/secd/src/lib.rs @@ -12,37 +12,26 @@ use client::{ Store, StoreError, }, }; +use config::Config; use email_address::EmailAddress; -use lettre::message::Mailbox; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::{ - env::{set_var, var}, - str::FromStr, - sync::Arc, +use serde_with::{ + base64::{Base64, UrlSafe}, + formats::Unpadded, + serde_as, }; +use std::{env::var, fs::read_to_string, str::FromStr, sync::Arc}; use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use util::crypter::{Crypter, CrypterError}; use uuid::Uuid; -pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING"; -pub const ENV_CRYPTER_SECRET_KEY: &str = "SECD_CRYPTER_SECRET_KEY"; -pub const ENV_EMAIL_ADDRESS_FROM: &str = "SECD_EMAIL_ADDRESS_FROM"; -pub const ENV_EMAIL_ADDRESS_REPLYTO: &str = "SECD_EMAIL_ADDRESS_REPLYTO"; -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"; -pub const ENV_EMAIL_SENDGRID_API_KEY: &str = "SECD_EMAIL_SENDGRID_API_KEY"; -pub const ENV_EMAIL_SIGNIN_MESSAGE: &str = "SECD_EMAIL_SIGNIN_MESSAGE"; -pub const ENV_EMAIL_SIGNUP_MESSAGE: &str = "SECD_EMAIL_SIGNUP_MESSAGE"; -pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET"; -pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER"; - const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6; const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5; const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true; +const API_TOKEN_SIZE_BYTES: usize = 64; +const CREDENTIAL_PUBLIC_PART_BYTES: usize = 16; const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t"; const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; @@ -55,7 +44,6 @@ pub type IdentityId = Uuid; pub type MotifId = Uuid; pub type PhoneNumber = String; pub type RefId = Uuid; -pub type SessionToken = String; #[derive(Debug, derive_more::Display, thiserror::Error)] pub enum SecdError { @@ -67,11 +55,14 @@ pub enum SecdError { AddressValidationExpiredOrConsumed, CredentialAlreadyExists, + InvalidCredential, + CredentialIsNotApiToken, CrypterError(#[from] CrypterError), TooManyIdentities, IdentityNotFound, + IdentityAlreadyExists, EmailMessengerError(#[from] EmailMessengerError), InvalidEmaillAddress(#[from] email_address::Error), @@ -79,10 +70,13 @@ pub enum SecdError { FailedToProvideSessionIdentity(String), InvalidSession, + ParseAssetFileError(String), + StoreError(#[from] StoreError), StoreInitFailure(String), FailedToDecodeInput(#[from] hex::FromHexError), + DecodeError(String), AuthorizationNotSupported(String), SpiceClient(#[from] SpiceError), @@ -97,11 +91,20 @@ pub struct Secd { cfg: Cfg, } +#[derive(Debug, Deserialize)] struct Cfg { - email_address_from: Option, - email_address_replyto: Option, - email_signup_message: Option, + auth_store_conn: String, + crypter_secret_key: Option, + email_address_from: Option, + email_address_replyto: Option, + email_messenger: Option, + email_sendgrid_api_key: Option, + email_signin_message_asset_loc: Option, + email_signup_message_asset_loc: Option, email_signin_message: Option, + email_signup_message: Option, + spice_secret: String, + spice_server: String, } #[async_trait] @@ -191,9 +194,9 @@ pub struct AddressValidation { #[serde(with = "time::serde::timestamp::option")] pub validated_at: Option, pub attempts: i32, - #[serde_as(as = "serde_with::hex::Hex")] + #[serde_as(as = "Base64")] hashed_token: Vec, - #[serde_as(as = "serde_with::hex::Hex")] + #[serde_as(as = "Base64")] hashed_code: Vec, } @@ -225,38 +228,19 @@ pub struct Credential { } #[serde_as] -#[derive(Debug, Display, Serialize, Deserialize, EnumString)] +#[derive(Clone, Debug, Display, Serialize, Deserialize, EnumString)] pub enum CredentialType { + ApiToken { public: String, private: String }, Passphrase { key: String, value: String }, - Oidc { value: String }, - OneTimeCodes { codes: Vec }, - // Totp { - // #[serde_as(as = "DisplayFromStr")] - // url: Url, - // code: String, - // }, - WebAuthn { value: String }, -} - -struct SecuredCredential { - pub id: CredentialId, - pub identity_id: IdentityId, - pub t: CredentialType, - pub created_at: OffsetDateTime, - pub revoked_at: Option, - pub deleted_at: Option, -} - -enum SecuredCredentialType { - Passphrase { key: String, value: String }, - Oidc { value: String }, - OneTimeCodes { codes: Vec }, + Session { key: String, secret: String }, + // Oidc { key: String, value: String }, + // OneTimeCodes { key: String, codes: Vec }, // Totp { // #[serde_as(as = "DisplayFromStr")] // url: Url, // code: String, // }, - WebAuthn { value: String }, + // WebAuthn { value: String }, } #[serde_with::skip_serializing_none] @@ -265,6 +249,8 @@ pub struct Identity { pub id: IdentityId, pub address_validations: Vec, pub credentials: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub new_credentials: Vec, pub rules: Vec, // TODO: rules for (e.g. mfa reqs) pub metadata: Option, #[serde(with = "time::serde::timestamp")] @@ -273,20 +259,22 @@ pub struct Identity { pub deleted_at: Option, } -#[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, - #[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, +impl Cfg { + fn resolve(&mut self) -> Result<(), SecdError> { + if let Some(path) = &self.email_signin_message_asset_loc { + self.email_signin_message = Some(read_to_string(path).map_err(|err| { + SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err)) + })?); + } + + if let Some(path) = &self.email_signup_message_asset_loc { + self.email_signup_message = Some(read_to_string(path).map_err(|err| { + SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err)) + })?); + } + + Ok(()) + } } impl Secd { @@ -294,16 +282,28 @@ impl Secd { /// /// Initialize SecD with the specified configuration, established the necessary /// constraints, persistance stores, and options. - pub async fn init(z_schema: Option<&str>) -> Result { - let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok()); + pub async fn init(cfg_path: Option<&str>, z_schema: Option<&str>) -> Result { + let mut cfg: Cfg = Config::builder() + .add_source(config::File::with_name(cfg_path.unwrap_or( + &var("SECD_CONFIG_PATH").expect("coud not read SECD_CONFIG_PATH from environment"), + ))) + .build() + .unwrap() + .try_deserialize() + .expect("failed to retrieve secd config.toml"); + + cfg.resolve()?; + + let auth_store = AuthStore::from(Some(cfg.auth_store_conn.clone())); let email_messenger = AuthEmailMessenger::from_str( - &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()), + &cfg.email_messenger + .clone() + .unwrap_or(AuthEmailMessenger::Local.to_string()), ) .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5"); - let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| { + let crypter_secret_key = cfg.crypter_secret_key.clone().unwrap_or_else(|| { warn!( - "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE ENVIORNMENT VARIABLE {}. THE DEFAULT KEY IS: {}", - ENV_CRYPTER_SECRET_KEY, + "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE CONFIGURATION KEY. THE DEFAULT KEY IS: {}", CRYPTER_SECRET_KEY_DEFAULT, ); CRYPTER_SECRET_KEY_DEFAULT.to_string() @@ -312,17 +312,6 @@ impl Secd { info!("starting client with auth_store: {:?}", auth_store); info!("starting client with email_messenger: {:?}", auth_store); - let cfg = Cfg { - email_address_from: var(ENV_EMAIL_ADDRESS_FROM) - .ok() - .and_then(|s| s.parse().ok()), - email_address_replyto: var(ENV_EMAIL_ADDRESS_REPLYTO) - .ok() - .and_then(|s| s.parse().ok()), - email_signup_message: var(ENV_EMAIL_SIGNUP_MESSAGE).ok(), - email_signin_message: var(ENV_EMAIL_SIGNIN_MESSAGE).ok(), - }; - let store = match auth_store { AuthStore::Sqlite { conn } => { if z_schema.is_some() { @@ -331,7 +320,7 @@ impl Secd { )); } - SqliteClient::new( + SqliteClient::new_ref( sqlx::sqlite::SqlitePoolOptions::new() .connect(&conn) .await @@ -342,7 +331,7 @@ impl Secd { .await } AuthStore::Postgres { conn } => { - PgClient::new( + PgClient::new_ref( sqlx::postgres::PgPoolOptions::new() .connect(&conn) .await @@ -352,7 +341,7 @@ impl Secd { ) .await } - rest @ _ => { + rest => { error!( "requested an AuthStore which has not yet been implemented: {:?}", rest @@ -362,19 +351,22 @@ impl Secd { }; let email_sender = match email_messenger { - AuthEmailMessenger::Local => LocalMailer::new(), - AuthEmailMessenger::Sendgrid => Sendgrid::new( - var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"), + AuthEmailMessenger::Local => LocalMailer::new_ref(), + AuthEmailMessenger::Sendgrid => Sendgrid::new_ref( + cfg.email_sendgrid_api_key + .clone() + .expect("No SENDGRID_API_KEY provided"), ), _ => unimplemented!(), }; let spice = match z_schema { Some(schema) => { - let c: Arc = Arc::new(Spice::new().await); + let c: Arc = + Arc::new(Spice::new(cfg.spice_secret.clone(), cfg.spice_server.clone()).await); c.write_schema(schema) .await - .expect("failed to write authorization schema".into()); + .unwrap_or_else(|_| panic!("{}", "failed to write authorization schema")); Some(c) } None => None, -- cgit v1.2.3