diff --git a/Cargo.lock b/Cargo.lock index facef49..6d9160d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,6 +573,7 @@ dependencies = [ "argon2", "dotenvy", "entity", + "futures", "jsonwebtoken", "migration", "sea-orm", @@ -1161,6 +1162,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1239,6 +1241,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -1260,6 +1273,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1688,6 +1702,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" name = "migration" version = "0.1.0" dependencies = [ + "argon2", "async-std", "sea-orm-migration", ] diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index c13d527..11d839b 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -18,3 +18,4 @@ sea-orm = { version = "0.12", features = [ ] } dotenvy = "*" jsonwebtoken = "*" +futures = "*" diff --git a/crates/backend/src/auth.rs b/crates/backend/src/auth.rs index 35129ee..60e37d5 100644 --- a/crates/backend/src/auth.rs +++ b/crates/backend/src/auth.rs @@ -1,26 +1,123 @@ -use jsonwebtoken::{EncodingKey, Header, Validation}; +use core::fmt; +use std::{pin::Pin, time::SystemTime}; + +use actix_web::{ + error::ErrorUnauthorized, + http::{header, StatusCode}, + web, Error, FromRequest, HttpRequest, HttpResponse, ResponseError, +}; +use futures::Future; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use migration::token; +use sea_orm::EntityTrait; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::AppState; + #[derive(Deserialize, Serialize)] struct Claims { sub: Uuid, name: String, + exp: u64, } pub fn create_jwt( user: entity::user::Model, key: &EncodingKey, ) -> Result { + let time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH") + .as_secs() + + 86400; let claims = Claims { sub: user.id, name: user.name, + exp: time, }; jsonwebtoken::encode(&Header::default(), &claims, key) } -pub fn verify(token: &str) { - let validation = Validation::new(jsonwebtoken::Algorithm::HS256); - // jsonwebtoken::decode(token, , validation) +pub struct AuthedUser(entity::user::Model); + +impl AuthedUser { + fn parse_token(req: &HttpRequest) -> Result { + let header = req + .headers() + .get(header::AUTHORIZATION) + .ok_or(AuthorizationError)?; + + if header.len() < 8 { + return Err(ErrorUnauthorized("Unauthorized")); + } + + let mut parts = header + .to_str() + .map_err(|_| ErrorUnauthorized("Unauthorized"))? + .splitn(2, ' '); + + match parts.next() { + Some("Bearer") => {} + _ => return Err(ErrorUnauthorized("Unauthorized")), + } + + let token = parts.next().ok_or(ErrorUnauthorized("Unauthorized"))?; + + let key = &req.app_data::>().unwrap().secret; + + let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); + let claims = jsonwebtoken::decode::( + token, + &DecodingKey::from_secret(key.as_bytes()), + &validation, + ) + .map_err(|e| ErrorUnauthorized(e))?; + Ok(claims.claims) + } +} + +impl FromRequest for AuthedUser { + type Error = actix_web::Error; + + type Future = Pin>>>; + + fn from_request( + req: &actix_web::HttpRequest, + _payload: &mut actix_web::dev::Payload, + ) -> Self::Future { + let res = AuthedUser::parse_token(req); + let state = req.app_data::>().unwrap().db.clone(); + Box::pin(async move { + let claims = res?; + let id = claims.sub; + + let user = entity::prelude::User::find_by_id(id) + .one(&state) + .await + .map_err(|_| ErrorUnauthorized("Unauthorized"))? + .ok_or(ErrorUnauthorized("Unauthorized"))?; + + Ok(AuthedUser(user)) + }) + } +} + +#[derive(Debug)] +struct AuthorizationError; + +impl fmt::Display for AuthorizationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.status_code(), f) + } +} + +impl ResponseError for AuthorizationError { + fn status_code(&self) -> actix_web::http::StatusCode { + StatusCode::UNAUTHORIZED + } + + fn error_response(&self) -> actix_web::HttpResponse { + HttpResponse::build(self.status_code()).finish() + } } diff --git a/crates/backend/src/controller/auth.rs b/crates/backend/src/controller/auth.rs index 8ec2d10..746d6b3 100644 --- a/crates/backend/src/controller/auth.rs +++ b/crates/backend/src/controller/auth.rs @@ -3,6 +3,7 @@ use actix_web::{ web, HttpResponse, Responder, }; use argon2::{Argon2, PasswordHash, PasswordVerifier}; +use jsonwebtoken::EncodingKey; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use serde::Deserialize; @@ -38,7 +39,8 @@ impl AuthController { .verify_password(login.password.as_bytes(), &parsed_hash) .map_err(ErrorBadRequest)?; - let jwt = create_jwt(user, jwt_secret).map_err(ErrorInternalServerError)?; + let jwt = create_jwt(user, &EncodingKey::from_secret(jwt_secret.as_bytes())) + .map_err(ErrorInternalServerError)?; Ok(HttpResponse::Ok().body(jwt)) } } diff --git a/crates/backend/src/controller/user.rs b/crates/backend/src/controller/user.rs index 814c778..61c5279 100644 --- a/crates/backend/src/controller/user.rs +++ b/crates/backend/src/controller/user.rs @@ -7,7 +7,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::AppState; +use crate::{auth::AuthedUser, AppState}; pub struct UserController; @@ -36,7 +36,10 @@ impl From for UserWithoutPassword { } impl UserController { - pub async fn list_users(state: web::Data) -> actix_web::Result { + pub async fn list_users( + state: web::Data, + _executor: AuthedUser, + ) -> actix_web::Result { let db = &state.db; let users = entity::prelude::User::find() .all(db) diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index 313ca8e..89213cf 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -12,7 +12,7 @@ mod routes; #[derive(Clone)] struct AppState { db: DatabaseConnection, - secret: EncodingKey, + secret: String, } #[actix_web::main] @@ -37,7 +37,7 @@ async fn main() -> std::io::Result<()> { let state = AppState { db: conn, - secret: EncodingKey::from_secret(jwt_secret.as_bytes()), + secret: jwt_secret, }; println!("Listening for connections..."); diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index 6230d31..c080265 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dependencies] async-std = { version = "1", features = ["attributes", "tokio1"] } +argon2 = "*" [dependencies.sea-orm-migration] version = "0.12.0" diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 04bbb7d..b6f111f 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -3,10 +3,14 @@ pub use sea_orm_migration::prelude::*; pub struct Migrator; mod m20240705_100914_create_user; +mod m20240708_085852_create_admin_user; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20240705_100914_create_user::Migration)] + vec![ + Box::new(m20240705_100914_create_user::Migration), + Box::new(m20240708_085852_create_admin_user::Migration), + ] } } diff --git a/crates/migration/src/m20240708_085852_create_admin_user.rs b/crates/migration/src/m20240708_085852_create_admin_user.rs new file mode 100644 index 0000000..df2bd1c --- /dev/null +++ b/crates/migration/src/m20240708_085852_create_admin_user.rs @@ -0,0 +1,39 @@ +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::SaltString; +use argon2::Argon2; +use argon2::PasswordHasher; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let argon2 = Argon2::default(); + let salt = SaltString::generate(OsRng); + let hash = argon2 + .hash_password("admin".as_bytes(), &salt) + .expect("Generating Admin Password Hash failed"); + let insert = Query::insert() + .into_table(User::Table) + .columns([User::Name, User::Email, User::Hash]) + .values_panic(["admin".into(), "admin".into(), hash.to_string().into()]) + .to_owned(); + + manager.exec_stmt(insert).await?; + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} + +#[derive(DeriveIden)] +enum User { + Table, + Name, + Email, + Hash, +}