Skip to main content

rainier_auth/
guard.rs

1//! Guards — [`Guard`], [`TokenGuard`], [`SessionGuard`] and [`AuthManager`].
2//!
3//! A guard answers one question: *who is making this request?* How it answers
4//! is the guard's business — a bearer token, a session cookie, a signed header
5//! — and the rest of the application only sees the answer.
6//!
7//! Guards are **stateless with respect to the request**: `user(request)` reads
8//! the request and returns, rather than caching "the current user" in the
9//! guard. A guard is a shared singleton serving every concurrent request, so
10//! a `current_user` field on it would be a race, not a convenience. The
11//! resolved user is stashed in the request's own extensions instead, by the
12//! [`Authenticate`](crate::Authenticate) middleware.
13
14use std::collections::HashMap;
15use std::sync::Arc;
16
17use rainier_http::Request;
18use rainier_session::SessionRequestExt;
19use rainier_support::{BoxFuture, Error, Result};
20
21use crate::session::AUTH_KEY;
22use crate::user::{Authenticatable, Credentials, UserProvider};
23
24/// Identifies the user behind a request.
25pub trait Guard<U: Authenticatable>: Send + Sync + 'static {
26    /// A label for diagnostics — `"session"`, `"api"`.
27    fn name(&self) -> &str;
28
29    /// The user this request belongs to, or `None` if it is unauthenticated.
30    ///
31    /// A *failure* to determine that (the database is down) is an `Err`;
32    /// "nobody is logged in" is `Ok(None)`. Conflating the two would turn an
33    /// outage into a silent mass logout.
34    fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>>;
35}
36
37/// A guard that can also log a user in and out — a session, as opposed to a
38/// stateless token.
39///
40/// Every method takes the request, because the session belongs to it. The
41/// cookie is not this trait's business: writing it is
42/// [`StartSession`](rainier_session::StartSession)'s job, and having two
43/// things write one cookie is how they end up disagreeing.
44pub trait StatefulGuard<U: Authenticatable>: Guard<U> {
45    /// Verify credentials and, on success, log the user into this request's
46    /// session. Returns the user, or `None` if the credentials were wrong.
47    fn attempt<'a>(
48        &'a self,
49        request: &'a Request,
50        credentials: &'a Credentials,
51    ) -> BoxFuture<'a, Result<Option<U>>>;
52
53    /// Log an already-verified user into this request's session.
54    fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>>;
55
56    /// End the session behind this request.
57    fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>>;
58}
59
60/// Extension methods every guard gets. Separate from [`Guard`] so that trait
61/// stays object-safe.
62#[async_trait::async_trait]
63pub trait GuardExt<U: Authenticatable>: Guard<U> {
64    /// Whether the request is authenticated.
65    async fn check(&self, request: &Request) -> Result<bool> {
66        Ok(self.user(request).await?.is_some())
67    }
68
69    /// Whether the request is *not* authenticated.
70    async fn guest(&self, request: &Request) -> Result<bool> {
71        Ok(!self.check(request).await?)
72    }
73
74    /// The authenticated user's identifier.
75    async fn id(&self, request: &Request) -> Result<Option<String>> {
76        Ok(self.user(request).await?.map(|user| user.auth_identifier()))
77    }
78
79    /// The authenticated user, failing with a `401` if there is none.
80    async fn authenticate(&self, request: &Request) -> Result<U> {
81        self.user(request).await?.ok_or_else(|| Error::unauthenticated("Unauthenticated."))
82    }
83}
84
85impl<U: Authenticatable, G: Guard<U> + ?Sized> GuardExt<U> for G {}
86
87/// Authenticates by `Authorization: Bearer …`.
88///
89/// Stateless: nothing is stored, so there is nothing to log out of. Revoking a
90/// token means changing it on the user row.
91pub struct TokenGuard<U: Authenticatable> {
92    name: String,
93    provider: Arc<dyn UserProvider<U>>,
94}
95
96impl<U: Authenticatable> TokenGuard<U> {
97    /// A token guard named `name`.
98    pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
99        Self { name: name.into(), provider }
100    }
101}
102
103impl<U: Authenticatable> Guard<U> for TokenGuard<U> {
104    fn name(&self) -> &str {
105        &self.name
106    }
107
108    fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>> {
109        Box::pin(async move {
110            // Accept the token from `Authorization: Bearer` or an
111            // `api_token` input, as Laravel's token guard does.
112            let token =
113                request.bearer_token().map(str::to_string).or_else(|| request.input("api_token"));
114
115            match token {
116                Some(token) if !token.is_empty() => self.provider.retrieve_by_token(&token).await,
117                _ => Ok(None),
118            }
119        })
120    }
121}
122
123/// Authenticates from the request's session.
124///
125/// Requires the [`StartSession`](rainier_session::StartSession) middleware to
126/// have run — without it there is no session, and this guard reports nobody
127/// rather than pretending.
128pub struct SessionGuard<U: Authenticatable> {
129    name: String,
130    provider: Arc<dyn UserProvider<U>>,
131}
132
133impl<U: Authenticatable> SessionGuard<U> {
134    /// A session guard named `name`.
135    pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
136        Self { name: name.into(), provider }
137    }
138
139    /// The user provider this guard looks users up through.
140    pub fn provider(&self) -> &Arc<dyn UserProvider<U>> {
141        &self.provider
142    }
143}
144
145impl<U: Authenticatable> Guard<U> for SessionGuard<U> {
146    fn name(&self) -> &str {
147        &self.name
148    }
149
150    fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>> {
151        Box::pin(async move {
152            let Some(session) = request.session() else {
153                // The route is not behind `StartSession`. Nobody is logged in
154                // here, and saying so is better than a 500 — but it is almost
155                // always a wiring mistake, so it is worth a line in the log.
156                tracing::debug!(
157                    guard = %self.name,
158                    "a session guard ran on a route with no session middleware"
159                );
160                return Ok(None);
161            };
162
163            let Some(user_id) = session.string(AUTH_KEY) else {
164                return Ok(None);
165            };
166
167            self.provider.retrieve_by_id(&user_id).await
168        })
169    }
170}
171
172impl<U: Authenticatable> StatefulGuard<U> for SessionGuard<U> {
173    fn attempt<'a>(
174        &'a self,
175        request: &'a Request,
176        credentials: &'a Credentials,
177    ) -> BoxFuture<'a, Result<Option<U>>> {
178        Box::pin(async move {
179            let Some(user) = self.provider.retrieve_by_credentials(credentials).await? else {
180                return Ok(None);
181            };
182            if !self.provider.validate_credentials(&user, credentials).await? {
183                return Ok(None);
184            }
185
186            self.login(request, &user).await?;
187            Ok(Some(user))
188        })
189    }
190
191    fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>> {
192        Box::pin(async move {
193            let session = request
194                .session()
195                .ok_or_else(|| Error::internal("logging in needs the session middleware"))?;
196
197            // Before writing the identity, not after: an attacker who fixed
198            // the pre-login session id must not end up holding a cookie for
199            // the authenticated one.
200            session.regenerate();
201            session.put(AUTH_KEY, user.auth_identifier())?;
202            Ok(())
203        })
204    }
205
206    fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>> {
207        Box::pin(async move {
208            if let Some(session) = request.session() {
209                // Everything, not just the auth key: a cart or a half-filled
210                // form left behind by a logout is the next person at that
211                // browser reading it.
212                session.invalidate();
213            }
214            Ok(())
215        })
216    }
217}
218
219/// Resolves named guards — Laravel's `Auth::guard('api')`.
220///
221/// One user type per manager. An application with genuinely different user
222/// kinds (customers and staff, with different tables) wants one manager each,
223/// which is also clearer than a single registry returning different shapes.
224pub struct AuthManager<U: Authenticatable> {
225    guards: HashMap<String, Arc<dyn Guard<U>>>,
226    default: String,
227}
228
229impl<U: Authenticatable> AuthManager<U> {
230    /// A manager whose default guard is `default`.
231    pub fn new(default: impl Into<String>) -> Self {
232        Self { guards: HashMap::new(), default: default.into() }
233    }
234
235    /// Register a guard under its own name.
236    pub fn register(mut self, guard: Arc<dyn Guard<U>>) -> Self {
237        self.guards.insert(guard.name().to_string(), guard);
238        self
239    }
240
241    /// The guard named `name`.
242    pub fn guard(&self, name: &str) -> Result<&Arc<dyn Guard<U>>> {
243        self.guards.get(name).ok_or_else(|| {
244            Error::internal(format!(
245                "no authentication guard named `{name}` — registered guards: {:?}",
246                self.guard_names()
247            ))
248        })
249    }
250
251    /// The default guard.
252    pub fn default_guard(&self) -> Result<&Arc<dyn Guard<U>>> {
253        self.guard(&self.default)
254    }
255
256    /// The default guard's name.
257    pub fn default_name(&self) -> &str {
258        &self.default
259    }
260
261    /// Every registered guard's name.
262    pub fn guard_names(&self) -> Vec<&str> {
263        let mut names: Vec<&str> = self.guards.keys().map(String::as_str).collect();
264        names.sort();
265        names
266    }
267
268    /// The user behind `request`, via the default guard.
269    pub async fn user(&self, request: &Request) -> Result<Option<U>> {
270        self.default_guard()?.user(request).await
271    }
272
273    /// The user behind `request`, via a named guard.
274    pub async fn user_via(&self, name: &str, request: &Request) -> Result<Option<U>> {
275        self.guard(name)?.user(request).await
276    }
277}
278
279impl<U: Authenticatable> std::fmt::Debug for AuthManager<U> {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.debug_struct("AuthManager")
282            .field("default", &self.default)
283            .field("guards", &self.guard_names())
284            .finish()
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::session::Session;
292    use rainier_support::Result as R;
293
294    #[derive(Debug, Clone, PartialEq)]
295    struct User {
296        id: u64,
297        email: String,
298        password: String,
299        token: Option<String>,
300    }
301
302    impl Authenticatable for User {
303        fn auth_identifier(&self) -> String {
304            self.id.to_string()
305        }
306        fn auth_password_hash(&self) -> Option<&str> {
307            Some(&self.password)
308        }
309    }
310
311    fn ada() -> User {
312        User {
313            id: 42,
314            email: "ada@example.com".into(),
315            password: "plain:secret".into(),
316            token: Some("tok_123".into()),
317        }
318    }
319
320    /// A provider over one in-memory user, verifying a `plain:` prefix rather
321    /// than running a real hash — the hashing itself is tested elsewhere.
322    struct FakeProvider {
323        user: Option<User>,
324        fail: bool,
325    }
326
327    impl FakeProvider {
328        fn holding(user: User) -> Arc<dyn UserProvider<User>> {
329            Arc::new(Self { user: Some(user), fail: false })
330        }
331        fn empty() -> Arc<dyn UserProvider<User>> {
332            Arc::new(Self { user: None, fail: false })
333        }
334        fn broken() -> Arc<dyn UserProvider<User>> {
335            Arc::new(Self { user: None, fail: true })
336        }
337    }
338
339    #[async_trait::async_trait]
340    impl UserProvider<User> for FakeProvider {
341        async fn retrieve_by_id(&self, id: &str) -> R<Option<User>> {
342            if self.fail {
343                return Err(Error::internal("database is down"));
344            }
345            Ok(self.user.clone().filter(|u| u.auth_identifier() == id))
346        }
347
348        async fn retrieve_by_credentials(&self, credentials: &Credentials) -> R<Option<User>> {
349            if self.fail {
350                return Err(Error::internal("database is down"));
351            }
352            Ok(self.user.clone().filter(|u| credentials.get("email") == Some(&u.email)))
353        }
354
355        async fn validate_credentials(&self, user: &User, credentials: &Credentials) -> R<bool> {
356            Ok(credentials.password_value().is_some_and(|p| user.password == format!("plain:{p}")))
357        }
358
359        async fn retrieve_by_token(&self, token: &str) -> R<Option<User>> {
360            if self.fail {
361                return Err(Error::internal("database is down"));
362            }
363            Ok(self.user.clone().filter(|u| u.token.as_deref() == Some(token)))
364        }
365    }
366
367    fn bearer(token: &str) -> Request {
368        Request::builder().header("authorization", &format!("Bearer {token}")).build()
369    }
370
371    // --- token guard -------------------------------------------------------
372
373    #[tokio::test]
374    async fn a_token_guard_resolves_a_bearer_token() {
375        let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
376
377        let user = guard.user(&bearer("tok_123")).await.unwrap();
378        assert_eq!(user.unwrap().id, 42);
379        assert!(guard.check(&bearer("tok_123")).await.unwrap());
380        assert_eq!(guard.id(&bearer("tok_123")).await.unwrap().as_deref(), Some("42"));
381    }
382
383    #[tokio::test]
384    async fn a_token_guard_rejects_an_unknown_or_absent_token() {
385        let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
386
387        assert!(guard.user(&bearer("wrong")).await.unwrap().is_none());
388        assert!(guard.user(&Request::builder().build()).await.unwrap().is_none());
389        assert!(guard.guest(&Request::builder().build()).await.unwrap());
390    }
391
392    #[tokio::test]
393    async fn a_token_guard_also_accepts_an_api_token_input() {
394        let guard = TokenGuard::new("api", FakeProvider::holding(ada()));
395        let request = Request::builder().uri("/x?api_token=tok_123").build();
396
397        assert!(guard.check(&request).await.unwrap());
398    }
399
400    #[tokio::test]
401    async fn authenticate_fails_with_a_401_for_a_guest() {
402        let guard = TokenGuard::new("api", FakeProvider::empty());
403        let err = guard.authenticate(&Request::builder().build()).await.unwrap_err();
404        assert_eq!(err.status(), 401);
405    }
406
407    #[tokio::test]
408    async fn a_backend_failure_is_an_error_not_a_silent_logout() {
409        // The distinction that matters: an outage must not read as "everyone
410        // is a guest", which would quietly drop authorisation everywhere.
411        let guard = TokenGuard::new("api", FakeProvider::broken());
412        assert!(guard.user(&bearer("tok_123")).await.is_err());
413        assert!(guard.check(&bearer("tok_123")).await.is_err());
414    }
415
416    // --- session guard -----------------------------------------------------
417
418    fn session_guard() -> SessionGuard<User> {
419        SessionGuard::new("web", FakeProvider::holding(ada()))
420    }
421
422    /// A request carrying a session, as `StartSession` would have left it.
423    fn with_session() -> Request {
424        Request::builder().build().with_extension(Session::new())
425    }
426
427    #[tokio::test]
428    async fn a_session_guard_logs_in_and_resolves_the_user() {
429        let guard = session_guard();
430        let request = with_session();
431
432        let user = guard
433            .attempt(&request, &Credentials::password("ada@example.com", "secret"))
434            .await
435            .unwrap()
436            .expect("correct credentials should log in");
437
438        assert_eq!(user.id, 42);
439        assert_eq!(guard.user(&request).await.unwrap().unwrap().id, 42);
440    }
441
442    #[tokio::test]
443    async fn logging_in_rotates_the_session_id() {
444        // Session fixation: an attacker who set the pre-login cookie must not
445        // be holding the authenticated session's id afterwards.
446        let guard = session_guard();
447        let request = with_session();
448        let before = request.session().unwrap().id();
449
450        guard.login(&request, &ada()).await.unwrap();
451
452        assert_ne!(request.session().unwrap().id(), before);
453    }
454
455    #[tokio::test]
456    async fn a_session_guard_rejects_a_wrong_password() {
457        let guard = session_guard();
458        let request = with_session();
459
460        assert!(guard
461            .attempt(&request, &Credentials::password("ada@example.com", "wrong"))
462            .await
463            .unwrap()
464            .is_none());
465        assert!(guard.guest(&request).await.unwrap(), "a failed attempt must not log anyone in");
466    }
467
468    #[tokio::test]
469    async fn a_session_guard_rejects_an_unknown_user() {
470        let guard = session_guard();
471        let request = with_session();
472
473        assert!(guard
474            .attempt(&request, &Credentials::password("nobody@example.com", "secret"))
475            .await
476            .unwrap()
477            .is_none());
478    }
479
480    #[tokio::test]
481    async fn logging_out_empties_the_session_and_rotates_it() {
482        let guard = session_guard();
483        let request = with_session();
484        guard.login(&request, &ada()).await.unwrap();
485
486        let before = request.session().unwrap().id();
487        request.session().unwrap().put("cart", vec![1u64]).unwrap();
488
489        assert!(guard.check(&request).await.unwrap());
490        guard.logout(&request).await.unwrap();
491
492        assert!(guard.guest(&request).await.unwrap());
493        assert!(
494            !request.session().unwrap().has("cart"),
495            "a logout must not leave the next person at this browser the cart"
496        );
497        assert_ne!(request.session().unwrap().id(), before);
498    }
499
500    #[tokio::test]
501    async fn a_request_with_no_session_is_a_guest_rather_than_an_error() {
502        // The route is not behind `StartSession`. Reporting nobody is the
503        // right answer; a 500 would take down a page that merely forgot it.
504        let guard = session_guard();
505        assert!(guard.guest(&Request::builder().build()).await.unwrap());
506    }
507
508    #[tokio::test]
509    async fn an_empty_session_is_a_guest() {
510        let guard = session_guard();
511        assert!(guard.guest(&with_session()).await.unwrap());
512    }
513
514    #[tokio::test]
515    async fn logging_out_without_a_session_is_not_an_error() {
516        let guard = session_guard();
517        assert!(guard.logout(&Request::builder().build()).await.is_ok());
518    }
519
520    #[tokio::test]
521    async fn logging_in_without_a_session_says_what_is_missing() {
522        let guard = session_guard();
523        let err = guard.login(&Request::builder().build(), &ada()).await.unwrap_err();
524
525        assert!(err.message().contains("session middleware"), "{}", err.message());
526    }
527
528    // --- manager -----------------------------------------------------------
529
530    #[tokio::test]
531    async fn the_manager_resolves_guards_by_name() {
532        let manager = AuthManager::<User>::new("web")
533            .register(Arc::new(session_guard()))
534            .register(Arc::new(TokenGuard::new("api", FakeProvider::holding(ada()))));
535
536        assert_eq!(manager.guard_names(), vec!["api", "web"]);
537        assert_eq!(manager.default_name(), "web");
538        assert_eq!(manager.default_guard().unwrap().name(), "web");
539        assert_eq!(manager.guard("api").unwrap().name(), "api");
540    }
541
542    #[tokio::test]
543    async fn an_unknown_guard_name_lists_the_known_ones() {
544        let manager = AuthManager::<User>::new("web").register(Arc::new(session_guard()));
545
546        let err = manager.guard("nope").err().expect("should fail");
547        assert!(err.message().contains("`nope`"), "{}", err.message());
548        assert!(err.message().contains("web"), "{}", err.message());
549    }
550
551    #[tokio::test]
552    async fn the_manager_authenticates_through_the_chosen_guard() {
553        let manager = AuthManager::<User>::new("api")
554            .register(Arc::new(TokenGuard::new("api", FakeProvider::holding(ada()))));
555
556        let request = bearer("tok_123");
557        assert_eq!(manager.user(&request).await.unwrap().unwrap().id, 42);
558        assert_eq!(manager.user_via("api", &request).await.unwrap().unwrap().id, 42);
559    }
560
561    #[tokio::test]
562    async fn a_missing_default_guard_is_reported_when_used() {
563        let manager = AuthManager::<User>::new("web");
564        assert!(manager.user(&Request::builder().build()).await.is_err());
565    }
566}