Skip to main content

actix_web_admin/auth/
middleware.rs

1use actix_session::SessionExt;
2use actix_web::body::BoxBody;
3use actix_web::{
4    dev::{Service, ServiceRequest, ServiceResponse, Transform},
5    Error, HttpResponse,
6};
7use std::future::{ready, Ready};
8use std::pin::Pin;
9use std::rc::Rc;
10use std::task::{Context, Poll};
11
12/// Middleware that requires authentication for admin routes.
13///
14/// Excludes login and logout paths. Redirects unauthenticated users
15/// to the login page.
16pub struct RequireAuth {
17    admin_prefix: String,
18}
19
20impl RequireAuth {
21    pub fn new(admin_prefix: &str) -> Self {
22        RequireAuth {
23            admin_prefix: admin_prefix.trim_end_matches('/').to_string(),
24        }
25    }
26}
27
28impl<S> Transform<S, ServiceRequest> for RequireAuth
29where
30    S: Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> + 'static,
31    S::Future: 'static,
32{
33    type Response = ServiceResponse<BoxBody>;
34    type Error = Error;
35    type Transform = RequireAuthMiddleware<S>;
36    type InitError = ();
37    type Future = Ready<Result<Self::Transform, Self::InitError>>;
38
39    fn new_transform(&self, service: S) -> Self::Future {
40        ready(Ok(RequireAuthMiddleware {
41            service: Rc::new(service),
42            login_url: format!("{}/login", self.admin_prefix),
43            admin_prefix: self.admin_prefix.clone(),
44        }))
45    }
46}
47
48pub struct RequireAuthMiddleware<S> {
49    service: Rc<S>,
50    login_url: String,
51    admin_prefix: String,
52}
53
54impl<S> Service<ServiceRequest> for RequireAuthMiddleware<S>
55where
56    S: Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> + 'static,
57    S::Future: 'static,
58{
59    type Response = ServiceResponse<BoxBody>;
60    type Error = Error;
61    type Future = Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>>;
62
63    fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
64        self.service.poll_ready(cx)
65    }
66
67    fn call(&self, req: ServiceRequest) -> Self::Future {
68        let svc = self.service.clone();
69        let path = req.path().to_string();
70        let login_url = self.login_url.clone();
71        let admin_prefix = self.admin_prefix.clone();
72        let is_public = path == format!("{admin_prefix}/login")
73            || path == format!("{admin_prefix}/login/")
74            || path == format!("{admin_prefix}/logout")
75            || path == format!("{admin_prefix}/logout/");
76
77        if is_public {
78            return Box::pin(async move { svc.call(req).await });
79        }
80
81        let session = req.get_session();
82        let is_auth = session
83            .get::<String>("admin_user")
84            .ok()
85            .flatten()
86            .is_some();
87
88        if is_auth {
89            Box::pin(async move { svc.call(req).await })
90        } else {
91            let response = HttpResponse::Found()
92                .insert_header(("Location", login_url))
93                .finish();
94            Box::pin(async move { Ok(req.into_response(response)) })
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use actix_web::{test, web, App};
103    use actix_session::{storage::CookieSessionStore, SessionMiddleware};
104    use actix_web::cookie::Key;
105
106    async fn test_app() -> impl actix_web::dev::Service<
107        actix_http::Request,
108        Response = actix_web::dev::ServiceResponse,
109        Error = actix_web::Error,
110    > {
111        test::init_service(
112            App::new()
113                .wrap(SessionMiddleware::new(
114                    CookieSessionStore::default(),
115                    Key::generate(),
116                ))
117                .service(
118                    web::scope("/admin")
119                        .wrap(RequireAuth::new("/admin"))
120                        .route("/login", web::get().to(|| async { HttpResponse::Ok().body("login page") }))
121                        .route("/dashboard", web::get().to(|| async { HttpResponse::Ok().body("dashboard") })),
122                ),
123        )
124        .await
125    }
126
127    #[actix_web::test]
128    async fn test_public_login_page() {
129        let app = test_app().await;
130        let req = test::TestRequest::get().uri("/admin/login").to_request();
131        let resp = test::call_service(&app, req).await;
132        assert!(resp.status().is_success(), "login page should be public");
133    }
134
135    #[actix_web::test]
136    async fn test_protected_route_redirects() {
137        let app = test_app().await;
138        let req = test::TestRequest::get()
139            .uri("/admin/dashboard")
140            .to_request();
141        let resp = test::call_service(&app, req).await;
142        assert_eq!(
143            resp.status(),
144            302,
145            "unauthenticated requests should redirect"
146        );
147        let location = resp.response().headers().get("Location");
148        assert!(location.is_some(), "should have Location header");
149        assert_eq!(
150            location.unwrap().to_str().unwrap(),
151            "/admin/login",
152            "should redirect to login"
153        );
154    }
155
156}