Working on session validation

This commit is contained in:
2025-04-11 09:53:05 -04:00
parent 56ac66e9b1
commit 98887d7fef
24 changed files with 724 additions and 132 deletions

View File

@@ -1,12 +1,11 @@
use std::collections::HashMap;
use std::str::FromStr;
use actix_web::web::Json;
use futures_util::try_join;
use moka::future::Cache;
use serde::{Deserialize, Serialize};
use sqlx::{Execute, Postgres, QueryBuilder};
use crate::airports::model::airport_category::AirportCategory;
use crate::airports::{Frequency, FrequencyRow, Runway, RunwayRow, UpdateFrequency, UpdateRunway};
use sqlx::{Postgres, QueryBuilder};
use crate::airports::{
AirportCategory, Frequency, FrequencyRow, Runway, RunwayRow, UpdateFrequency, UpdateRunway,
};
use crate::db;
use crate::error::{ApiResult, Error};
use crate::metars::Metar;
@@ -516,7 +515,7 @@ impl Airport {
}
// TODO
pub async fn update(icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
pub async fn update(_icao: &str, _airport: &UpdateAirport) -> ApiResult<()> {
Ok(())
}

View File

@@ -1,14 +1,12 @@
use std::str::FromStr;
use futures_util::stream::StreamExt as _;
use crate::{
airports::{Airport, AirportCategory},
airports::Airport,
db::Paged,
auth::{Auth, verify_role},
};
use actix_multipart::Multipart;
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
use serde::{Serialize, Deserialize};
use crate::airports::{AirportQuery, UpdateAirport};
use crate::users::ADMIN_ROLE;

View File

@@ -5,7 +5,6 @@ use argon2::{
use rand::distr::Alphanumeric;
use rand::prelude::*;
use rand_chacha::ChaCha20Rng;
use serde::{Deserialize, Serialize};
mod model;
mod routes;

View File

@@ -27,9 +27,12 @@ impl FromRequest for Auth {
Some(key_id) => {
let fut = async move {
// Check if the Session API key exists
let api_key = match Session::get(&key_id).await? {
Some(session) => session,
None => return Err(Error::new(401, "API Key does not exist".to_string()).into()),
let api_key = match Session::get(&key_id).await {
Ok(session) => session,
Err(err) => {
log::error!("Invalid session auth attempt: {}", err);
return Err(Error::new(401, "API Key does not exist".to_string()).into());
}
};
match User::select(&api_key.email).await {
Some(user) => Ok(Auth {

View File

@@ -1,17 +1,11 @@
use std::sync::OnceLock;
use actix_web::{
post, web, HttpResponse, ResponseError,
cookie::{Cookie, time::Duration},
HttpRequest, put,
};
use actix_web::{post, web, HttpResponse, ResponseError, HttpRequest, put, get};
use crate::{
auth::{verify_hash, Session, SESSION_COOKIE_NAME},
error::Error,
users::{LoginRequest, RegisterRequest, User, UserResponse},
};
use crate::auth::{hash, Auth, DEFAULT_SESSION_TTL};
use crate::error::ApiResult;
use crate::auth::Auth;
use crate::users::UpdateUser;
#[post("/register")]
@@ -19,20 +13,20 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
let register_user = user.into_inner();
let email = register_user.email.clone();
let ip_address = req.peer_addr().unwrap().ip().to_string();
let mut insert_user: User = match register_user.to_user() {
let insert_user: User = match register_user.to_user() {
Ok(user) => user,
Err(err) => return ResponseError::error_response(&err),
};
match insert_user.insert().await {
Ok(user) => {
let response: UserResponse = user.into();
let user_response: UserResponse = user.into();
log::info!(
"Successful user registration [Email: {}] [IP Address: {}]",
email,
ip_address
);
HttpResponse::Created().json(response)
HttpResponse::Created().json(user_response)
}
Err(err) => {
// Obfuscate the service error message to prevent leaking database details
@@ -63,8 +57,8 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
if verify_hash(&request.password, &query_user.password_hash) {
// Create a session
let session = Session::new(64, &email, &ip_address, Some(DEFAULT_SESSION_TTL));
let session_cookie = session.to_cookie();
let session = Session::default(&email, &ip_address);
let session_cookie = session.cookie();
// Save the session to the database
if let Err(err) = session.store().await {
log::error!(
@@ -80,7 +74,10 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
email,
ip_address
);
HttpResponse::Ok().cookie(session_cookie).finish()
let user_response: UserResponse = query_user.into();
HttpResponse::Ok()
.cookie(session_cookie)
.json(user_response)
} else {
log::error!(
"Invalid login attempt [Email: {}] [IP Address: {}]",
@@ -119,19 +116,59 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
}
}
let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "")
.path("/")
.max_age(Duration::seconds(-1))
.secure(true)
.http_only(true)
.finish();
log::info!(
"Successful logout attempt [Email: {}] [IP Address: {}]",
email,
ip_address
);
HttpResponse::Ok().cookie(session_cookie).finish()
HttpResponse::Ok().cookie(Session::empty_cookie()).finish()
}
#[get("/session")]
async fn validate_session(req: HttpRequest) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string();
// Verify a session cookie exists
match req.cookie(SESSION_COOKIE_NAME) {
// Validate the session
Some(cookie) => {
let session_id = cookie.value().to_string();
let session = match Session::replace(&session_id, &ip_address).await {
Ok(session) => session,
Err(err) => {
log::error!(
"Invalid session validate attempt [Session: {}] [IP Address: {}]",
session_id,
ip_address
);
return ResponseError::error_response(&Error::new(500, err.to_string()));
}
};
let email = &session.email;
let query_user = match User::select(&email).await {
Some(query_user) => query_user,
None => {
return HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.finish()
}
};
let user_response: UserResponse = query_user.into();
let session_cookie = session.cookie();
log::info!(
"Successful session validate attempt [Email: {}] [IP Address: {}]",
email,
ip_address
);
HttpResponse::Ok()
.cookie(session_cookie)
.json(user_response)
}
None => HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.finish(),
}
}
#[put("/password")]
@@ -178,8 +215,8 @@ async fn change_password(
}
#[post("/password-reset")]
async fn password_reset(req: HttpRequest, auth: Auth) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string();
async fn password_reset(req: HttpRequest, _auth: Auth) -> HttpResponse {
let _ip_address = req.peer_addr().unwrap().ip().to_string();
HttpResponse::Ok().finish()
}
@@ -189,6 +226,7 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
.service(register)
.service(login)
.service(logout)
.service(change_password),
.service(change_password)
.service(validate_session),
);
}

View File

@@ -2,15 +2,14 @@ use actix_web::cookie::{time::Duration, Cookie};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use redis::{AsyncCommands, RedisResult};
use tokio::task;
use crate::{
db::redis_async_connection,
error::{Error, ApiResult},
};
use super::{csprng, hash, verify_hash};
pub const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
pub const SESSION_COOKIE_NAME: &str = "session";
#[derive(Debug, Serialize, Deserialize)]
@@ -23,6 +22,10 @@ pub struct Session {
}
impl Session {
pub fn default(email: &str, ip_address: &str) -> Self {
Self::new(64, email, ip_address, Some(DEFAULT_SESSION_TTL))
}
pub fn new(take: usize, email: &str, ip_address: &str, ttl: Option<i64>) -> Self {
let now = Utc::now();
Self {
@@ -53,16 +56,32 @@ impl Session {
}
}
pub async fn get(session_id: &str) -> ApiResult<Option<Self>> {
pub async fn get(session_id: &str) -> ApiResult<Self> {
let mut conn = redis_async_connection().await?;
let result: RedisResult<Option<String>> = conn.get(session_id).await;
match result {
Ok(Some(value)) => Ok(Some(serde_json::from_str(&value)?)),
Ok(None) => Ok(None),
Ok(Some(value)) => Ok(serde_json::from_str(&value)?),
Ok(None) => Err(Error::new(401, format!("Missing session {}", session_id))),
Err(err) => Err(err.into()),
}
}
pub async fn replace(session_id: &str, ip_address: &str) -> ApiResult<Self> {
let mut session = Self::verify(session_id, ip_address).await?;
let session_id_owned = session_id.to_owned();
task::spawn(async move {
if let Err(err) = Self::delete(&session_id_owned).await {
log::error!(
"Error deleting old session in replace session call: {}",
err
);
};
});
session = Session::default(&session.email, ip_address);
session.store().await?;
Ok(session)
}
pub async fn delete(session_id: &str) -> ApiResult<()> {
let mut conn = redis_async_connection().await?;
let result: RedisResult<()> = conn.del(session_id).await;
@@ -73,11 +92,7 @@ impl Session {
}
pub async fn verify(session_id: &str, ip_address: &str) -> ApiResult<Self> {
// Check if the session exists
let session = match Self::get(session_id).await? {
Some(session) => session,
None => return Err(Error::new(401, "Session does not exist".to_string())),
};
let session = Self::get(session_id).await?;
// Check if the IP Address matches the Session's IP Address
if verify_hash(ip_address, &session.ip_address) {
@@ -87,7 +102,7 @@ impl Session {
}
}
pub fn to_cookie(&self) -> Cookie {
pub fn cookie(&self) -> Cookie {
let expires_at = match self.expires_at {
Some(expires_at) => expires_at.timestamp(),
None => DEFAULT_SESSION_TTL,
@@ -96,14 +111,13 @@ impl Session {
let mut cookie = Cookie::build(SESSION_COOKIE_NAME, self.session_id.clone())
.path("/")
.max_age(Duration::seconds(ttl))
// TODO: enable secure and http_only
.secure(true)
.http_only(true)
.finish();
if let Ok(environment) = std::env::var("ENVIRONMENT") {
if environment == "development" || environment == "dev" {
log::debug!(
log::trace!(
"Development cookie [Email: {}]: {}",
self.email,
self.session_id
@@ -115,4 +129,22 @@ impl Session {
cookie
}
pub fn empty_cookie() -> Cookie<'static> {
let mut cookie = Cookie::build(SESSION_COOKIE_NAME, "")
.path("/")
.max_age(Duration::seconds(-1))
.secure(true)
.http_only(true)
.finish();
if let Ok(environment) = std::env::var("ENVIRONMENT") {
if environment == "development" || environment == "dev" {
cookie.set_secure(false);
cookie.set_http_only(false);
}
}
cookie
}
}

View File

@@ -23,10 +23,16 @@ pub async fn initialize() -> ApiResult<()> {
let db_url = format!(
"postgres://{}:{}@{}:{}/{}",
db_user, db_password, db_host, db_port, db_name
&db_user, &db_password, &db_host, &db_port, &db_name
);
log::info!("Connecting to database at {}...", &db_url);
log::info!(
"Connecting to database at postgres://{}:*****@{}:{}/{}...",
&db_user,
&db_host,
&db_port,
&db_name
);
// Setup Postgres pool connection
let pool = PgPoolOptions::new()
.max_connections(5)
@@ -35,7 +41,7 @@ pub async fn initialize() -> ApiResult<()> {
.await?;
match POOL.set(pool) {
Ok(_) => log::info!("Database connection established"),
Err(_) => log::warn!("Database pool already initialized")
Err(_) => log::warn!("Database pool already initialized"),
}
// Setup Redis connection
@@ -47,7 +53,7 @@ pub async fn initialize() -> ApiResult<()> {
};
match REDIS.set(redis) {
Ok(_) => log::info!("Redis connection established"),
Err(_) => log::warn!("Redis client already initialized")
Err(_) => log::warn!("Redis client already initialized"),
}
let schema = std::env::var("MINIO_SCHEMA").unwrap_or("http".to_string());
@@ -76,7 +82,7 @@ pub async fn initialize() -> ApiResult<()> {
match BUCKET.set(*bucket) {
Ok(_) => log::info!("Bucket initialized"),
Err(_) => log::warn!("Bucket client already initialized")
Err(_) => log::warn!("Bucket client already initialized"),
}
// Run migrations

View File

@@ -3,7 +3,6 @@ use std::env;
use actix_cors::Cors;
use actix_web::{App, HttpServer, middleware::Logger, web};
use dotenv::from_filename;
use moka::future::Cache;
use crate::auth::hash;
use crate::users::{User, ADMIN_ROLE};
@@ -63,14 +62,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.allow_any_header()
.supports_credentials()
.max_age(3600);
App::new()
.wrap(cors)
.wrap(Logger::default())
.service(web::scope("api")
App::new().wrap(cors).wrap(Logger::default()).service(
web::scope("api")
.configure(airports::init_routes)
.configure(metars::init_routes)
.configure(auth::init_routes)
.configure(users::init_routes))
.configure(users::init_routes),
)
})
.bind(format!("{}:{}", host, port))
{

View File

@@ -2,7 +2,6 @@ use crate::error::Error;
use crate::{error::ApiResult, db};
use chrono::{DateTime, Datelike, Utc};
use std::collections::HashSet;
use moka::future::Cache;
use redis::{AsyncCommands, RedisResult};
use serde::{Deserialize, Serialize};
use crate::db::redis_async_connection;
@@ -294,7 +293,7 @@ impl Metar {
Ok(day) => day,
Err(err) => return Err(err.into()),
};
let mut observation_time_hour = match observation_time[2..4].parse::<u32>() {
let observation_time_hour = match observation_time[2..4].parse::<u32>() {
Ok(hour) => hour,
Err(err) => return Err(err.into()),
};

View File

@@ -1,4 +1,3 @@
use crate::error::Error;
use crate::metars::Metar;
use actix_web::{get, web, HttpResponse, HttpRequest};
use log::error;

View File

@@ -1,7 +1,7 @@
use tokio::time::{sleep, Duration};
// use tokio::time::{sleep, Duration};
// use crate::airports::{AirportDb, AirportFilter};
use crate::metars::Metar;
// use crate::metars::Metar;
pub fn update_airports() {
// tokio::spawn(async {

View File

@@ -152,7 +152,7 @@
// }
// }
pub fn init_routes(config: &mut actix_web::web::ServiceConfig) {
pub fn init_routes(_config: &mut actix_web::web::ServiceConfig) {
// config.service(
// web::scope("users")
// .service(get_favorites)