1use 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
24pub trait Guard<U: Authenticatable>: Send + Sync + 'static {
26 fn name(&self) -> &str;
28
29 fn user<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Option<U>>>;
35}
36
37pub trait StatefulGuard<U: Authenticatable>: Guard<U> {
45 fn attempt<'a>(
48 &'a self,
49 request: &'a Request,
50 credentials: &'a Credentials,
51 ) -> BoxFuture<'a, Result<Option<U>>>;
52
53 fn login<'a>(&'a self, request: &'a Request, user: &'a U) -> BoxFuture<'a, Result<()>>;
55
56 fn logout<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<()>>;
58}
59
60#[async_trait::async_trait]
63pub trait GuardExt<U: Authenticatable>: Guard<U> {
64 async fn check(&self, request: &Request) -> Result<bool> {
66 Ok(self.user(request).await?.is_some())
67 }
68
69 async fn guest(&self, request: &Request) -> Result<bool> {
71 Ok(!self.check(request).await?)
72 }
73
74 async fn id(&self, request: &Request) -> Result<Option<String>> {
76 Ok(self.user(request).await?.map(|user| user.auth_identifier()))
77 }
78
79 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
87pub struct TokenGuard<U: Authenticatable> {
92 name: String,
93 provider: Arc<dyn UserProvider<U>>,
94}
95
96impl<U: Authenticatable> TokenGuard<U> {
97 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 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
123pub struct SessionGuard<U: Authenticatable> {
129 name: String,
130 provider: Arc<dyn UserProvider<U>>,
131}
132
133impl<U: Authenticatable> SessionGuard<U> {
134 pub fn new(name: impl Into<String>, provider: Arc<dyn UserProvider<U>>) -> Self {
136 Self { name: name.into(), provider }
137 }
138
139 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 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 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 session.invalidate();
213 }
214 Ok(())
215 })
216 }
217}
218
219pub struct AuthManager<U: Authenticatable> {
225 guards: HashMap<String, Arc<dyn Guard<U>>>,
226 default: String,
227}
228
229impl<U: Authenticatable> AuthManager<U> {
230 pub fn new(default: impl Into<String>) -> Self {
232 Self { guards: HashMap::new(), default: default.into() }
233 }
234
235 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 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 pub fn default_guard(&self) -> Result<&Arc<dyn Guard<U>>> {
253 self.guard(&self.default)
254 }
255
256 pub fn default_name(&self) -> &str {
258 &self.default
259 }
260
261 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 pub async fn user(&self, request: &Request) -> Result<Option<U>> {
270 self.default_guard()?.user(request).await
271 }
272
273 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 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 #[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 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 fn session_guard() -> SessionGuard<User> {
419 SessionGuard::new("web", FakeProvider::holding(ada()))
420 }
421
422 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 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 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 #[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}