actix_identity/
middleware.rs1use 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#[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 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#[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}