From ea8329d5376cb0a91f6648cee6f8a9fb3d945f03 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 5 Dec 2024 12:29:11 -0800 Subject: [PATCH] Guides --- docs/docs/security/hashing.md | 57 ++++++++++++++ docs/docs/user-guides/.pages | 2 + .../user-guides/build-your-app/add-users.md | 78 +++++++++++++++++++ docs/docs/user-guides/build-your-app/index.md | 21 +++++ examples/users/src/models.rs | 6 +- 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 docs/docs/security/hashing.md create mode 100644 docs/docs/user-guides/build-your-app/add-users.md create mode 100644 docs/docs/user-guides/build-your-app/index.md diff --git a/docs/docs/security/hashing.md b/docs/docs/security/hashing.md new file mode 100644 index 00000000..f301154a --- /dev/null +++ b/docs/docs/security/hashing.md @@ -0,0 +1,57 @@ +# Password hashing + +Password hashing is a technique for storing and validating user passwords without exposing what those passwords are. All modern web apps must use this technique +to store credentials, and Rwf comes with built-in support for hashing using [Argon2](https://en.wikipedia.org/wiki/Argon2). + +## Generate hashes + +A password hash can be generated using the `rwf::crypto::hash` function, for example: + +```rust +use rwf::crypto::hash; + +let hashed = hash("secret_password".as_bytes()).unwrap(); +``` + +The hash generated by this function is a Rust string; it can be saved in a database. Since Argon2 is cryptographically secure, strong passwords are reasonably protected against brute force attacks in case the hashes are leaked. + +!!! note + While hashes are hard to brute force, it's still inadvisable to allow hashes + to be easily accessible to anyone. Make every effort to protect your production database + against unauthorized access. + +## Validate passwords + +Hashes are used to check that some information the application has seen previously matches what the it's seeing now. For example, when one of your users wants to log into the application, +they will provide the application with a login and a password. The password can be validated against an existing hash, and if the two match, it's safe to assume that the password is correct. + +Passwords can be validated using the `rwf::crypto::hash_validate` function, for example: + +```rust +use rwf::crypto::hash_validate; + +let matches = hash_validate( + "secret_password".as_bytes(), + &hashed, +).unwrap(); +``` + +## Using with Tokio + +You'll note that both `hash` and `hash_validate` functions are slow. In fact, it can take upwards a second to generate or validate a hash. This is done on purpose, to make hashes hard to brute force. +To avoid blocking the Tokio runtime and slowing down your application, make sure to use both functions inside blocking tasks: + +```rust +use tokio::task::spawn_blocking; + +let hashed = spawn_blocking(move || { + hash("secret_password".as_bytes()) +}) +.await +.unwrap() +.unwrap(); +``` + +## Learn more + +- [examples/users](https://github.com/levkk/rwf/tree/main/examples/users) diff --git a/docs/docs/user-guides/.pages b/docs/docs/user-guides/.pages index 622b20e6..89521218 100644 --- a/docs/docs/user-guides/.pages +++ b/docs/docs/user-guides/.pages @@ -1,5 +1,7 @@ nav: - 'index.md' + - 'build-your-app' - 'hot-reload.md' + - '...' - 'deploy-to-prod.md' - 'admin.md' diff --git a/docs/docs/user-guides/build-your-app/add-users.md b/docs/docs/user-guides/build-your-app/add-users.md new file mode 100644 index 00000000..4568ddb9 --- /dev/null +++ b/docs/docs/user-guides/build-your-app/add-users.md @@ -0,0 +1,78 @@ +# Add users + +!!! note + This guide is a work-in-progress. + +Unless you're building simple demo applications or static informational websites, your web app will need a way for your users to sign up and personalize their experience. There are many ways to accomplish this, and your implementation should be specific to your use case. For example, many web apps allow users to sign up using an OAuth2 provider like [Google](https://developers.google.com/identity/protocols/oauth2) or [GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). + +In this guide, we'll cover the most popular and simple way to create user accounts: using a username and a password. + +## Username and password + +Allowing your users to create accounts in your application using a username and password is pretty universal. Implementing this system requires using all 3 components of the MVC framework: creating a database model to store usernames and password hashes, controllers to process signup and login requests, and views to serve signup and login forms. + +Rwf supports all three components natively. + +### Create the model + +To create a model in Rwf, you need to define the schema in the database and define the model in Rust code. The two should match as closely as possible. + +#### Create the schema + +Starting with the data model, let's create a simple `"users"` table in your database. This table will store usernames, password hashes, and other metadata about our users, like when their accounts were created. + +Creating a table with Rwf should be done by writing a [migration](../../models/migrations.md). This makes sure changes to the database schema are documented and deterministic. To create a migration, use the Rwf CLI: + +=== "Command" + ``` + rwf-cli migrate add -n users + ``` +=== "Output" + ``` + Created "migrations/1733265254409864495_users.up.sql" + Created "migrations/1733265254409864495_users.down.sql" + ``` + +The migration is empty, so let's create the table by adding it to the `users.up.sql` file: + +```postgresql +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR NOT NULL UNIQUE, + password_hash VARCHAR NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +As mentioned above, our table will store usernames, password hashes, and metadata. The `id` column is the primary key of this table, allowing us to identify our users using a unique number. + +!!! note + Rwf models by default expect the presence of the `id` column, and use it as the primary key. + This is configurable on a per-model basis, and models can be created without a primary key, + but this will prevent them from being updated or deleted by the ORM. + +Once the schema is ready, create the table in the database by applying the migration: + +=== "Command" + ``` + rwf-cli migrate run + ``` +=== "Output" + ``` + applying migration "1733265254409864495_users" + migration "1733265254409864495_users" applied + ``` + +#### Define the Rust model + +With the schema ready to go, we need to create a Rust struct which we'll use in code to reference the model records. The Rust struct should have the same fields as the columns in our table, and their data types should match as well: + +```rust +#[derive(Clone, macros::Model)] +pub struct User { + id: Option, + username: String, + password_hash: String, + created_at: OffsetDateTime, +} +``` diff --git a/docs/docs/user-guides/build-your-app/index.md b/docs/docs/user-guides/build-your-app/index.md new file mode 100644 index 00000000..39dd9a50 --- /dev/null +++ b/docs/docs/user-guides/build-your-app/index.md @@ -0,0 +1,21 @@ +# Build with Rwf + +Rwf has a lot of features and mixing them together can create powerful and efficient web apps. This guide will demonstrate how +to build a generic application from scratch using Rwf as your web framework. + +!!! note + This guide is a work in progress. Please check back soon for more updates. [Contributions](https://github.com/levkk/rwf/tree/main/docs/docs/user-guides/build-your-app) are welcome! + +## Getting started + +If you'd like to build an application with this guide, make sure make sure to install the Rwf CLI first: + +``` +cargo install rwf-cli +``` + +Once the CLI is installed, make sure to follow the [instructions](../../index.md) on creating a new Rwf application. + +## Chapters + +1. [Add users](add-users.md) diff --git a/examples/users/src/models.rs b/examples/users/src/models.rs index 58f58a95..166096d0 100644 --- a/examples/users/src/models.rs +++ b/examples/users/src/models.rs @@ -1,6 +1,7 @@ // use rwf::model::Error; use rwf::crypto::{hash, hash_validate}; use rwf::prelude::*; +use tokio::task::spawn_blocking; pub enum UserLogin { NoSuchUser, @@ -19,7 +20,10 @@ pub struct User { impl User { /// Create new user with email and password. pub async fn signup(email: &str, password: &str) -> Result { - let encrypted_password = hash(password.as_bytes())?; + let hash_password = password.to_owned(); + let encrypted_password = spawn_blocking(move || hash(hash_password.as_bytes())) + .await + .unwrap()?; match Self::login(email, password).await? { UserLogin::Ok(user) => return Ok(UserLogin::Ok(user)),