Compare commits
	
		
			6 Commits
		
	
	
		
			ad58d2de7e
			...
			9a4c725b4e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9a4c725b4e | |||
| f08fddc79c | |||
| da74acaed8 | |||
| 91fd763fe1 | |||
| 9e72e6a044 | |||
| cb4daa1112 | 
							
								
								
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -395,6 +395,7 @@ dependencies = [ | ||||
|  "mime_guess", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "urlencoding", | ||||
|  "uuid", | ||||
| ] | ||||
|  | ||||
| @@ -1625,6 +1626,12 @@ dependencies = [ | ||||
|  "percent-encoding", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "urlencoding" | ||||
| version = "2.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" | ||||
|  | ||||
| [[package]] | ||||
| name = "uuid" | ||||
| version = "0.8.2" | ||||
|   | ||||
| @@ -19,4 +19,5 @@ bcrypt = "0.12.1" | ||||
| uuid = { version = "0.8.2", features = ["v4"] } | ||||
| mime_guess = "2.0.4" | ||||
| askama = "0.11.1" | ||||
| futures-util = "0.3.21" | ||||
| futures-util = "0.3.21" | ||||
| urlencoding = "2.1.0" | ||||
| @@ -9,13 +9,22 @@ pub const DEFAULT_ADMIN_PASSWORD: &str = "admin"; | ||||
| pub const APP_NAME: &str = "Basic OIDC"; | ||||
|  | ||||
| /// Maximum session duration after inactivity, in seconds | ||||
| pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30; | ||||
| pub const MAX_INACTIVITY_DURATION: i64 = 60 * 30; | ||||
|  | ||||
| /// Minimum interval between each last activity record in session | ||||
| pub const MIN_ACTIVITY_RECORD_TIME: u64 = 10; | ||||
| /// Maximum session duration (6 hours) | ||||
| pub const MAX_SESSION_DURATION: i64 = 3600 * 6; | ||||
|  | ||||
| /// Minimum password length | ||||
| pub const MIN_PASS_LEN: usize = 4; | ||||
|  | ||||
| /// Maximum session duration (6 hours) | ||||
| pub const MAX_SESSION_DURATION: u64 = 3600 * 6; | ||||
| /// The name of the cookie used to store session information | ||||
| pub const SESSION_COOKIE_NAME: &str = "auth-cookie"; | ||||
|  | ||||
| /// Authenticated routes prefix | ||||
| pub const AUTHENTICATED_ROUTES: &str = "/settings"; | ||||
|  | ||||
| /// Admin routes prefix | ||||
| pub const ADMIN_ROUTES: &str = "/admin"; | ||||
|  | ||||
| /// Auth route | ||||
| pub const LOGIN_ROUTE: &str = "/login"; | ||||
| @@ -1,8 +1,22 @@ | ||||
| use std::fmt::Display; | ||||
|  | ||||
| use actix_web::HttpResponse; | ||||
|  | ||||
| use crate::constants::LOGIN_ROUTE; | ||||
|  | ||||
| /// Create a redirect user response | ||||
| pub fn redirect_user(uri: &str) -> HttpResponse { | ||||
|     HttpResponse::Found() | ||||
|         .append_header(("Location", uri)) | ||||
|         .finish() | ||||
| } | ||||
| } | ||||
|  | ||||
| /// Redirect user to authenticate him | ||||
| pub fn redirect_user_for_login<P: Display>(redirect_uri: P) -> HttpResponse { | ||||
|     HttpResponse::Found() | ||||
|         .append_header(( | ||||
|             "Location", | ||||
|             format!("{}?redirect={}", LOGIN_ROUTE, urlencoding::encode(&redirect_uri.to_string())) | ||||
|         )) | ||||
|         .finish() | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,7 @@ struct BaseLoginPage { | ||||
|     success: String, | ||||
|     page_title: &'static str, | ||||
|     app_name: &'static str, | ||||
|     redirect_uri: String, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| @@ -41,6 +42,7 @@ pub struct LoginRequestBody { | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct LoginRequestQuery { | ||||
|     logout: Option<bool>, | ||||
|     redirect: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Authenticate user | ||||
| @@ -52,6 +54,14 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | ||||
|     let mut success = String::new(); | ||||
|     let mut login = String::new(); | ||||
|  | ||||
|     let redirect_uri = match query.redirect.as_deref() { | ||||
|         None => "/", | ||||
|         Some(s) => match s.starts_with('/') && !s.starts_with("//") { | ||||
|             true => s, | ||||
|             false => "/", | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Check if user session must be closed | ||||
|     if let Some(true) = query.logout { | ||||
|         id.forget(); | ||||
| @@ -60,7 +70,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | ||||
|  | ||||
|     // Check if user is already authenticated | ||||
|     if SessionIdentity(&id).is_authenticated() { | ||||
|         return redirect_user("/"); | ||||
|         return redirect_user(redirect_uri); | ||||
|     } | ||||
|  | ||||
|     // Check if user is setting a new  password | ||||
| @@ -78,7 +88,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | ||||
|                 danger = "Failed to change password!".to_string(); | ||||
|             } else { | ||||
|                 SessionIdentity(&id).set_status(SessionStatus::SignedIn); | ||||
|                 return redirect_user("/"); | ||||
|                 return redirect_user(redirect_uri); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -100,7 +110,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | ||||
|                 if user.need_reset_password { | ||||
|                     SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); | ||||
|                 } else { | ||||
|                     return redirect_user("/"); | ||||
|                     return redirect_user(redirect_uri); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -122,6 +132,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | ||||
|                     danger, | ||||
|                     success, | ||||
|                     app_name: APP_NAME, | ||||
|                     redirect_uri: urlencoding::encode(redirect_uri).to_string(), | ||||
|                 }, | ||||
|                 min_pass_len: MIN_PASS_LEN, | ||||
|             }.render().unwrap()); | ||||
| @@ -136,6 +147,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | ||||
|                 danger, | ||||
|                 success, | ||||
|                 app_name: APP_NAME, | ||||
|                 redirect_uri: urlencoding::encode(redirect_uri).to_string(), | ||||
|             }, | ||||
|             login, | ||||
|         }.render().unwrap()) | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| use actix_identity::Identity; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::constants::{MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, MIN_ACTIVITY_RECORD_TIME}; | ||||
| use crate::data::user::{User, UserID}; | ||||
| use crate::utils::time::time; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] | ||||
| pub enum SessionStatus { | ||||
| @@ -21,30 +19,21 @@ impl Default for SessionStatus { | ||||
|  | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Default)] | ||||
| struct SessionIdentityData { | ||||
| pub struct SessionIdentityData { | ||||
|     pub id: UserID, | ||||
|     pub is_admin: bool, | ||||
|     login_time: u64, | ||||
|     last_access: u64, | ||||
|     pub status: SessionStatus, | ||||
| } | ||||
|  | ||||
| pub struct SessionIdentity<'a>(pub &'a Identity); | ||||
|  | ||||
| impl<'a> SessionIdentity<'a> { | ||||
|     pub fn set_user(&self, user: &User) { | ||||
|         self.set_session_data(&SessionIdentityData { | ||||
|             id: user.uid.clone(), | ||||
|             is_admin: user.admin, | ||||
|             login_time: time(), | ||||
|             last_access: time(), | ||||
|             status: SessionStatus::SignedIn, | ||||
|         }); | ||||
|     fn get_session_data(&self) -> Option<SessionIdentityData> { | ||||
|         Self::deserialize_session_data(self.0.identity()) | ||||
|     } | ||||
|  | ||||
|     fn get_session_data(&self) -> Option<SessionIdentityData> { | ||||
|         let mut res: Option<SessionIdentityData> = self.0.identity() | ||||
|             .as_deref() | ||||
|     pub fn deserialize_session_data(data: Option<String>) -> Option<SessionIdentityData> { | ||||
|         let res: Option<SessionIdentityData> = data.as_deref() | ||||
|             .map(serde_json::from_str) | ||||
|             .map(|f| match f { | ||||
|                 Ok(d) => Some(d), | ||||
| @@ -62,26 +51,6 @@ impl<'a> SessionIdentity<'a> { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let Some(session) = res.as_mut() { | ||||
|             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); | ||||
|                 self.0.forget(); | ||||
|                 return None; | ||||
|             } | ||||
|  | ||||
|             if session.last_access + MIN_ACTIVITY_RECORD_TIME < time() { | ||||
|                 log::debug!("Refresh last access for session"); | ||||
|                 session.last_access = time(); | ||||
|                 self.set_session_data(session); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         res | ||||
|     } | ||||
|  | ||||
| @@ -92,6 +61,14 @@ impl<'a> SessionIdentity<'a> { | ||||
|         self.0.remember(s); | ||||
|     } | ||||
|  | ||||
|     pub fn set_user(&self, user: &User) { | ||||
|         self.set_session_data(&SessionIdentityData { | ||||
|             id: user.uid.clone(), | ||||
|             is_admin: user.admin, | ||||
|             status: SessionStatus::SignedIn, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     pub fn set_status(&self, status: SessionStatus) { | ||||
|         let mut sess = self.get_session_data().unwrap_or_default(); | ||||
|         sess.status = status; | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,11 +1,13 @@ | ||||
| use actix::Actor; | ||||
| use actix_identity::{CookieIdentityPolicy, IdentityService}; | ||||
| use actix_web::{App, get, HttpServer, web}; | ||||
| use actix_web::cookie::SameSite; | ||||
| use actix_web::cookie::time::Duration; | ||||
| use actix_web::middleware::Logger; | ||||
| 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, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME}; | ||||
| use basic_oidc::controllers::assets_controller::assets_route; | ||||
| use basic_oidc::controllers::login_controller::{login_route, logout_route}; | ||||
| use basic_oidc::data::app_config::AppConfig; | ||||
| @@ -63,16 +65,19 @@ async fn main() -> std::io::Result<()> { | ||||
|  | ||||
|     HttpServer::new(move || { | ||||
|         let policy = CookieIdentityPolicy::new(config.token_key.as_bytes()) | ||||
|             .name("auth-cookie") | ||||
|             .secure(config.secure_auth_cookie); | ||||
|             .name(SESSION_COOKIE_NAME) | ||||
|             .secure(config.secure_auth_cookie) | ||||
|             .visit_deadline(Duration::seconds(MAX_INACTIVITY_DURATION)) | ||||
|             .login_deadline(Duration::seconds(MAX_SESSION_DURATION)) | ||||
|             .same_site(SameSite::Strict); | ||||
|  | ||||
|  | ||||
|         App::new() | ||||
|             .app_data(web::Data::new(users_actor.clone())) | ||||
|  | ||||
|             .wrap(Logger::default()) | ||||
|             .wrap(IdentityService::new(policy)) | ||||
|             .wrap(AuthMiddleware {}) | ||||
|             .wrap(IdentityService::new(policy)) | ||||
|  | ||||
|             // /health route | ||||
|             .service(health) | ||||
|   | ||||
| @@ -4,8 +4,14 @@ use std::future::{Future, ready, Ready}; | ||||
| use std::pin::Pin; | ||||
| use std::rc::Rc; | ||||
|  | ||||
| use actix_identity::RequestIdentity; | ||||
| use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpResponse}; | ||||
| use actix_web::body::EitherBody; | ||||
| use askama::Template; | ||||
|  | ||||
| use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES}; | ||||
| use crate::controllers::base_controller::redirect_user_for_login; | ||||
| use crate::data::session_identity::{SessionIdentity, SessionIdentityData}; | ||||
|  | ||||
| // There are two steps in middleware processing. | ||||
| // 1. Middleware initialization, middleware factory gets called with | ||||
| @@ -33,6 +39,27 @@ impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| enum SessionStatus { | ||||
|     SignedOut, | ||||
|     RegularUser, | ||||
|     Admin, | ||||
| } | ||||
|  | ||||
| impl SessionStatus { | ||||
|     pub fn is_auth(&self) -> bool { | ||||
|         !matches!(self, SessionStatus::SignedOut) | ||||
|     } | ||||
|  | ||||
|     pub fn is_admin(&self) -> bool { | ||||
|         matches!(self, SessionStatus::Admin) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "access_denied.html")] | ||||
| struct AccessDeniedTemplate {} | ||||
|  | ||||
| pub struct AuthInnerMiddleware<S> { | ||||
|     service: Rc<S>, | ||||
| } | ||||
| @@ -45,13 +72,13 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S> | ||||
| { | ||||
|     type Response = ServiceResponse<EitherBody<B>>; | ||||
|     type Error = Error; | ||||
|  | ||||
|     #[allow(clippy::type_complexity)] | ||||
|     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 | ||||
| @@ -64,6 +91,27 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S> | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             let identity = match SessionIdentity::deserialize_session_data(req.get_identity()) { | ||||
|                 None => SessionStatus::SignedOut, | ||||
|                 Some(SessionIdentityData { is_admin: true, .. }) => SessionStatus::Admin, | ||||
|                 _ => SessionStatus::RegularUser, | ||||
|             }; | ||||
|  | ||||
|             // Redirect user to login page | ||||
|             if !identity.is_auth() && (req.path().starts_with(ADMIN_ROUTES) || | ||||
|                 req.path().starts_with(AUTHENTICATED_ROUTES)) { | ||||
|                 let path = req.path().to_string(); | ||||
|                 return Ok(req.into_response(redirect_user_for_login(path)) | ||||
|                     .map_into_right_body()); | ||||
|             } | ||||
|  | ||||
|             // Restrict access to admin pages | ||||
|             if !identity.is_admin() && req.path().starts_with(ADMIN_ROUTES) { | ||||
|                 return Ok(req.into_response(HttpResponse::Unauthorized() | ||||
|                     .body(AccessDeniedTemplate {}.render().unwrap())) | ||||
|                     .map_into_right_body()); | ||||
|             } | ||||
|  | ||||
|             service | ||||
|                 .call(req) | ||||
|                 .await | ||||
|   | ||||
							
								
								
									
										12
									
								
								templates/access_denied.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								templates/access_denied.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <title>Access denied</title> | ||||
|  | ||||
|     <link href="/assets/css/bootstrap.css" rel="stylesheet" crossorigin="anonymous"/> | ||||
| </head> | ||||
| <body> | ||||
|     <p>You are not allowed to access this resource.</p> | ||||
| </body> | ||||
| </html> | ||||
| @@ -1,6 +1,6 @@ | ||||
| {% extends "base_login_page.html" %} | ||||
| {% block content %} | ||||
| <form action="/login" method="post"> | ||||
| <form action="/login?redirect={{ redirect_uri }}" method="post"> | ||||
|     <div> | ||||
|         <div class="form-floating"> | ||||
|             <input name="login" type="text" required class="form-control" id="floatingName" placeholder="unsername" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| {% extends "base_login_page.html" %} | ||||
| {% block content %} | ||||
| <form action="/login" method="post" id="reset_password_form"> | ||||
| <form action="/login?redirect={{ redirect_uri }}" method="post" id="reset_password_form"> | ||||
|     <div> | ||||
|         <p>You need to configure a new password:</p> | ||||
|  | ||||
| @@ -11,13 +11,13 @@ | ||||
|  | ||||
|         <div class="form-floating"> | ||||
|             <input name="password" type="password" required class="form-control" id="pass1" | ||||
|                    placeholder="unsername"/> | ||||
|                    placeholder="Password"/> | ||||
|             <label for="pass1">New password</label> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-floating"> | ||||
|             <input type="password" required class="form-control" id="pass2" | ||||
|                    placeholder="Password"/> | ||||
|                    placeholder="Password confirmation"/> | ||||
|             <label for="pass2">Confirm new password</label> | ||||
|         </div> | ||||
|     </div> | ||||
| @@ -47,6 +47,7 @@ | ||||
|             form.submit(); | ||||
|     }) | ||||
|  | ||||
|  | ||||
| </script> | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user