Compare commits

...

4 Commits

8 changed files with 122 additions and 14 deletions

14
Cargo.lock generated
View File

@ -389,6 +389,7 @@ dependencies = [
"bcrypt", "bcrypt",
"clap", "clap",
"env_logger", "env_logger",
"futures-util",
"include_dir", "include_dir",
"log", "log",
"mime_guess", "mime_guess",
@ -707,6 +708,17 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.21" version = "0.3.21"
@ -726,9 +738,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]

View File

@ -19,3 +19,4 @@ bcrypt = "0.12.1"
uuid = { version = "0.8.2", features = ["v4"] } uuid = { version = "0.8.2", features = ["v4"] }
mime_guess = "2.0.4" mime_guess = "2.0.4"
askama = "0.11.1" askama = "0.11.1"
futures-util = "0.3.21"

View File

@ -9,10 +9,13 @@ pub const DEFAULT_ADMIN_PASSWORD: &str = "admin";
pub const APP_NAME: &str = "Basic OIDC"; pub const APP_NAME: &str = "Basic OIDC";
/// Maximum session duration after inactivity, in seconds /// Maximum session duration after inactivity, in seconds
pub const MAX_SESSION_DURATION: u64 = 60 * 30; pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30;
/// Minimum interval between each last activity record in session /// Minimum interval between each last activity record in session
pub const MIN_ACTIVITY_RECORD_TIME: u64 = 10; pub const MIN_ACTIVITY_RECORD_TIME: u64 = 10;
/// Minimum password length /// Minimum password length
pub const MIN_PASS_LEN: usize = 4; pub const MIN_PASS_LEN: usize = 4;
/// Maximum session duration (6 hours)
pub const MAX_SESSION_DURATION: u64 = 3600 * 6;

View File

@ -1,8 +1,8 @@
use actix_identity::Identity; use actix_identity::Identity;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::constants::{MAX_SESSION_DURATION, MIN_ACTIVITY_RECORD_TIME}; use crate::constants::{MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, MIN_ACTIVITY_RECORD_TIME};
use crate::data::user::User; use crate::data::user::{User, UserID};
use crate::utils::time::time; use crate::utils::time::time;
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
@ -22,12 +22,11 @@ impl Default for SessionStatus {
#[derive(Debug, Serialize, Deserialize, Default)] #[derive(Debug, Serialize, Deserialize, Default)]
struct SessionIdentityData { struct SessionIdentityData {
pub id: String, pub id: UserID,
pub is_admin: bool, pub is_admin: bool,
login_time: u64,
last_access: u64, last_access: u64,
pub status: SessionStatus, pub status: SessionStatus,
// TODO : add session max duration (1 day)
} }
pub struct SessionIdentity<'a>(pub &'a Identity); pub struct SessionIdentity<'a>(pub &'a Identity);
@ -37,6 +36,7 @@ impl<'a> SessionIdentity<'a> {
self.set_session_data(&SessionIdentityData { self.set_session_data(&SessionIdentityData {
id: user.uid.clone(), id: user.uid.clone(),
is_admin: user.admin, is_admin: user.admin,
login_time: time(),
last_access: time(), last_access: time(),
status: SessionStatus::SignedIn, status: SessionStatus::SignedIn,
}); });
@ -46,7 +46,14 @@ impl<'a> SessionIdentity<'a> {
let mut res: Option<SessionIdentityData> = self.0.identity() let mut res: Option<SessionIdentityData> = self.0.identity()
.as_deref() .as_deref()
.map(serde_json::from_str) .map(serde_json::from_str)
.map(|f| f.expect("Failed to deserialize session data!")); .map(|f| match f {
Ok(d) => Some(d),
Err(e) => {
log::warn!("Failed to deserialize session data! {:?}", e);
None
}
})
.unwrap_or(None);
// Check if session is valid // Check if session is valid
if let Some(sess) = &res { if let Some(sess) = &res {
@ -56,7 +63,13 @@ impl<'a> SessionIdentity<'a> {
} }
if let Some(session) = res.as_mut() { if let Some(session) = res.as_mut() {
if session.last_access + MAX_SESSION_DURATION < time() { if session.login_time + MAX_SESSION_DURATION < time() {
log::info!("Session for {} reached max duration timeout", session.id);
self.0.forget();
return None;
}
if session.last_access + MAX_INACTIVITY_DURATION < time() {
log::info!("Session is expired for {}", session.id); log::info!("Session is expired for {}", session.id);
self.0.forget(); self.0.forget();
return None; return None;
@ -97,7 +110,7 @@ impl<'a> SessionIdentity<'a> {
.unwrap_or(false) .unwrap_or(false)
} }
pub fn user_id(&self) -> String { pub fn user_id(&self) -> UserID {
self.get_session_data() self.get_session_data()
.unwrap_or_default() .unwrap_or_default()
.id .id

View File

@ -2,4 +2,5 @@ pub mod data;
pub mod utils; pub mod utils;
pub mod constants; pub mod constants;
pub mod controllers; pub mod controllers;
pub mod actors; pub mod actors;
pub mod middlewares;

View File

@ -1,16 +1,17 @@
use actix::Actor;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{App, get, HttpServer, web}; use actix_web::{App, get, HttpServer, web};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use clap::Parser; use clap::Parser;
use basic_oidc::actors::users_actor::UsersActor;
use basic_oidc::constants::{DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME}; use basic_oidc::constants::{DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME};
use basic_oidc::controllers::assets_controller::assets_route; use basic_oidc::controllers::assets_controller::assets_route;
use basic_oidc::controllers::login_controller::{login_route, logout_route}; use basic_oidc::controllers::login_controller::{login_route, logout_route};
use basic_oidc::data::app_config::AppConfig; use basic_oidc::data::app_config::AppConfig;
use basic_oidc::data::entity_manager::EntityManager; use basic_oidc::data::entity_manager::EntityManager;
use basic_oidc::data::user::{hash_password, User}; use basic_oidc::data::user::{hash_password, User};
use basic_oidc::actors::users_actor::UsersActor; use basic_oidc::middlewares::auth_middleware::AuthMiddleware;
use actix::Actor;
use actix_identity::{IdentityService, CookieIdentityPolicy};
#[get("/health")] #[get("/health")]
async fn health() -> &'static str { async fn health() -> &'static str {
@ -71,6 +72,7 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(IdentityService::new(policy)) .wrap(IdentityService::new(policy))
.wrap(AuthMiddleware {})
// /health route // /health route
.service(health) .service(health)

View File

@ -0,0 +1,73 @@
//! # Authentication middleware
use std::future::{Future, ready, Ready};
use std::pin::Pin;
use std::rc::Rc;
use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpResponse};
use actix_web::body::EitherBody;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct AuthMiddleware;
// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = AuthInnerMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthInnerMiddleware { service: Rc::new(service) }))
}
}
pub struct AuthInnerMiddleware<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
where
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output=Result<Self::Response, Self::Error>>>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
println!("Hi from start. You requested: {}", req.path());
let service = Rc::clone(&self.service);
// Forward request
Box::pin(async move {
if req.path().starts_with("/.git") {
return Ok(req.into_response(
HttpResponse::Unauthorized()
.body("Hey don't touch this!")
.map_into_right_body()
));
}
service
.call(req)
.await
.map(ServiceResponse::map_into_left_body)
})
}
}

1
src/middlewares/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod auth_middleware;