actix_identity/
middleware.rs

1use std::rc::Rc;
2
3use actix_session::SessionExt;
4use actix_utils::future::{ready, Ready};
5use actix_web::{
6    body::MessageBody,
7    cookie::time::{format_description::well_known::Rfc3339, OffsetDateTime},
8    dev::{Service, ServiceRequest, ServiceResponse, Transform},
9    Error, HttpMessage as _, Result,
10};
11use futures_core::future::LocalBoxFuture;
12
13use crate::{
14    config::{Configuration, IdentityMiddlewareBuilder},
15    identity::IdentityInner,
16    Identity,
17};
18
19/// Identity management middleware.
20///
21/// ```no_run
22/// use actix_web::{cookie::Key, App, HttpServer};
23/// use actix_session::storage::RedisSessionStore;
24/// use actix_identity::{Identity, IdentityMiddleware};
25/// use actix_session::{Session, SessionMiddleware};
26///
27/// #[actix_web::main]
28/// async fn main() {
29///     let secret_key = Key::generate();
30///     let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379").await.unwrap();
31///
32///     HttpServer::new(move || {
33///        App::new()
34///            // Install the identity framework first.
35///            .wrap(IdentityMiddleware::default())
36///            // The identity system is built on top of sessions.
37///            // You must install the session middleware to leverage `actix-identity`.
38///            .wrap(SessionMiddleware::new(redis_store.clone(), secret_key.clone()))
39///     })
40/// # ;
41/// }
42/// ```
43#[derive(Default, Clone)]
44pub struct IdentityMiddleware {
45    configuration: Rc<Configuration>,
46}
47
48impl IdentityMiddleware {
49    pub(crate) fn new(configuration: Configuration) -> Self {
50        Self {
51            configuration: Rc::new(configuration),
52        }
53    }
54
55    /// A fluent API to configure [`IdentityMiddleware`].
56    pub fn builder() -> IdentityMiddlewareBuilder {
57        IdentityMiddlewareBuilder::new()
58    }
59}
60
61impl<S, B> Transform<S, ServiceRequest> for IdentityMiddleware
62where
63    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
64    S::Future: 'static,
65    B: MessageBody + 'static,
66{
67    type Response = ServiceResponse<B>;
68    type Error = Error;
69    type Transform = InnerIdentityMiddleware<S>;
70    type InitError = ();
71    type Future = Ready<Result<Self::Transform, Self::InitError>>;
72
73    fn new_transform(&self, service: S) -> Self::Future {
74        ready(Ok(InnerIdentityMiddleware {
75            service: Rc::new(service),
76            configuration: Rc::clone(&self.configuration),
77        }))
78    }
79}
80
81#[doc(hidden)]
82pub struct InnerIdentityMiddleware<S> {
83    service: Rc<S>,
84    configuration: Rc<Configuration>,
85}
86
87impl<S> Clone for InnerIdentityMiddleware<S> {
88    fn clone(&self) -> Self {
89        Self {
90            service: Rc::clone(&self.service),
91            configuration: Rc::clone(&self.configuration),
92        }
93    }
94}
95
96impl<S, B> Service<ServiceRequest> for InnerIdentityMiddleware<S>
97where
98    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
99    S::Future: 'static,
100    B: MessageBody + 'static,
101{
102    type Response = ServiceResponse<B>;
103    type Error = Error;
104    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
105
106    actix_service::forward_ready!(service);
107
108    fn call(&self, req: ServiceRequest) -> Self::Future {
109        let srv = Rc::clone(&self.service);
110        let configuration = Rc::clone(&self.configuration);
111        Box::pin(async move {
112            let identity_inner = IdentityInner {
113                session: req.get_session(),
114                logout_behavior: configuration.on_logout.clone(),
115                is_login_deadline_enabled: configuration.login_deadline.is_some(),
116                is_visit_deadline_enabled: configuration.visit_deadline.is_some(),
117                id_key: configuration.id_key,
118                last_visit_unix_timestamp_key: configuration.last_visit_unix_timestamp_key,
119                login_unix_timestamp_key: configuration.login_unix_timestamp_key,
120            };
121            req.extensions_mut().insert(identity_inner);
122            enforce_policies(&req, &configuration);
123            srv.call(req).await
124        })
125    }
126}
127
128// easier to scan with returns where they are
129// especially if the function body were to evolve in the future
130#[allow(clippy::needless_return)]
131fn enforce_policies(req: &ServiceRequest, configuration: &Configuration) {
132    let must_extract_identity =
133        configuration.login_deadline.is_some() || configuration.visit_deadline.is_some();
134
135    if !must_extract_identity {
136        return;
137    }
138
139    let identity = match Identity::extract(&req.extensions()) {
140        Ok(identity) => identity,
141        Err(err) => {
142            tracing::debug!(
143                error.display = %err,
144                error.debug = ?err,
145                "Failed to extract an `Identity` from the incoming request."
146            );
147            return;
148        }
149    };
150
151    if let Some(login_deadline) = configuration.login_deadline {
152        if matches!(
153            enforce_login_deadline(&identity, login_deadline),
154            PolicyDecision::LogOut
155        ) {
156            identity.logout();
157            return;
158        }
159    }
160
161    if let Some(visit_deadline) = configuration.visit_deadline {
162        if matches!(
163            enforce_visit_deadline(&identity, visit_deadline),
164            PolicyDecision::LogOut
165        ) {
166            identity.logout();
167            return;
168        } else if let Err(err) = identity.set_last_visited_at() {
169            tracing::warn!(
170                error.display = %err,
171                error.debug = ?err,
172                "Failed to set the last visited timestamp on `Identity` for an incoming request."
173            );
174        }
175    }
176}
177
178fn enforce_login_deadline(
179    identity: &Identity,
180    login_deadline: std::time::Duration,
181) -> PolicyDecision {
182    match identity.logged_at() {
183        Ok(None) => {
184            tracing::info!(
185                "Login deadline is enabled, but there is no login timestamp in the session \
186                state attached to the incoming request. Logging the user out."
187            );
188            PolicyDecision::LogOut
189        }
190        Err(err) => {
191            tracing::info!(
192                error.display = %err,
193                error.debug = ?err,
194                "Login deadline is enabled but we failed to extract the login timestamp from the \
195                session state attached to the incoming request. Logging the user out."
196            );
197            PolicyDecision::LogOut
198        }
199        Ok(Some(logged_in_at)) => {
200            let elapsed = OffsetDateTime::now_utc() - logged_in_at;
201            if elapsed > login_deadline {
202                tracing::info!(
203                    user.logged_in_at = %logged_in_at.format(&Rfc3339).unwrap_or_default(),
204                    identity.login_deadline_seconds = login_deadline.as_secs(),
205                    identity.elapsed_since_login_seconds = elapsed.whole_seconds(),
206                    "Login deadline is enabled and too much time has passed since the user logged \
207                    in. Logging the user out."
208                );
209                PolicyDecision::LogOut
210            } else {
211                PolicyDecision::StayLoggedIn
212            }
213        }
214    }
215}
216
217fn enforce_visit_deadline(
218    identity: &Identity,
219    visit_deadline: std::time::Duration,
220) -> PolicyDecision {
221    match identity.last_visited_at() {
222        Ok(None) => {
223            tracing::info!(
224                "Last visit deadline is enabled, but there is no last visit timestamp in the \
225                session state attached to the incoming request. Logging the user out."
226            );
227            PolicyDecision::LogOut
228        }
229        Err(err) => {
230            tracing::info!(
231                error.display = %err,
232                error.debug = ?err,
233                "Last visit deadline is enabled but we failed to extract the last visit timestamp \
234                from the session state attached to the incoming request. Logging the user out."
235            );
236            PolicyDecision::LogOut
237        }
238        Ok(Some(last_visited_at)) => {
239            let elapsed = OffsetDateTime::now_utc() - last_visited_at;
240            if elapsed > visit_deadline {
241                tracing::info!(
242                    user.last_visited_at = %last_visited_at.format(&Rfc3339).unwrap_or_default(),
243                    identity.visit_deadline_seconds = visit_deadline.as_secs(),
244                    identity.elapsed_since_last_visit_seconds = elapsed.whole_seconds(),
245                    "Last visit deadline is enabled and too much time has passed since the last \
246                    time the user visited. Logging the user out."
247                );
248                PolicyDecision::LogOut
249            } else {
250                PolicyDecision::StayLoggedIn
251            }
252        }
253    }
254}
255
256enum PolicyDecision {
257    StayLoggedIn,
258    LogOut,
259}