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