Skip to main content

rainier_auth/
middleware.rs

1//! The [`Authenticate`] middleware — `->middleware("auth")`.
2//!
3//! Resolves the user once, puts them in the request's extensions, and rejects
4//! the request if there is nobody there. Doing it in middleware rather than in
5//! each controller means the guard runs exactly once per request, and a handler
6//! that is behind `auth` can read [`AuthenticatedUser`] without re-checking.
7
8use std::sync::Arc;
9
10use rainier_http::{FromRequest, IntoResponse, Request, Response};
11use rainier_middleware::{Middleware, MiddlewareStack, Next};
12use rainier_support::{BoxedFuture, Error, Result};
13
14use crate::guard::AuthManager;
15use crate::user::Authenticatable;
16
17/// The user resolved for this request, stored in its extensions.
18///
19/// A newtype rather than a bare `U` so it cannot be confused with any other
20/// value of the same type an application might attach.
21#[derive(Debug)]
22pub struct AuthenticatedUser<U>(pub Arc<U>);
23
24/// Cloning shares the user rather than copying it, so this holds whatever `U`
25/// is — a derived `Clone` would have demanded `U: Clone` for no reason.
26impl<U> Clone for AuthenticatedUser<U> {
27    fn clone(&self) -> Self {
28        Self(Arc::clone(&self.0))
29    }
30}
31
32impl<U> AuthenticatedUser<U> {
33    /// The user.
34    pub fn get(&self) -> &U {
35        &self.0
36    }
37}
38
39impl<U> std::ops::Deref for AuthenticatedUser<U> {
40    type Target = U;
41
42    fn deref(&self) -> &U {
43        &self.0
44    }
45}
46
47/// The authenticated user, as a **controller parameter**.
48///
49/// Laravel injects the user; this is that, and it is why an action can read
50///
51/// ```ignore
52/// pub async fn me(user: AuthenticatedUser<User>) -> Result<Response> {
53///     Ok(Response::json(user.get()))
54/// }
55/// ```
56///
57/// rather than taking the whole request and digging the user out of it.
58///
59/// Fails with `401` when there is none — which can only happen on a route with
60/// no [`Authenticate`] middleware in front, so the message says so rather than
61/// blaming the caller for a wiring mistake.
62impl<U: Send + Sync + 'static> FromRequest for AuthenticatedUser<U> {
63    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
64        Box::pin(async move {
65            request
66                .extension::<AuthenticatedUser<U>>()
67                .cloned()
68                .ok_or_else(|| Error::unauthenticated("Unauthenticated."))
69        })
70    }
71}
72
73/// Rejects unauthenticated requests, and attaches the user to authenticated
74/// ones.
75pub struct Authenticate<U: Authenticatable> {
76    auth: Arc<AuthManager<U>>,
77    /// The guard to use, or `None` for the manager's default — this is the
78    /// `api` in `->middleware("auth:api")`.
79    guard: Option<String>,
80}
81
82impl<U: Authenticatable> Authenticate<U> {
83    /// Authenticate with the manager's default guard.
84    pub fn new(auth: Arc<AuthManager<U>>) -> Self {
85        Self { auth, guard: None }
86    }
87
88    /// Authenticate with a named guard.
89    pub fn with_guard(auth: Arc<AuthManager<U>>, guard: impl Into<String>) -> Self {
90        Self { auth, guard: Some(guard.into()) }
91    }
92
93    /// A stack that builds this middleware from the container.
94    ///
95    /// Routes are declared before the container is populated, so the
96    /// `AuthManager` cannot be handed over at declaration time. This defers the
97    /// resolve to when the router compiles:
98    ///
99    /// ```ignore
100    /// router.get("/me", me).middleware(Authenticate::<User>::resolved());
101    /// ```
102    ///
103    /// The replacement for Laravel's `->middleware('auth')`. The user type is
104    /// named explicitly, which is the thing a string alias could never say.
105    pub fn resolved() -> MiddlewareStack {
106        MiddlewareStack::new().resolved(|auth: Arc<AuthManager<U>>| Self::new(auth))
107    }
108
109    /// [`resolved`](Self::resolved) with a named guard — `->middleware('auth:api')`.
110    pub fn resolved_with_guard(guard: impl Into<String>) -> MiddlewareStack {
111        let guard = guard.into();
112        MiddlewareStack::new()
113            .resolved(move |auth: Arc<AuthManager<U>>| Self::with_guard(auth, guard.clone()))
114    }
115}
116
117#[async_trait::async_trait]
118impl<U: Authenticatable> Middleware for Authenticate<U> {
119    async fn handle(&self, mut request: Request, next: Next) -> Response {
120        let resolved = match &self.guard {
121            Some(name) => self.auth.user_via(name, &request).await,
122            None => self.auth.user(&request).await,
123        };
124
125        match resolved {
126            Ok(Some(user)) => {
127                request.extensions_mut().insert(AuthenticatedUser(Arc::new(user)));
128                next.run(request).await
129            }
130            Ok(None) => Error::unauthenticated("Unauthenticated.").into_response(),
131            // A guard failure is a 500, not a 401: the user may well be
132            // authenticated, and telling them they are not would be a lie that
133            // sends them to a login page which will also fail.
134            Err(e) => {
135                tracing::error!(error = %e, "the authentication guard failed");
136                e.into_response()
137            }
138        }
139    }
140
141    fn name(&self) -> &'static str {
142        "Authenticate"
143    }
144}
145
146/// Rejects *authenticated* requests — the `guest` middleware, for a login page
147/// an already-logged-in user should not see.
148pub struct RedirectIfAuthenticated<U: Authenticatable> {
149    auth: Arc<AuthManager<U>>,
150    to: String,
151}
152
153impl<U: Authenticatable> RedirectIfAuthenticated<U> {
154    /// Send authenticated users to `to`.
155    pub fn new(auth: Arc<AuthManager<U>>, to: impl Into<String>) -> Self {
156        Self { auth, to: to.into() }
157    }
158}
159
160#[async_trait::async_trait]
161impl<U: Authenticatable> Middleware for RedirectIfAuthenticated<U> {
162    async fn handle(&self, request: Request, next: Next) -> Response {
163        match self.auth.user(&request).await {
164            Ok(Some(_)) => rainier_http::Redirect::to(self.to.clone()).into_response(),
165            // A guard failure here is not a reason to block a login attempt —
166            // failing open to the login page is the safe direction.
167            Ok(None) | Err(_) => next.run(request).await,
168        }
169    }
170
171    fn name(&self) -> &'static str {
172        "RedirectIfAuthenticated"
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::guard::TokenGuard;
180    use crate::user::{Credentials, UserProvider};
181    use rainier_http::StatusCode;
182    use rainier_middleware::Pipeline;
183    use rainier_support::Result;
184
185    #[derive(Debug, Clone, PartialEq)]
186    struct User {
187        id: u64,
188    }
189
190    impl Authenticatable for User {
191        fn auth_identifier(&self) -> String {
192            self.id.to_string()
193        }
194        fn auth_password_hash(&self) -> Option<&str> {
195            None
196        }
197    }
198
199    struct Provider {
200        fail: bool,
201    }
202
203    #[async_trait::async_trait]
204    impl UserProvider<User> for Provider {
205        async fn retrieve_by_id(&self, _: &str) -> Result<Option<User>> {
206            Ok(None)
207        }
208        async fn retrieve_by_credentials(&self, _: &Credentials) -> Result<Option<User>> {
209            Ok(None)
210        }
211        async fn validate_credentials(&self, _: &User, _: &Credentials) -> Result<bool> {
212            Ok(false)
213        }
214        async fn retrieve_by_token(&self, token: &str) -> Result<Option<User>> {
215            if self.fail {
216                return Err(Error::internal("database is down"));
217            }
218            Ok((token == "good").then_some(User { id: 7 }))
219        }
220    }
221
222    fn manager(fail: bool) -> Arc<AuthManager<User>> {
223        Arc::new(
224            AuthManager::new("api")
225                .register(Arc::new(TokenGuard::new("api", Arc::new(Provider { fail })))),
226        )
227    }
228
229    async fn reveal_user(request: Request) -> Response {
230        match request.extension::<AuthenticatedUser<User>>() {
231            Some(user) => Response::text(user.id.to_string()),
232            None => Response::text("anonymous"),
233        }
234    }
235
236    async fn body_of(response: Response) -> String {
237        String::from_utf8(response.into_http().into_body().collect().await.unwrap().to_vec())
238            .unwrap()
239    }
240
241    fn bearer(token: &str) -> Request {
242        Request::builder().header("authorization", &format!("Bearer {token}")).build()
243    }
244
245    #[tokio::test]
246    async fn an_authenticated_request_reaches_the_handler_with_its_user() {
247        let response = Pipeline::new()
248            .through(Authenticate::new(manager(false)))
249            .then(reveal_user)
250            .run(bearer("good"))
251            .await;
252
253        assert_eq!(response.status(), StatusCode::OK);
254        assert_eq!(body_of(response).await, "7");
255    }
256
257    #[tokio::test]
258    async fn an_unauthenticated_request_is_rejected_before_the_handler() {
259        let response = Pipeline::new()
260            .through(Authenticate::new(manager(false)))
261            .then(|_: Request| async { panic!("the handler must not run") })
262            .run(bearer("bad"))
263            .await;
264
265        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
266    }
267
268    #[tokio::test]
269    async fn a_guard_failure_is_a_500_not_a_401() {
270        // Telling an authenticated user they are not sends them to a login
271        // page that will also fail. Report the outage instead.
272        let response = Pipeline::new()
273            .through(Authenticate::new(manager(true)))
274            .then(reveal_user)
275            .run(bearer("good"))
276            .await;
277
278        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
279    }
280
281    #[tokio::test]
282    async fn a_named_guard_can_be_selected() {
283        let middleware = Authenticate::with_guard(manager(false), "api");
284        let response =
285            Pipeline::new().through(middleware).then(reveal_user).run(bearer("good")).await;
286
287        assert_eq!(body_of(response).await, "7");
288    }
289
290    #[tokio::test]
291    async fn an_unknown_named_guard_fails_loudly() {
292        let middleware = Authenticate::with_guard(manager(false), "nope");
293        let response =
294            Pipeline::new().through(middleware).then(reveal_user).run(bearer("good")).await;
295
296        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
297    }
298
299    #[tokio::test]
300    async fn the_guest_middleware_redirects_an_authenticated_user() {
301        let response = Pipeline::new()
302            .through(RedirectIfAuthenticated::new(manager(false), "/dashboard"))
303            .then(|_: Request| async { Response::text("login form") })
304            .run(bearer("good"))
305            .await;
306
307        assert_eq!(response.status(), StatusCode::FOUND);
308        assert_eq!(response.header("location"), Some("/dashboard"));
309    }
310
311    #[tokio::test]
312    async fn the_guest_middleware_lets_a_guest_through() {
313        let response = Pipeline::new()
314            .through(RedirectIfAuthenticated::new(manager(false), "/dashboard"))
315            .then(|_: Request| async { Response::text("login form") })
316            .run(Request::builder().build())
317            .await;
318
319        assert_eq!(body_of(response).await, "login form");
320    }
321
322    #[tokio::test]
323    async fn the_guest_middleware_fails_open_to_the_login_page() {
324        // If the guard is broken, blocking the login page helps nobody.
325        let response = Pipeline::new()
326            .through(RedirectIfAuthenticated::new(manager(true), "/dashboard"))
327            .then(|_: Request| async { Response::text("login form") })
328            .run(bearer("good"))
329            .await;
330
331        assert_eq!(body_of(response).await, "login form");
332    }
333}