feat(api): implemented /account/auth

This commit is contained in:
antifallobst 2024-03-22 20:32:26 +01:00
parent 9d01fc2a54
commit eb6aa532ff
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
6 changed files with 101 additions and 12 deletions

View File

@ -4,7 +4,7 @@
- /account - /account
- [X] `POST` /register - [X] `POST` /register
- [ ] `POST` /auth - [X] `POST` /auth
- [ ] `POST` /delete - [ ] `POST` /delete
- [ ] `GET` /blob - [ ] `GET` /blob
- [ ] `POST` /blob - [ ] `POST` /blob

View File

@ -4,6 +4,8 @@ use crate::backend::Backend;
use actix_web::{post, web, HttpResponse, Responder}; use actix_web::{post, web, HttpResponse, Responder};
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
use uuid::Uuid;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct RegisterRequest { struct RegisterRequest {
@ -13,7 +15,7 @@ struct RegisterRequest {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct RegisterResponse { struct RegisterResponse {
uuid: String, userid: String,
} }
#[post("/account/register")] #[post("/account/register")]
@ -30,8 +32,39 @@ pub async fn register(
Ok(res) => match res { Ok(res) => match res {
Err(e) => e.into(), Err(e) => e.into(),
Ok(uuid) => HttpResponse::Ok().json(RegisterResponse { 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<Backend>, body: web::Json<AuthRequest>) -> 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 }),
},
}
}

View File

@ -11,6 +11,7 @@ pub async fn start(config: &Config, backend: Backend) -> Result<()> {
App::new() App::new()
.app_data(web::Data::new(backend.clone())) .app_data(web::Data::new(backend.clone()))
.service(endpoints::account::register) .service(endpoints::account::register)
.service(endpoints::account::auth)
.service(endpoints::account::invite::new) .service(endpoints::account::invite::new)
}) })
.bind((config.addr.as_str(), config.port))?; .bind((config.addr.as_str(), config.port))?;

View File

@ -4,15 +4,18 @@ use thiserror::Error;
#[derive(Debug, Error, Serialize, Copy, Clone)] #[derive(Debug, Error, Serialize, Copy, Clone)]
pub enum Error { pub enum Error {
#[error("Failed to authenticate with the given credentials")]
AuthenticationFailure,
#[error("The given token is invalid")] #[error("The given token is invalid")]
InvalidToken, InvalidToken,
#[error("The given token is expired")]
TokenExpired,
#[error("Permission denied: {0}")] #[error("Permission denied: {0}")]
PermissionDenied(&'static str), PermissionDenied(&'static str),
#[error("The given token is expired")]
TokenExpired,
#[error("The given user cannot be found")] #[error("The given user cannot be found")]
UserNotFound, UserNotFound,
} }
@ -31,9 +34,10 @@ impl Into<HttpResponse> for Error {
}; };
match self { match self {
Error::AuthenticationFailure => HttpResponse::Unauthorized().json(body),
Error::InvalidToken => HttpResponse::Unauthorized().json(body), Error::InvalidToken => HttpResponse::Unauthorized().json(body),
Error::TokenExpired => HttpResponse::Gone().json(body),
Error::PermissionDenied(_) => HttpResponse::Forbidden().json(body), Error::PermissionDenied(_) => HttpResponse::Forbidden().json(body),
Error::TokenExpired => HttpResponse::Gone().json(body),
Error::UserNotFound => HttpResponse::NotFound().json(body), Error::UserNotFound => HttpResponse::NotFound().json(body),
} }
} }

View File

@ -5,7 +5,7 @@ mod user;
use crate::backend::{error::Error, permissions::Permission, user::IntoUser}; use crate::backend::{error::Error, permissions::Permission, user::IntoUser};
use crate::config::Config; use crate::config::Config;
use anyhow::Result; use anyhow::{bail, Result};
use argon2::{ use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2, Argon2,
@ -99,7 +99,7 @@ impl Backend {
} }
/// Check whether an invite-token is valid or not. /// Check whether an invite-token is valid or not.
async fn check_activation_token(&self, token: &str) -> Result<Result<(), Error>> { async fn check_invite_token(&self, token: &str) -> Result<Result<(), Error>> {
match sqlx::query_as!( match sqlx::query_as!(
InviteTokensRow, InviteTokensRow,
r#"SELECT * FROM InviteTokens WHERE token = ?;"#, r#"SELECT * FROM InviteTokens WHERE token = ?;"#,
@ -149,7 +149,7 @@ impl Backend {
token: String, token: String,
password: String, password: String,
) -> Result<Result<Uuid, Error>> { ) -> Result<Result<Uuid, Error>> {
if let Err(e) = self.check_activation_token(&token).await? { if let Err(e) = self.check_invite_token(&token).await? {
return Ok(Err(e)); return Ok(Err(e));
} }
@ -173,6 +173,43 @@ impl Backend {
Ok(Ok(userid)) 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<Result<String, Error>> {
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. /// 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<Result<String, Error>> { pub async fn create_invite(&self, user: impl IntoUser) -> Result<Result<String, Error>> {
let user = match user.into_user(&self).await? { let user = match user.into_user(&self).await? {
@ -188,11 +225,10 @@ impl Backend {
let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48);
// TODO: change days to 7
sqlx::query!( sqlx::query!(
r#"INSERT INTO InviteTokens VALUES (?, ?);"#, r#"INSERT INTO InviteTokens VALUES (?, ?);"#,
token, token,
Utc::now().naive_utc().checked_add_days(Days::new(14)) Utc::now().naive_utc().checked_add_days(Days::new(7))
) )
.execute(&self.pool) .execute(&self.pool)
.await?; .await?;

View File

@ -2,6 +2,7 @@ use crate::backend::db_structures::UsersRow;
use crate::backend::error::Error; use crate::backend::error::Error;
use crate::backend::{permissions::Permission, Backend}; use crate::backend::{permissions::Permission, Backend};
use anyhow::Result; use anyhow::Result;
use argon2::PasswordVerifier;
use std::str::FromStr; use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
@ -27,6 +28,20 @@ impl User {
(self.permissions & permission.as_u16()) > 0 (self.permissions & permission.as_u16()) > 0
} }
pub fn verify_password(&self, password: &str) -> Result<bool> {
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<()> { async fn flush(&self, backend: &Backend) -> Result<()> {
sqlx::query!( sqlx::query!(
r#"UPDATE Users SET password = ?, permissions = ? WHERE userid = ?;"#, r#"UPDATE Users SET password = ?, permissions = ? WHERE userid = ?;"#,