From eb6aa532ffee1958db19e31991fcd4057b0ee209 Mon Sep 17 00:00:00 2001 From: antifallobst Date: Fri, 22 Mar 2024 20:32:26 +0100 Subject: [PATCH] feat(api): implemented /account/auth --- docs/api.md | 2 +- src/api/endpoints/account/mod.rs | 37 +++++++++++++++++++++++-- src/api/mod.rs | 1 + src/backend/error.rs | 12 ++++++--- src/backend/mod.rs | 46 ++++++++++++++++++++++++++++---- src/backend/user.rs | 15 +++++++++++ 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/docs/api.md b/docs/api.md index e234328..a03fe00 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,7 +4,7 @@ - /account - [X] `POST` /register - - [ ] `POST` /auth + - [X] `POST` /auth - [ ] `POST` /delete - [ ] `GET` /blob - [ ] `POST` /blob diff --git a/src/api/endpoints/account/mod.rs b/src/api/endpoints/account/mod.rs index 6631d8a..b73eee6 100644 --- a/src/api/endpoints/account/mod.rs +++ b/src/api/endpoints/account/mod.rs @@ -4,6 +4,8 @@ use crate::backend::Backend; use actix_web::{post, web, HttpResponse, Responder}; use log::error; use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use uuid::Uuid; #[derive(Debug, Deserialize)] struct RegisterRequest { @@ -13,7 +15,7 @@ struct RegisterRequest { #[derive(Debug, Serialize)] struct RegisterResponse { - uuid: String, + userid: String, } #[post("/account/register")] @@ -30,8 +32,39 @@ pub async fn register( Ok(res) => match res { Err(e) => e.into(), Ok(uuid) => HttpResponse::Ok().json(RegisterResponse { - uuid: uuid.to_string(), + userid: uuid.to_string(), }), }, } } + +#[derive(Debug, Deserialize)] +struct AuthRequest { + userid: String, + password: String, +} + +#[derive(Debug, Serialize)] +struct AuthResponse { + token: String, +} + +#[post("/account/auth")] +pub async fn auth(backend: web::Data, body: web::Json) -> impl Responder { + let body = body.into_inner(); + let userid = match Uuid::from_str(&body.userid) { + Ok(userid) => userid, + Err(_) => return HttpResponse::BadRequest().finish(), + }; + + match backend.authenticate(userid, body.password).await { + Err(e) => { + error!("{e}"); + HttpResponse::InternalServerError().finish() + } + Ok(res) => match res { + Err(e) => e.into(), + Ok(token) => HttpResponse::Ok().json(AuthResponse { token }), + }, + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 528b29e..8820788 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -11,6 +11,7 @@ pub async fn start(config: &Config, backend: Backend) -> Result<()> { App::new() .app_data(web::Data::new(backend.clone())) .service(endpoints::account::register) + .service(endpoints::account::auth) .service(endpoints::account::invite::new) }) .bind((config.addr.as_str(), config.port))?; diff --git a/src/backend/error.rs b/src/backend/error.rs index 12a4945..5537804 100644 --- a/src/backend/error.rs +++ b/src/backend/error.rs @@ -4,15 +4,18 @@ use thiserror::Error; #[derive(Debug, Error, Serialize, Copy, Clone)] pub enum Error { + #[error("Failed to authenticate with the given credentials")] + AuthenticationFailure, + #[error("The given token is invalid")] InvalidToken, - #[error("The given token is expired")] - TokenExpired, - #[error("Permission denied: {0}")] PermissionDenied(&'static str), + #[error("The given token is expired")] + TokenExpired, + #[error("The given user cannot be found")] UserNotFound, } @@ -31,9 +34,10 @@ impl Into for Error { }; match self { + Error::AuthenticationFailure => HttpResponse::Unauthorized().json(body), Error::InvalidToken => HttpResponse::Unauthorized().json(body), - Error::TokenExpired => HttpResponse::Gone().json(body), Error::PermissionDenied(_) => HttpResponse::Forbidden().json(body), + Error::TokenExpired => HttpResponse::Gone().json(body), Error::UserNotFound => HttpResponse::NotFound().json(body), } } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 693f56b..5ddf351 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -5,7 +5,7 @@ mod user; use crate::backend::{error::Error, permissions::Permission, user::IntoUser}; use crate::config::Config; -use anyhow::Result; +use anyhow::{bail, Result}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, @@ -99,7 +99,7 @@ impl Backend { } /// Check whether an invite-token is valid or not. - async fn check_activation_token(&self, token: &str) -> Result> { + async fn check_invite_token(&self, token: &str) -> Result> { match sqlx::query_as!( InviteTokensRow, r#"SELECT * FROM InviteTokens WHERE token = ?;"#, @@ -149,7 +149,7 @@ impl Backend { token: String, password: String, ) -> Result> { - if let Err(e) = self.check_activation_token(&token).await? { + if let Err(e) = self.check_invite_token(&token).await? { return Ok(Err(e)); } @@ -173,6 +173,43 @@ impl Backend { Ok(Ok(userid)) } + /// Generates an auth token for a user, given their UUID and password. + pub async fn authenticate( + &self, + userid: Uuid, + password: String, + ) -> Result> { + let user = match userid.into_user(&self).await? { + Ok(user) => user, + Err(e) => return Ok(Err(e)), + }; + + if !user.verify_password(&password)? { + return Ok(Err(Error::AuthenticationFailure)); + } + + let mut token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); + // just for the case, that there's some duplication + loop { + match self.resolve_auth_token(&token).await? { + Ok(_) => token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48), + Err(Error::InvalidToken) | Err(Error::TokenExpired) => break, + Err(e) => bail!("!THIS ERROR SHOULDN'T BE HERE! -> {e}"), + } + } + + sqlx::query!( + r#"INSERT INTO AuthTokens VALUES (?, ?, ?);"#, + token, + userid.as_bytes().as_slice(), + Utc::now().naive_utc().checked_add_days(Days::new(14)) + ) + .execute(&self.pool) + .await?; + + Ok(Ok(token)) + } + /// Generates a new invite token, if the user identified by the UUID has the permission to do so. pub async fn create_invite(&self, user: impl IntoUser) -> Result> { let user = match user.into_user(&self).await? { @@ -188,11 +225,10 @@ impl Backend { let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); - // TODO: change days to 7 sqlx::query!( r#"INSERT INTO InviteTokens VALUES (?, ?);"#, token, - Utc::now().naive_utc().checked_add_days(Days::new(14)) + Utc::now().naive_utc().checked_add_days(Days::new(7)) ) .execute(&self.pool) .await?; diff --git a/src/backend/user.rs b/src/backend/user.rs index 65ac9a1..e821f7a 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -2,6 +2,7 @@ use crate::backend::db_structures::UsersRow; use crate::backend::error::Error; use crate::backend::{permissions::Permission, Backend}; use anyhow::Result; +use argon2::PasswordVerifier; use std::str::FromStr; use uuid::Uuid; @@ -27,6 +28,20 @@ impl User { (self.permissions & permission.as_u16()) > 0 } + pub fn verify_password(&self, password: &str) -> Result { + let hash = argon2::PasswordHash::new(self.password.as_str()).map_err(|_| { + anyhow::Error::msg(format!( + "Failed to parse the password hash of user: {}", + self.uuid + )) + })?; + + match argon2::Argon2::default().verify_password(password.as_bytes(), &hash) { + Err(_) => Ok(false), + Ok(_) => Ok(true), + } + } + async fn flush(&self, backend: &Backend) -> Result<()> { sqlx::query!( r#"UPDATE Users SET password = ?, permissions = ? WHERE userid = ?;"#,