Nivethan
Posted on October 25, 2020
Welcome back! I hope you had a well deserved break. Or you came straight here, in which case, coughnerdcough. At this point I hope that you can see the forest that is our application and can see how the major pieces fit together. Now its time to get into the weeds. In this chapter we're going to fix one of our most glaring mistakes. Our passwords. We currently save plaintext passwords to our database. Let's fix this!
What we want to do is hash the passwords we get when a user registers for the first time. We don't want to save unhashed passwords because if our database falls into the wrong hands, then they still shouldn't be able to compromise our users.
We will use the argonautica rust crate to do our password hashing and as a rule you will always be using a respected library to do any sort of password hashing.
Hashing Passwords
The first step to make our password storage better is to include the argonautica crate.
./Cargo.toml
...
chrono = { version = "0.4", features = ["serde"] }
argonautica = "0.2"
The first thing we need to do is a add a function to our NewUser struct in our models.rs file. This is because we want to do our password hashing in our model.
Before we start we need to add 2 new includes to our models.
./src/models.rs
use dotenv::dotenv;
use argonautica::Hasher;
We will use dotenv to load in our secret key which we will then use in our Hasher which we get from argonautica.
./src/models.rs
...
#[derive(Debug, Deserialize, Insertable)]
#[table_name="users"]
pub struct NewUser {
pub username: String,
pub email: String,
pub password: String,
}
impl NewUser {
pub fn new(username: String, email: String, password: String) -> Self {
dotenv().ok();
let secret = std::env::var("SECRET_KEY")
.expect("SECRET_KEY must be set");
let hash = Hasher::default()
.with_password(password)
.with_secret_key(secret)
.hash()
.unwrap();
NewUser {
username: username,
email: email,
password: hash,
}
}
}
...
Here we add a new constructor that will take in user, email and password and return a new NewUser object. Inside our new function we load in our .env file which we still need to add a SECRET_KEY to and then we load SECRET_KEY.
Next we run our Hasher .hash function against our password using our key.
Finally we return a NewUser object with our hashed password.
./.env
DATABASE_URL=postgres://postgres:postgres@localhost/hackerclone
SECRET_KEY="THIS IS OUR SUPER SUPER SUPER SUPER SECRET KEY"
The secret key should be a randomly generated string. We will use this again when we go to compare a password to a hash.
Now we can go back to our main.rs file and update our process_signup function.
./src/main.rs
...
async fn process_signup(data: web::Form<NewUser>) -> impl Responder {
use schema::users;
let connection = establish_connection();
let new_user = NewUser::new(data.username.clone(), data.email.clone(), data.password.clone());
diesel::insert_into(users::table)
.values(&new_user)
.get_result::<User>(&connection)
.expect("Error registering used.");
println!("{:?}", data);
HttpResponse::Ok().body(format!("Successfully saved user: {}", data.username))
}
...
Now, instead of inserting the data we extracted from our request, we will use the extracted data to build a NewUser object using the constructor. This way it will run the password through our hash function.
We then insert this NewUser object instead and from this point on we will not be saving passwords in our database.
! Almost there, the next piece we need to update is our login function.
./src/main.rs
if u.password == data.password {
let session_token = String::from(u.username);
id.remember(session_token);
HttpResponse::Ok().body(format!("Logged in: {}", data.username))
} else {
HttpResponse::Ok().body("Password is incorrect.")
}
Currently we do a straight comparison of our password with what we have in our database. Now that we've hashed the password in the database, we need to do the same thing when we got to compare them.
./src/main.rs
...
match user {
Ok(u) => {
dotenv().ok();
let secret = std::env::var("SECRET_KEY")
.expect("SECRET_KEY must be set");
let valid = Verifier::default()
.with_hash(u.password)
.with_password(data.password.clone())
.with_secret_key(secret)
.verify()
.unwrap();
if valid {
let session_token = String::from(u.username);
id.remember(session_token);
HttpResponse::Ok().body(format!("Logged in: {}", data.username))
} else {
HttpResponse::Ok().body("Password is incorrect.")
}
},
Err(e) => {
println!("{:?}", e);
HttpResponse::Ok().body("User doesn't exist.")
}
}
...
Now our process_login function will use the Verifier in argonautica. Similar to how we did the Hasher, we will first use dotenv to load in the environment variables.
We then read in the secret key and run the verify function against the password hash and the password we received from the user. If the verify function succeeds, then it means the user has entered the correct password and we can log them in.
Voila! We have now fixed our passwords! At this point we have broken our existing users as our new comparison function will be comparing a hashed password against an unhashed one. You will need to create new users to test against.
In the next chapter we'll look at setting up connection pooling!
Posted on October 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.