Skip to main content

actix_web_httpauth/
middleware.rs

1//! HTTP Authentication middleware.
2
3use std::{
4    future::Future,
5    marker::PhantomData,
6    pin::Pin,
7    rc::Rc,
8    sync::Arc,
9    task::{Context, Poll},
10};
11
12use actix_web::{
13    body::{EitherBody, MessageBody},
14    dev::{Service, ServiceRequest, ServiceResponse, Transform},
15    Error, FromRequest,
16};
17use futures_core::ready;
18use futures_util::future::{self, LocalBoxFuture, TryFutureExt as _};
19
20use crate::extractors::{basic, bearer};
21
22/// Middleware for checking HTTP authentication.
23///
24/// By default, if the extractor `T` fails (for example because the `Authorization` header is
25/// missing), this middleware returns an error immediately, without calling the `F` callback.
26///
27/// To make authentication optional (or to implement multiple auth methods), wrap the extractor
28/// in `Option<T>` or `Result<T, T::Error>`. In those cases, extraction never fails, so the
29/// validator always runs and can decide how to proceed.
30///
31/// Otherwise, it will pass both the request and the parsed credentials into it. In case of
32/// successful validation `F` callback is required to return the `ServiceRequest` back.
33#[derive(Debug, Clone)]
34pub struct HttpAuthentication<T, F>
35where
36    T: FromRequest,
37{
38    process_fn: Arc<F>,
39    _extractor: PhantomData<T>,
40}
41
42impl<T, F, O> HttpAuthentication<T, F>
43where
44    T: FromRequest,
45    F: Fn(ServiceRequest, T) -> O,
46    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>>,
47{
48    /// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and
49    /// validation callback `F`.
50    ///
51    /// This function can be used to implement optional authentication and/or custom responses to
52    /// missing authentication.
53    ///
54    /// # Examples
55    ///
56    /// ## Required Basic Auth
57    ///
58    /// ```no_run
59    /// # use actix_web_httpauth::extractors::basic::BasicAuth;
60    /// # use actix_web::dev::ServiceRequest;
61    /// async fn validator(
62    ///     req: ServiceRequest,
63    ///     credentials: BasicAuth,
64    /// ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
65    ///     eprintln!("{credentials:?}");
66    ///
67    ///     if credentials.user_id().contains('x') {
68    ///         return Err((actix_web::error::ErrorBadRequest("user ID contains x"), req));
69    ///     }
70    ///
71    ///     Ok(req)
72    /// }
73    /// # actix_web_httpauth::middleware::HttpAuthentication::with_fn(validator);
74    /// ```
75    ///
76    /// ## Optional Bearer Auth (fallback to other auth methods)
77    ///
78    /// ```no_run
79    /// # use actix_web_httpauth::extractors::bearer::BearerAuth;
80    /// # use actix_web::dev::ServiceRequest;
81    /// async fn validator(
82    ///     req: ServiceRequest,
83    ///     credentials: Option<BearerAuth>,
84    /// ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
85    ///     let Some(credentials) = credentials else {
86    ///         // No Authorization header; allow other auth methods (eg cookies/sessions) to proceed.
87    ///         return Ok(req);
88    ///     };
89    ///
90    ///     eprintln!("{credentials:?}");
91    ///
92    ///     if credentials.token().contains('x') {
93    ///         return Err((actix_web::error::ErrorBadRequest("token contains x"), req));
94    ///     }
95    ///
96    ///     Ok(req)
97    /// }
98    /// # actix_web_httpauth::middleware::HttpAuthentication::with_fn(validator);
99    /// ```
100    pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
101        HttpAuthentication {
102            process_fn: Arc::new(process_fn),
103            _extractor: PhantomData,
104        }
105    }
106}
107
108impl<F, O> HttpAuthentication<basic::BasicAuth, F>
109where
110    F: Fn(ServiceRequest, basic::BasicAuth) -> O,
111    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>>,
112{
113    /// Construct `HttpAuthentication` middleware for the HTTP "Basic" authentication scheme.
114    ///
115    /// # Examples
116    /// ```
117    /// # use actix_web::{Error, dev::ServiceRequest};
118    /// # use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication};
119    /// // In this example validator returns immediately, but since it is required to return
120    /// // anything that implements `IntoFuture` trait, it can be extended to query database or to
121    /// // do something else in a async manner.
122    /// async fn validator(
123    ///     req: ServiceRequest,
124    ///     credentials: BasicAuth,
125    /// ) -> Result<ServiceRequest, (Error, ServiceRequest)> {
126    ///     // All users are great and more than welcome!
127    ///     Ok(req)
128    /// }
129    ///
130    /// let middleware = HttpAuthentication::basic(validator);
131    /// ```
132    pub fn basic(process_fn: F) -> Self {
133        Self::with_fn(process_fn)
134    }
135}
136
137impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
138where
139    F: Fn(ServiceRequest, bearer::BearerAuth) -> O,
140    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>>,
141{
142    /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" authentication scheme.
143    ///
144    /// # Examples
145    /// ```
146    /// # use actix_web::{Error, dev::ServiceRequest};
147    /// # use actix_web_httpauth::{
148    /// #     extractors::{AuthenticationError, AuthExtractorConfig, bearer::{self, BearerAuth}},
149    /// #     middleware::HttpAuthentication,
150    /// # };
151    /// async fn validator(
152    ///     req: ServiceRequest,
153    ///     credentials: BearerAuth
154    /// ) -> Result<ServiceRequest, (Error, ServiceRequest)> {
155    ///     if credentials.token() == "mF_9.B5f-4.1JqM" {
156    ///         Ok(req)
157    ///     } else {
158    ///         let config = req.app_data::<bearer::Config>()
159    ///             .cloned()
160    ///             .unwrap_or_default()
161    ///             .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
162    ///
163    ///         Err((AuthenticationError::from(config).into(), req))
164    ///     }
165    /// }
166    ///
167    /// let middleware = HttpAuthentication::bearer(validator);
168    /// ```
169    pub fn bearer(process_fn: F) -> Self {
170        Self::with_fn(process_fn)
171    }
172}
173
174impl<S, B, T, F, O> Transform<S, ServiceRequest> for HttpAuthentication<T, F>
175where
176    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
177    S::Future: 'static,
178    F: Fn(ServiceRequest, T) -> O + 'static,
179    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>> + 'static,
180    T: FromRequest + 'static,
181    B: MessageBody + 'static,
182{
183    type Response = ServiceResponse<EitherBody<B>>;
184    type Error = Error;
185    type Transform = AuthenticationMiddleware<S, F, T>;
186    type InitError = ();
187    type Future = future::Ready<Result<Self::Transform, Self::InitError>>;
188
189    fn new_transform(&self, service: S) -> Self::Future {
190        future::ok(AuthenticationMiddleware {
191            service: Rc::new(service),
192            process_fn: self.process_fn.clone(),
193            _extractor: PhantomData,
194        })
195    }
196}
197
198#[doc(hidden)]
199pub struct AuthenticationMiddleware<S, F, T>
200where
201    T: FromRequest,
202{
203    service: Rc<S>,
204    process_fn: Arc<F>,
205    _extractor: PhantomData<T>,
206}
207
208impl<S, B, F, T, O> Service<ServiceRequest> for AuthenticationMiddleware<S, F, T>
209where
210    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
211    S::Future: 'static,
212    F: Fn(ServiceRequest, T) -> O + 'static,
213    O: Future<Output = Result<ServiceRequest, (Error, ServiceRequest)>> + 'static,
214    T: FromRequest + 'static,
215    B: MessageBody + 'static,
216{
217    type Response = ServiceResponse<EitherBody<B>>;
218    type Error = S::Error;
219    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
220
221    actix_web::dev::forward_ready!(service);
222
223    fn call(&self, req: ServiceRequest) -> Self::Future {
224        let process_fn = Arc::clone(&self.process_fn);
225        let service = Rc::clone(&self.service);
226
227        Box::pin(async move {
228            let (req, credentials) = match Extract::<T>::new(req).await {
229                Ok(req) => req,
230                Err((err, req)) => {
231                    return Ok(req.error_response(err).map_into_right_body());
232                }
233            };
234
235            let req = match process_fn(req, credentials).await {
236                Ok(req) => req,
237                Err((err, req)) => {
238                    return Ok(req.error_response(err).map_into_right_body());
239                }
240            };
241
242            service.call(req).await.map(|res| res.map_into_left_body())
243        })
244    }
245}
246
247struct Extract<T> {
248    req: Option<ServiceRequest>,
249    fut: Option<LocalBoxFuture<'static, Result<T, Error>>>,
250    _extractor: PhantomData<fn() -> T>,
251}
252
253impl<T> Extract<T> {
254    pub fn new(req: ServiceRequest) -> Self {
255        Extract {
256            req: Some(req),
257            fut: None,
258            _extractor: PhantomData,
259        }
260    }
261}
262
263impl<T> Future for Extract<T>
264where
265    T: FromRequest,
266    T::Future: 'static,
267    T::Error: 'static,
268{
269    type Output = Result<(ServiceRequest, T), (Error, ServiceRequest)>;
270
271    fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Self::Output> {
272        if self.fut.is_none() {
273            let req = self.req.as_mut().expect("Extract future was polled twice!");
274            let fut = req.extract::<T>().map_err(Into::into);
275            self.fut = Some(Box::pin(fut));
276        }
277
278        let fut = self
279            .fut
280            .as_mut()
281            .expect("Extraction future should be initialized at this point");
282
283        let credentials = ready!(fut.as_mut().poll(ctx)).map_err(|err| {
284            (
285                err,
286                // returning request allows a proper error response to be created
287                self.req.take().expect("Extract future was polled twice!"),
288            )
289        })?;
290
291        let req = self.req.take().expect("Extract future was polled twice!");
292        Poll::Ready(Ok((req, credentials)))
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use actix_service::into_service;
299    use actix_web::{
300        error::{self, ErrorForbidden},
301        http::StatusCode,
302        test::TestRequest,
303        web, App, HttpResponse,
304    };
305
306    use super::*;
307    use crate::extractors::{basic::BasicAuth, bearer::BearerAuth};
308
309    /// This is a test for https://github.com/actix/actix-extras/issues/10
310    #[actix_web::test]
311    async fn test_middleware_panic() {
312        let middleware = AuthenticationMiddleware {
313            service: Rc::new(into_service(|_: ServiceRequest| async move {
314                actix_web::rt::time::sleep(std::time::Duration::from_secs(1)).await;
315                Err::<ServiceResponse, _>(error::ErrorBadRequest("error"))
316            })),
317            process_fn: Arc::new(|req, _: BearerAuth| async { Ok(req) }),
318            _extractor: PhantomData,
319        };
320
321        let req = TestRequest::get()
322            .append_header(("Authorization", "Bearer 1"))
323            .to_srv_request();
324
325        let f = middleware.call(req).await;
326
327        let _res = futures_util::future::lazy(|cx| middleware.poll_ready(cx)).await;
328
329        assert!(f.is_err());
330    }
331
332    /// This is a test for https://github.com/actix/actix-extras/issues/10
333    #[actix_web::test]
334    async fn test_middleware_panic_several_orders() {
335        let middleware = AuthenticationMiddleware {
336            service: Rc::new(into_service(|_: ServiceRequest| async move {
337                actix_web::rt::time::sleep(std::time::Duration::from_secs(1)).await;
338                Err::<ServiceResponse, _>(error::ErrorBadRequest("error"))
339            })),
340            process_fn: Arc::new(|req, _: BearerAuth| async { Ok(req) }),
341            _extractor: PhantomData,
342        };
343
344        let req = TestRequest::get()
345            .append_header(("Authorization", "Bearer 1"))
346            .to_srv_request();
347
348        let f1 = middleware.call(req).await;
349
350        let req = TestRequest::get()
351            .append_header(("Authorization", "Bearer 1"))
352            .to_srv_request();
353
354        let f2 = middleware.call(req).await;
355
356        let req = TestRequest::get()
357            .append_header(("Authorization", "Bearer 1"))
358            .to_srv_request();
359
360        let f3 = middleware.call(req).await;
361
362        let _res = futures_util::future::lazy(|cx| middleware.poll_ready(cx)).await;
363
364        assert!(f1.is_err());
365        assert!(f2.is_err());
366        assert!(f3.is_err());
367    }
368
369    #[actix_web::test]
370    async fn test_middleware_opt_extractor() {
371        let middleware = AuthenticationMiddleware {
372            service: Rc::new(into_service(|req: ServiceRequest| async move {
373                Ok::<ServiceResponse, _>(req.into_response(HttpResponse::Ok().finish()))
374            })),
375            process_fn: Arc::new(|req, auth: Option<BearerAuth>| {
376                assert!(auth.is_none());
377                async { Ok(req) }
378            }),
379            _extractor: PhantomData,
380        };
381
382        let req = TestRequest::get()
383            .append_header(("Authorization996", "Bearer 1"))
384            .to_srv_request();
385
386        let f = middleware.call(req).await;
387
388        let _res = futures_util::future::lazy(|cx| middleware.poll_ready(cx)).await;
389
390        assert!(f.is_ok());
391    }
392
393    #[actix_web::test]
394    async fn test_middleware_res_extractor() {
395        let middleware = AuthenticationMiddleware {
396            service: Rc::new(into_service(|req: ServiceRequest| async move {
397                Ok::<ServiceResponse, _>(req.into_response(HttpResponse::Ok().finish()))
398            })),
399            process_fn: Arc::new(
400                |req, auth: Result<BearerAuth, <BearerAuth as FromRequest>::Error>| {
401                    assert!(auth.is_err());
402                    async { Ok(req) }
403                },
404            ),
405            _extractor: PhantomData,
406        };
407
408        let req = TestRequest::get()
409            .append_header(("Authorization", "BearerLOL"))
410            .to_srv_request();
411
412        let f = middleware.call(req).await;
413
414        let _res = futures_util::future::lazy(|cx| middleware.poll_ready(cx)).await;
415
416        assert!(f.is_ok());
417    }
418
419    #[actix_web::test]
420    async fn test_middleware_works_with_app() {
421        async fn validator(
422            req: ServiceRequest,
423            _credentials: BasicAuth,
424        ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
425            Err((ErrorForbidden("You are not welcome!"), req))
426        }
427        let middleware = HttpAuthentication::basic(validator);
428
429        let srv = actix_web::test::init_service(
430            App::new()
431                .wrap(middleware)
432                .route("/", web::get().to(HttpResponse::Ok)),
433        )
434        .await;
435
436        let req = actix_web::test::TestRequest::with_uri("/")
437            .append_header(("Authorization", "Basic DoNotCare"))
438            .to_request();
439
440        let resp = srv.call(req).await.unwrap();
441        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
442    }
443
444    #[actix_web::test]
445    async fn test_middleware_works_with_scope() {
446        async fn validator(
447            req: ServiceRequest,
448            _credentials: BasicAuth,
449        ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
450            Err((ErrorForbidden("You are not welcome!"), req))
451        }
452        let middleware = actix_web::middleware::Compat::new(HttpAuthentication::basic(validator));
453
454        let srv = actix_web::test::init_service(
455            App::new().service(
456                web::scope("/")
457                    .wrap(middleware)
458                    .route("/", web::get().to(HttpResponse::Ok)),
459            ),
460        )
461        .await;
462
463        let req = actix_web::test::TestRequest::with_uri("/")
464            .append_header(("Authorization", "Basic DontCare"))
465            .to_request();
466
467        let resp = srv.call(req).await.unwrap();
468        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
469    }
470}