Skip to content

Commit

Permalink
Guides
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Dec 5, 2024
1 parent a180f9b commit ea8329d
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 1 deletion.
57 changes: 57 additions & 0 deletions docs/docs/security/hashing.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions docs/docs/user-guides/.pages
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
nav:
- 'index.md'
- 'build-your-app'
- 'hot-reload.md'
- '...'
- 'deploy-to-prod.md'
- 'admin.md'
78 changes: 78 additions & 0 deletions docs/docs/user-guides/build-your-app/add-users.md
Original file line number Diff line number Diff line change
@@ -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<i64>,
username: String,
password_hash: String,
created_at: OffsetDateTime,
}
```
21 changes: 21 additions & 0 deletions docs/docs/user-guides/build-your-app/index.md
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 5 additions & 1 deletion examples/users/src/models.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<UserLogin, Error> {
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)),
Expand Down

0 comments on commit ea8329d

Please sign in to comment.