backend: working auth

This commit is contained in:
Sphereso 2024-07-08 12:55:11 +02:00
parent 8c3becf843
commit ab22d6d830
9 changed files with 172 additions and 10 deletions

15
Cargo.lock generated
View file

@ -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",
]

View file

@ -18,3 +18,4 @@ sea-orm = { version = "0.12", features = [
] }
dotenvy = "*"
jsonwebtoken = "*"
futures = "*"

View file

@ -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<String, jsonwebtoken::errors::Error> {
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<Claims, Error> {
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::<web::Data<AppState>>().unwrap().secret;
let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
let claims = jsonwebtoken::decode::<Claims>(
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<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
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::<web::Data<AppState>>().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<actix_web::body::BoxBody> {
HttpResponse::build(self.status_code()).finish()
}
}

View file

@ -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))
}
}

View file

@ -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<entity::user::Model> for UserWithoutPassword {
}
impl UserController {
pub async fn list_users(state: web::Data<AppState>) -> actix_web::Result<impl Responder> {
pub async fn list_users(
state: web::Data<AppState>,
_executor: AuthedUser,
) -> actix_web::Result<impl Responder> {
let db = &state.db;
let users = entity::prelude::User::find()
.all(db)

View file

@ -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...");

View file

@ -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"

View file

@ -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<Box<dyn MigrationTrait>> {
vec![Box::new(m20240705_100914_create_user::Migration)]
vec![
Box::new(m20240705_100914_create_user::Migration),
Box::new(m20240708_085852_create_admin_user::Migration),
]
}
}

View file

@ -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,
}