use std::rc::Rc; use actix_session::SessionExt; use actix_utils::future::{ready, Ready}; use actix_web::{ body::MessageBody, cookie::time::{format_description::well_known::Rfc3339, OffsetDateTime}, dev::{Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage as _, Result, }; use futures_core::future::LocalBoxFuture; use crate::{ config::{Configuration, IdentityMiddlewareBuilder}, identity::IdentityInner, Identity, }; /// Identity management middleware. /// /// ```no_run /// use actix_web::{cookie::Key, App, HttpServer}; /// use actix_session::storage::RedisSessionStore; /// use actix_identity::{Identity, IdentityMiddleware}; /// use actix_session::{Session, SessionMiddleware}; /// /// #[actix_web::main] /// async fn main() { /// let secret_key = Key::generate(); /// let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379").await.unwrap(); /// /// HttpServer::new(move || { /// App::new() /// // Install the identity framework first. /// .wrap(IdentityMiddleware::default()) /// // The identity system is built on top of sessions. /// // You must install the session middleware to leverage `actix-identity`. /// .wrap(SessionMiddleware::new(redis_store.clone(), secret_key.clone())) /// }) /// # ; /// } /// ``` #[derive(Default, Clone)] pub struct IdentityMiddleware { configuration: Rc, } impl IdentityMiddleware { pub(crate) fn new(configuration: Configuration) -> Self { Self { configuration: Rc::new(configuration), } } /// A fluent API to configure [`IdentityMiddleware`]. pub fn builder() -> IdentityMiddlewareBuilder { IdentityMiddlewareBuilder::new() } } impl Transform for IdentityMiddleware where S: Service, Error = Error> + 'static, S::Future: 'static, B: MessageBody + 'static, { type Response = ServiceResponse; type Error = Error; type Transform = InnerIdentityMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(InnerIdentityMiddleware { service: Rc::new(service), configuration: Rc::clone(&self.configuration), })) } } #[doc(hidden)] pub struct InnerIdentityMiddleware { service: Rc, configuration: Rc, } impl Clone for InnerIdentityMiddleware { fn clone(&self) -> Self { Self { service: Rc::clone(&self.service), configuration: Rc::clone(&self.configuration), } } } impl Service for InnerIdentityMiddleware where S: Service, Error = Error> + 'static, S::Future: 'static, B: MessageBody + 'static, { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let srv = Rc::clone(&self.service); let configuration = Rc::clone(&self.configuration); Box::pin(async move { let identity_inner = IdentityInner { session: req.get_session(), logout_behaviour: configuration.on_logout.clone(), is_login_deadline_enabled: configuration.login_deadline.is_some(), is_visit_deadline_enabled: configuration.visit_deadline.is_some(), }; req.extensions_mut().insert(identity_inner); enforce_policies(&req, &configuration); srv.call(req).await }) } } // easier to scan with returns where they are // especially if the function body were to evolve in the future #[allow(clippy::needless_return)] fn enforce_policies(req: &ServiceRequest, configuration: &Configuration) { let must_extract_identity = configuration.login_deadline.is_some() || configuration.visit_deadline.is_some(); if !must_extract_identity { return; } let identity = match Identity::extract(&req.extensions()) { Ok(identity) => identity, Err(err) => { tracing::debug!( error.display = %err, error.debug = ?err, "Failed to extract an `Identity` from the incoming request." ); return; } }; if let Some(login_deadline) = configuration.login_deadline { if matches!( enforce_login_deadline(&identity, login_deadline), PolicyDecision::LogOut ) { identity.logout(); return; } } if let Some(visit_deadline) = configuration.visit_deadline { if matches!( enforce_visit_deadline(&identity, visit_deadline), PolicyDecision::LogOut ) { identity.logout(); return; } else if let Err(err) = identity.set_last_visited_at() { tracing::warn!( error.display = %err, error.debug = ?err, "Failed to set the last visited timestamp on `Identity` for an incoming request." ); } } } fn enforce_login_deadline( identity: &Identity, login_deadline: std::time::Duration, ) -> PolicyDecision { match identity.logged_at() { Ok(None) => { tracing::info!( "Login deadline is enabled, but there is no login timestamp in the session \ state attached to the incoming request. Logging the user out." ); PolicyDecision::LogOut } Err(err) => { tracing::info!( error.display = %err, error.debug = ?err, "Login deadline is enabled but we failed to extract the login timestamp from the \ session state attached to the incoming request. Logging the user out." ); PolicyDecision::LogOut } Ok(Some(logged_in_at)) => { let elapsed = OffsetDateTime::now_utc() - logged_in_at; if elapsed > login_deadline { tracing::info!( user.logged_in_at = %logged_in_at.format(&Rfc3339).unwrap_or_default(), identity.login_deadline_seconds = login_deadline.as_secs(), identity.elapsed_since_login_seconds = elapsed.whole_seconds(), "Login deadline is enabled and too much time has passed since the user logged \ in. Logging the user out." ); PolicyDecision::LogOut } else { PolicyDecision::StayLoggedIn } } } } fn enforce_visit_deadline( identity: &Identity, visit_deadline: std::time::Duration, ) -> PolicyDecision { match identity.last_visited_at() { Ok(None) => { tracing::info!( "Last visit deadline is enabled, but there is no last visit timestamp in the \ session state attached to the incoming request. Logging the user out." ); PolicyDecision::LogOut } Err(err) => { tracing::info!( error.display = %err, error.debug = ?err, "Last visit deadline is enabled but we failed to extract the last visit timestamp \ from the session state attached to the incoming request. Logging the user out." ); PolicyDecision::LogOut } Ok(Some(last_visited_at)) => { let elapsed = OffsetDateTime::now_utc() - last_visited_at; if elapsed > visit_deadline { tracing::info!( user.last_visited_at = %last_visited_at.format(&Rfc3339).unwrap_or_default(), identity.visit_deadline_seconds = visit_deadline.as_secs(), identity.elapsed_since_last_visit_seconds = elapsed.whole_seconds(), "Last visit deadline is enabled and too much time has passed since the last \ time the user visited. Logging the user out." ); PolicyDecision::LogOut } else { PolicyDecision::StayLoggedIn } } } } enum PolicyDecision { StayLoggedIn, LogOut, }