start implementing basic login functionality

This commit is contained in:
Ilion Beyst 2021-12-13 22:41:20 +01:00
parent 8b4440f723
commit eabeb7ed7b
14 changed files with 326 additions and 8 deletions

View file

@ -6,11 +6,18 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rocket = "0.4.10" rocket = { version= "0.5.0-rc.1", features = ["json"] }
diesel = { version = "1.4.4", features = ["postgres"] } diesel = { version = "1.4.4", features = ["postgres", "r2d2"] }
dotenv = "0.15.0" dotenv = "0.15.0"
rust-argon2 = "0.8"
rand = "0.8.4"
serde = { version = "1.0", features = ["derive"] }
serde_bytes = "0.11"
chrono = { version = "0.4", features = ["serde"] }
serde_json = "1.0"
base64 = "0.13.0"
[dependencies.rocket_contrib]
version = "0.4.10" [dependencies.rocket_sync_db_pools]
default-features = false version = "0.1.0-rc.1"
features = ["diesel_postgres_pool"] features = ["diesel_postgres_pool"]

2
backend/Rocket.toml Normal file
View file

@ -0,0 +1,2 @@
[debug.databases.postgresql_database]
url = "postgresql://planetwars:planetwars@localhost/planetwars"

View file

@ -0,0 +1 @@
DROP TABLE users;

View file

@ -0,0 +1,6 @@
CREATE TABLE users(
id SERIAL PRIMARY KEY,
username VARCHAR(52) NOT NULL,
password_salt BYTEA NOT NULL,
password_hash BYTEA NOT NULL
);

View file

@ -0,0 +1 @@
DROP TABLE sessions;

View file

@ -0,0 +1,5 @@
CREATE TABLE sessions (
id serial PRIMARY KEY,
user_id integer REFERENCES users(id) NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE
)

2
backend/src/db/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod sessions;
pub mod users;

View file

@ -0,0 +1,46 @@
use super::users::User;
use crate::schema::{sessions, users};
use base64;
use diesel::PgConnection;
use diesel::{insert_into, prelude::*, Insertable, RunQueryDsl};
use rand::{self, Rng};
#[derive(Insertable)]
#[table_name = "sessions"]
struct NewSession {
token: String,
user_id: i32,
}
#[derive(Queryable, Debug, PartialEq)]
pub struct Session {
pub id: i32,
pub user_id: i32,
pub token: String,
}
pub fn create_session(user: &User, conn: &PgConnection) -> Session {
let new_session = NewSession {
token: gen_session_token(),
user_id: user.user_id,
};
let session = insert_into(sessions::table)
.values(&new_session)
.get_result::<Session>(conn)
.unwrap();
return session;
}
pub fn find_user_by_session(token: &str, conn: &PgConnection) -> QueryResult<(Session, User)> {
sessions::table
.inner_join(users::table)
.filter(sessions::token.eq(&token))
.first::<(Session, User)>(conn)
}
pub fn gen_session_token() -> String {
let mut rng = rand::thread_rng();
let token: [u8; 32] = rng.gen();
return base64::encode(&token);
}

106
backend/src/db/users.rs Normal file
View file

@ -0,0 +1,106 @@
use crate::{schema::users, DbConn};
use argon2;
use diesel::{prelude::*, PgConnection};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct Credentials<'a> {
pub username: &'a str,
pub password: &'a str,
}
#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser<'a> {
pub username: &'a str,
pub password_hash: &'a [u8],
pub password_salt: &'a [u8],
}
#[derive(Queryable, Debug)]
pub struct User {
pub user_id: i32,
pub username: String,
pub password_salt: Vec<u8>,
pub password_hash: Vec<u8>,
}
// TODO: make this configurable somewhere
fn argon2_config() -> argon2::Config<'static> {
argon2::Config {
variant: argon2::Variant::Argon2i,
version: argon2::Version::Version13,
mem_cost: 4096,
time_cost: 3,
lanes: 1,
thread_mode: argon2::ThreadMode::Sequential,
// TODO: set a secret
secret: &[],
ad: &[],
hash_length: 32,
}
}
pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResult<User> {
let argon_config = argon2_config();
let salt: [u8; 32] = rand::thread_rng().gen();
let hash = argon2::hash_raw(credentials.password.as_bytes(), &salt, &argon_config).unwrap();
let new_user = NewUser {
username: &credentials.username,
password_salt: &salt,
password_hash: &hash,
};
diesel::insert_into(users::table)
.values(&new_user)
.get_result::<User>(conn)
}
pub fn authenticate_user(credentials: &Credentials, db_conn: &PgConnection) -> Option<User> {
let user = users::table
.filter(users::username.eq(&credentials.username))
.first::<User>(db_conn)
.unwrap();
let password_matches = argon2::verify_raw(
credentials.password.as_bytes(),
&user.password_salt,
&user.password_hash,
&argon2_config(),
)
.unwrap();
if password_matches {
return Some(user);
} else {
return None;
}
}
#[test]
fn test_argon() {
let credentials = Credentials {
username: "piepkonijn",
password: "geheim123",
};
let argon_config = argon2_config();
let salt: [u8; 32] = rand::thread_rng().gen();
let hash = argon2::hash_raw(credentials.password.as_bytes(), &salt, &argon_config).unwrap();
let new_user = NewUser {
username: &credentials.username,
password_hash: &hash,
password_salt: &salt,
};
let password_matches = argon2::verify_raw(
credentials.password.as_bytes(),
&new_user.password_salt,
&new_user.password_hash,
&argon2_config(),
)
.unwrap();
assert!(password_matches);
}

View file

@ -1,15 +1,36 @@
#![feature(proc_macro_hygiene, decl_macro)] #![feature(proc_macro_hygiene, decl_macro)]
use rocket::{Build, Rocket};
use rocket_sync_db_pools::database;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
#[macro_use] #[macro_use]
extern crate rocket_contrib; extern crate diesel;
mod db;
mod routes;
mod schema;
#[database("postgresql_database")]
pub struct DbConn(diesel::PgConnection);
#[get("/")] #[get("/")]
fn index() -> &'static str { fn index() -> &'static str {
"Hello, world!" "Hello, world!"
} }
fn main() { #[launch]
rocket::ignite().mount("/", routes![index]).launch(); fn rocket() -> Rocket<Build> {
rocket::build()
.mount(
"/",
routes![
index,
routes::users::register,
routes::users::login,
routes::users::current_user,
],
)
.attach(DbConn::fairing())
} }

View file

@ -0,0 +1 @@
pub mod users;

100
backend/src/routes/users.rs Normal file
View file

@ -0,0 +1,100 @@
use crate::db::{sessions, users};
use crate::{
db::users::{Credentials, User},
DbConn,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
#[derive(Debug)]
pub enum AuthTokenError {
BadCount,
Missing,
Invalid,
}
// TODO: error handling and proper lifetimes
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
type Error = AuthTokenError;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let keys: Vec<_> = request.headers().get("Authorization").collect();
let token = match keys.len() {
0 => return Outcome::Failure((Status::BadRequest, AuthTokenError::Missing)),
1 => keys[0].to_string(),
_ => return Outcome::Failure((Status::BadRequest, AuthTokenError::BadCount)),
};
let db = request.guard::<DbConn>().await.unwrap();
let (_session, user) = db
.run(move |conn| sessions::find_user_by_session(&token, conn))
.await
.unwrap();
Outcome::Success(user)
}
}
#[derive(Serialize, Deserialize)]
pub struct UserData {
pub user_id: i32,
pub username: String,
}
impl From<User> for UserData {
fn from(user: User) -> Self {
UserData {
user_id: user.user_id,
username: user.username,
}
}
}
#[derive(Deserialize)]
pub struct RegistrationParams {
pub username: String,
pub password: String,
}
#[post("/register", data = "<params>")]
pub async fn register(db_conn: DbConn, params: Json<RegistrationParams>) -> Json<UserData> {
db_conn
.run(move |conn| {
let credentials = Credentials {
username: &params.username,
password: &params.password,
};
let user = users::create_user(&credentials, conn).unwrap();
Json(user.into())
})
.await
}
#[derive(Deserialize)]
pub struct LoginParams {
pub username: String,
pub password: String,
}
#[post("/login", data = "<params>")]
pub async fn login(db_conn: DbConn, params: Json<LoginParams>) -> String {
db_conn
.run(move |conn| {
let credentials = Credentials {
username: &params.username,
password: &params.password,
};
// TODO: handle failures
let user = users::authenticate_user(&credentials, conn).unwrap();
let session = sessions::create_session(&user, conn);
return session.token;
})
.await
}
#[get("/users/me")]
pub async fn current_user(user: User) -> Json<UserData> {
Json(user.into())
}

20
backend/src/schema.rs Normal file
View file

@ -0,0 +1,20 @@
table! {
sessions (id) {
id -> Int4,
user_id -> Int4,
token -> Varchar,
}
}
table! {
users (id) {
id -> Int4,
username -> Varchar,
password_salt -> Bytea,
password_hash -> Bytea,
}
}
joinable!(sessions -> users (user_id));
allow_tables_to_appear_in_same_query!(sessions, users,);

0
backend/tests/common.rs Normal file
View file