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,
};
#[derive(Default, Clone)]
pub struct IdentityMiddleware {
configuration: Rc<Configuration>,
}
impl IdentityMiddleware {
pub(crate) fn new(configuration: Configuration) -> Self {
Self {
configuration: Rc::new(configuration),
}
}
pub fn builder() -> IdentityMiddlewareBuilder {
IdentityMiddlewareBuilder::new()
}
}
impl<S, B> Transform<S, ServiceRequest> for IdentityMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = InnerIdentityMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
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<S> {
service: Rc<S>,
configuration: Rc<Configuration>,
}
impl<S> Clone for InnerIdentityMiddleware<S> {
fn clone(&self) -> Self {
Self {
service: Rc::clone(&self.service),
configuration: Rc::clone(&self.configuration),
}
}
}
impl<S, B> Service<ServiceRequest> for InnerIdentityMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
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(),
id_key: configuration.id_key,
last_visit_unix_timestamp_key: configuration.last_visit_unix_timestamp_key,
login_unix_timestamp_key: configuration.login_unix_timestamp_key,
};
req.extensions_mut().insert(identity_inner);
enforce_policies(&req, &configuration);
srv.call(req).await
})
}
}
#[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,
}