From b2e843290d8985344e3c5643d80b5212629a8589 Mon Sep 17 00:00:00 2001 From: Cole MacKenzie Date: Mon, 27 Jan 2025 22:04:42 -0800 Subject: [PATCH] feat: add create_user to AuthPlugin trait Moving the user table into `torii-core` with a minimum set of fields that all plugins should be safe using. I also added a new enum (only one variant right now) to aid in the creation of users via different plugins. This is preferred over expanding the user struct. Password hashing is now down via https://docs.rs/password-auth/latest/password_auth/ which uses argon2 by default. --- README.md | 13 +++++++++ torii-auth-email/Cargo.toml | 3 +- torii-auth-email/src/lib.rs | 40 ++++++++++++++++++++++---- torii-auth-email/src/migrations/mod.rs | 17 +++++------ torii-core/src/plugin.rs | 33 +++++++++++++++++++-- torii/Cargo.toml | 2 +- 6 files changed, 89 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 501f9d7..60b5d9c 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,25 @@ Torii is currently under active development. As we are in the initial phases, many features are experimental and may be significantly modified or removed in future versions. +The goal of Torii is to provide a simple and flexible authentication system for web applications through plugins. + ## Development Status - 🚧 Early Development - ⚠️ Not Production Ready - 📝 APIs Subject to Change +## Current Plugins + +- [Password Auth](./torii-auth-email/README.md) +- OAuth 2.0 Auth (coming soon) +- OpenID Connect Auth (coming soon) +- WebAuthn Auth (coming soon) + +## Disclaimer + +This project is not production ready and should not be used in production environments. The maintainers are not responsible for any data loss or other issues that may arise from using this software. + ## Contributing As this project is in its early stages, we welcome discussions and feedback, but please note that major changes may occur. diff --git a/torii-auth-email/Cargo.toml b/torii-auth-email/Cargo.toml index 8502093..80c522e 100644 --- a/torii-auth-email/Cargo.toml +++ b/torii-auth-email/Cargo.toml @@ -9,4 +9,5 @@ license.workspace = true torii-core = { path = "../torii-core" } async-trait.workspace = true sqlx.workspace = true -thiserror.workspace = true \ No newline at end of file +thiserror.workspace = true +password-auth = { version = "1", features = ["argon2"] } diff --git a/torii-auth-email/src/lib.rs b/torii-auth-email/src/lib.rs index 88311eb..d464fce 100644 --- a/torii-auth-email/src/lib.rs +++ b/torii-auth-email/src/lib.rs @@ -1,10 +1,12 @@ mod migrations; use async_trait::async_trait; +use password_auth::{generate_hash, verify_password}; use sqlx::pool::Pool; use sqlx::sqlite::Sqlite; use sqlx::Row; use torii_core::migration::PluginMigration; +use torii_core::plugin::CreateUserParams; use torii_core::{AuthPlugin, Credentials, Error, User}; pub struct EmailPasswordPlugin; @@ -31,6 +33,30 @@ impl AuthPlugin for EmailPasswordPlugin { Ok(()) } + async fn create_user( + &self, + pool: &Pool, + params: &CreateUserParams, + ) -> Result<(), Error> { + let (username, password) = match params { + CreateUserParams::Password { username, password } => (username, password), + _ => return Err(Error::Auth("Unsupported create user params".into())), + }; + + let password_hash = generate_hash(password); + + sqlx::query( + r#" + INSERT INTO torii_users (username, password_hash) VALUES (?, ?) + "#, + ) + .bind(username) + .bind(password_hash) + .execute(pool) + .await?; + Ok(()) + } + async fn authenticate(&self, pool: &Pool, creds: &Credentials) -> Result { let password = creds .password @@ -39,16 +65,20 @@ impl AuthPlugin for EmailPasswordPlugin { let row = sqlx::query( r#" - SELECT id, username - FROM users - WHERE username = ? AND password_hash = ? + SELECT id, username, password_hash + FROM torii_users + WHERE username = ? "#, ) .bind(&creds.username) - .bind(password) .fetch_one(pool) .await?; + let stored_hash: String = row.get("password_hash"); + if verify_password(password, &stored_hash).is_err() { + return Err(Error::Auth("Invalid credentials".into())); + } + Ok(User { id: row.get(0), username: row.get(1), @@ -56,6 +86,6 @@ impl AuthPlugin for EmailPasswordPlugin { } fn migrations(&self) -> Vec> { - vec![Box::new(migrations::CreateUsersTable)] + vec![Box::new(migrations::AddPasswordColumn)] } } diff --git a/torii-auth-email/src/migrations/mod.rs b/torii-auth-email/src/migrations/mod.rs index ffbd18d..3199ea6 100644 --- a/torii-auth-email/src/migrations/mod.rs +++ b/torii-auth-email/src/migrations/mod.rs @@ -4,27 +4,22 @@ use torii_core::migration::PluginMigration; use torii_core::Error; // Example implementation for EmailPasswordPlugin -pub(crate) struct CreateUsersTable; +pub(crate) struct AddPasswordColumn; #[async_trait] -impl PluginMigration for CreateUsersTable { +impl PluginMigration for AddPasswordColumn { fn version(&self) -> i64 { 1 } fn name(&self) -> &str { - "create_users_table" + "add_password_column" } async fn up(&self, pool: &Pool) -> Result<(), Error> { sqlx::query( r#" - CREATE TABLE users ( - id INTEGER PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + ALTER TABLE torii_users ADD COLUMN password_hash TEXT NOT NULL; "#, ) .execute(pool) @@ -33,7 +28,9 @@ impl PluginMigration for CreateUsersTable { } async fn down(&self, pool: &Pool) -> Result<(), Error> { - sqlx::query("DROP TABLE users").execute(pool).await?; + sqlx::query("ALTER TABLE torii_users DROP COLUMN password_hash;") + .execute(pool) + .await?; Ok(()) } } diff --git a/torii-core/src/plugin.rs b/torii-core/src/plugin.rs index e767ff8..937d033 100644 --- a/torii-core/src/plugin.rs +++ b/torii-core/src/plugin.rs @@ -17,11 +17,22 @@ pub struct Credentials { pub password: Option, } +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum CreateUserParams { + Password { username: String, password: String }, +} + #[async_trait] pub trait AuthPlugin: Send + Sync { fn name(&self) -> &'static str; async fn setup(&self, pool: &Pool) -> Result<(), Error>; async fn authenticate(&self, pool: &Pool, creds: &Credentials) -> Result; + async fn create_user( + &self, + pool: &Pool, + params: &CreateUserParams, + ) -> Result<(), Error>; fn migrations(&self) -> Vec>; } @@ -94,6 +105,23 @@ impl PluginManager { Ok(()) } + async fn init_user_table(&self, pool: &Pool) -> Result<(), Error> { + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS torii_users ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(username) + ) + "#, + ) + .execute(pool) + .await?; + Ok(()) + } + async fn get_applied_migrations( &self, pool: &Pool, @@ -113,7 +141,7 @@ impl PluginManager { &self, pool: &Pool, plugin_name: &str, - migration: &Box, + migration: &dyn PluginMigration, ) -> Result<(), Error> { // Start transaction let mut tx = pool.begin().await?; @@ -136,6 +164,7 @@ impl PluginManager { pub async fn migrate(&self, pool: &Pool) -> Result<(), Error> { self.init_migration_table(pool).await?; + self.init_user_table(pool).await?; for (plugin_name, plugin) in &self.plugins { let applied = self.get_applied_migrations(pool, plugin_name).await?; @@ -145,7 +174,7 @@ impl PluginManager { .filter(|m| !applied.contains(&m.version())); for migration in pending { - self.apply_migration(pool, plugin_name, &migration).await?; + self.apply_migration(pool, plugin_name, &*migration).await?; } } Ok(()) diff --git a/torii/Cargo.toml b/torii/Cargo.toml index 850d877..c65bfde 100644 --- a/torii/Cargo.toml +++ b/torii/Cargo.toml @@ -23,4 +23,4 @@ email-auth = ["torii-auth-email"] sqlite = ["sqlx/sqlite"] postgres = ["sqlx/postgres"] # Runtime -tokio-runtime = ["tokio"] \ No newline at end of file +tokio-runtime = ["tokio"]