rainier_auth/
middleware.rs1use 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::abilities::Abilities;
15use crate::guard::AuthManager;
16use crate::user::Authenticatable;
17
18#[derive(Debug)]
23pub struct AuthenticatedUser<U>(pub Arc<U>);
24
25impl<U> Clone for AuthenticatedUser<U> {
28 fn clone(&self) -> Self {
29 Self(Arc::clone(&self.0))
30 }
31}
32
33impl<U> AuthenticatedUser<U> {
34 pub fn get(&self) -> &U {
36 &self.0
37 }
38}
39
40impl<U> std::ops::Deref for AuthenticatedUser<U> {
41 type Target = U;
42
43 fn deref(&self) -> &U {
44 &self.0
45 }
46}
47
48impl<U: Send + Sync + 'static> FromRequest for AuthenticatedUser<U> {
64 fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
65 Box::pin(async move {
66 request
67 .extension::<AuthenticatedUser<U>>()
68 .cloned()
69 .ok_or_else(|| Error::unauthenticated("Unauthenticated."))
70 })
71 }
72}
73
74pub struct Authenticate<U: Authenticatable> {
77 auth: Arc<AuthManager<U>>,
78 guard: Option<String>,
81}
82
83impl<U: Authenticatable> Authenticate<U> {
84 pub fn new(auth: Arc<AuthManager<U>>) -> Self {
86 Self { auth, guard: None }
87 }
88
89 pub fn with_guard(auth: Arc<AuthManager<U>>, guard: impl Into<String>) -> Self {
91 Self { auth, guard: Some(guard.into()) }
92 }
93
94 pub fn resolved() -> MiddlewareStack {
107 MiddlewareStack::new().resolved(|auth: Arc<AuthManager<U>>| Self::new(auth))
108 }
109
110 pub fn resolved_with_guard(guard: impl Into<String>) -> MiddlewareStack {
112 let guard = guard.into();
113 MiddlewareStack::new()
114 .resolved(move |auth: Arc<AuthManager<U>>| Self::with_guard(auth, guard.clone()))
115 }
116}
117
118#[async_trait::async_trait]
119impl<U: Authenticatable> Middleware for Authenticate<U> {
120 async fn handle(&self, mut request: Request, next: Next) -> Response {
121 let resolved = match &self.guard {
122 Some(name) => self.auth.user_via(name, &request).await,
123 None => self.auth.user(&request).await,
124 };
125
126 match resolved {
127 Ok(Some(user)) => {
128 let abilities = match &self.guard {
133 Some(name) => self.auth.abilities_via(name, &request).await,
134 None => self.auth.abilities(&request).await,
135 };
136
137 match abilities {
138 Ok(abilities) => {
139 request.extensions_mut().insert(TokenAbilities(abilities));
140 }
141 Err(e) => {
142 tracing::error!(error = %e, "could not read the token's abilities");
143 return e.into_response();
144 }
145 }
146
147 request.extensions_mut().insert(AuthenticatedUser(Arc::new(user)));
148 next.run(request).await
149 }
150 Ok(None) => Error::unauthenticated("Unauthenticated.").into_response(),
151 Err(e) => {
155 tracing::error!(error = %e, "the authentication guard failed");
156 e.into_response()
157 }
158 }
159 }
160
161 fn name(&self) -> &'static str {
162 "Authenticate"
163 }
164}
165
166#[derive(Debug, Clone)]
175pub struct TokenAbilities(pub Abilities);
176
177impl std::ops::Deref for TokenAbilities {
178 type Target = Abilities;
179
180 fn deref(&self) -> &Self::Target {
181 &self.0
182 }
183}
184
185#[async_trait::async_trait]
186impl FromRequest for TokenAbilities {
187 fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
188 Box::pin(async move {
189 request.extension::<TokenAbilities>().cloned().ok_or_else(|| {
190 Error::internal(
191 "no token abilities on the request — is this route behind the `auth` \
192 middleware?",
193 )
194 })
195 })
196 }
197}
198
199pub trait AbilitiesRequestExt {
201 fn token_abilities(&self) -> Option<&Abilities>;
203
204 fn token_can(&self, ability: &str) -> bool;
209}
210
211impl AbilitiesRequestExt for Request {
212 fn token_abilities(&self) -> Option<&Abilities> {
213 self.extension::<TokenAbilities>().map(|abilities| &abilities.0)
214 }
215
216 fn token_can(&self, ability: &str) -> bool {
217 self.token_abilities().is_some_and(|abilities| abilities.can(ability))
218 }
219}
220
221#[derive(Debug, Clone)]
233pub struct RequireAbility {
234 required: Vec<String>,
235 all: bool,
237}
238
239impl RequireAbility {
240 pub fn any(abilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
244 Self { required: abilities.into_iter().map(Into::into).collect(), all: false }
245 }
246
247 pub fn all(abilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
249 Self { required: abilities.into_iter().map(Into::into).collect(), all: true }
250 }
251
252 pub fn required(&self) -> &[String] {
254 &self.required
255 }
256
257 fn satisfied_by(&self, abilities: &Abilities) -> bool {
258 let required = self.required.iter().map(String::as_str);
259
260 if self.all {
261 abilities.can_all(required)
262 } else {
263 abilities.can_any(required)
264 }
265 }
266}
267
268#[async_trait::async_trait]
269impl Middleware for RequireAbility {
270 async fn handle(&self, request: Request, next: Next) -> Response {
271 let Some(abilities) = request.token_abilities() else {
272 tracing::error!(
275 required = ?self.required,
276 "`RequireAbility` ran without `Authenticate` before it, so the request has no \
277 abilities to check — refusing"
278 );
279 return Error::unauthorized("This action is unauthorized.").into_response();
280 };
281
282 if !self.satisfied_by(abilities) {
283 return Error::unauthorized(format!(
284 "This token is not allowed to {}.",
285 self.required.join(" or ")
286 ))
287 .into_response();
288 }
289
290 next.run(request).await
291 }
292
293 fn name(&self) -> &'static str {
294 "RequireAbility"
295 }
296}
297
298pub struct RedirectIfAuthenticated<U: Authenticatable> {
301 auth: Arc<AuthManager<U>>,
302 to: String,
303}
304
305impl<U: Authenticatable> RedirectIfAuthenticated<U> {
306 pub fn new(auth: Arc<AuthManager<U>>, to: impl Into<String>) -> Self {
308 Self { auth, to: to.into() }
309 }
310}
311
312#[async_trait::async_trait]
313impl<U: Authenticatable> Middleware for RedirectIfAuthenticated<U> {
314 async fn handle(&self, request: Request, next: Next) -> Response {
315 match self.auth.user(&request).await {
316 Ok(Some(_)) => rainier_http::Redirect::to(self.to.clone()).into_response(),
317 Ok(None) | Err(_) => next.run(request).await,
320 }
321 }
322
323 fn name(&self) -> &'static str {
324 "RedirectIfAuthenticated"
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::guard::TokenGuard;
332 use crate::user::{Credentials, UserProvider};
333 use rainier_http::StatusCode;
334 use rainier_middleware::Pipeline;
335 use rainier_support::Result;
336
337 #[derive(Debug, Clone, PartialEq)]
338 struct User {
339 id: u64,
340 }
341
342 impl Authenticatable for User {
343 fn auth_identifier(&self) -> String {
344 self.id.to_string()
345 }
346 fn auth_password_hash(&self) -> Option<&str> {
347 None
348 }
349 }
350
351 struct Provider {
352 fail: bool,
353 }
354
355 #[async_trait::async_trait]
356 impl UserProvider<User> for Provider {
357 async fn retrieve_by_id(&self, _: &str) -> Result<Option<User>> {
358 Ok(None)
359 }
360 async fn retrieve_by_credentials(&self, _: &Credentials) -> Result<Option<User>> {
361 Ok(None)
362 }
363 async fn validate_credentials(&self, _: &User, _: &Credentials) -> Result<bool> {
364 Ok(false)
365 }
366 async fn retrieve_by_token(&self, token: &str) -> Result<Option<User>> {
367 if self.fail {
368 return Err(Error::internal("database is down"));
369 }
370 Ok((token == "good").then_some(User { id: 7 }))
371 }
372 }
373
374 fn manager(fail: bool) -> Arc<AuthManager<User>> {
375 Arc::new(
376 AuthManager::new("api")
377 .register(Arc::new(TokenGuard::new("api", Arc::new(Provider { fail })))),
378 )
379 }
380
381 async fn reveal_user(request: Request) -> Response {
382 match request.extension::<AuthenticatedUser<User>>() {
383 Some(user) => Response::text(user.id.to_string()),
384 None => Response::text("anonymous"),
385 }
386 }
387
388 async fn body_of(response: Response) -> String {
389 String::from_utf8(response.into_http().into_body().collect().await.unwrap().to_vec())
390 .unwrap()
391 }
392
393 fn bearer(token: &str) -> Request {
394 Request::builder().header("authorization", &format!("Bearer {token}")).build()
395 }
396
397 #[tokio::test]
398 async fn an_authenticated_request_reaches_the_handler_with_its_user() {
399 let response = Pipeline::new()
400 .through(Authenticate::new(manager(false)))
401 .then(reveal_user)
402 .run(bearer("good"))
403 .await;
404
405 assert_eq!(response.status(), StatusCode::OK);
406 assert_eq!(body_of(response).await, "7");
407 }
408
409 #[tokio::test]
410 async fn an_unauthenticated_request_is_rejected_before_the_handler() {
411 let response = Pipeline::new()
412 .through(Authenticate::new(manager(false)))
413 .then(|_: Request| async { panic!("the handler must not run") })
414 .run(bearer("bad"))
415 .await;
416
417 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
418 }
419
420 #[tokio::test]
421 async fn a_guard_failure_is_a_500_not_a_401() {
422 let response = Pipeline::new()
425 .through(Authenticate::new(manager(true)))
426 .then(reveal_user)
427 .run(bearer("good"))
428 .await;
429
430 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
431 }
432
433 #[tokio::test]
434 async fn a_named_guard_can_be_selected() {
435 let middleware = Authenticate::with_guard(manager(false), "api");
436 let response =
437 Pipeline::new().through(middleware).then(reveal_user).run(bearer("good")).await;
438
439 assert_eq!(body_of(response).await, "7");
440 }
441
442 #[tokio::test]
443 async fn an_unknown_named_guard_fails_loudly() {
444 let middleware = Authenticate::with_guard(manager(false), "nope");
445 let response =
446 Pipeline::new().through(middleware).then(reveal_user).run(bearer("good")).await;
447
448 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
449 }
450
451 #[tokio::test]
452 async fn the_guest_middleware_redirects_an_authenticated_user() {
453 let response = Pipeline::new()
454 .through(RedirectIfAuthenticated::new(manager(false), "/dashboard"))
455 .then(|_: Request| async { Response::text("login form") })
456 .run(bearer("good"))
457 .await;
458
459 assert_eq!(response.status(), StatusCode::FOUND);
460 assert_eq!(response.header("location"), Some("/dashboard"));
461 }
462
463 #[tokio::test]
464 async fn the_guest_middleware_lets_a_guest_through() {
465 let response = Pipeline::new()
466 .through(RedirectIfAuthenticated::new(manager(false), "/dashboard"))
467 .then(|_: Request| async { Response::text("login form") })
468 .run(Request::builder().build())
469 .await;
470
471 assert_eq!(body_of(response).await, "login form");
472 }
473
474 #[tokio::test]
475 async fn the_guest_middleware_fails_open_to_the_login_page() {
476 let response = Pipeline::new()
478 .through(RedirectIfAuthenticated::new(manager(true), "/dashboard"))
479 .then(|_: Request| async { Response::text("login form") })
480 .run(bearer("good"))
481 .await;
482
483 assert_eq!(body_of(response).await, "login form");
484 }
485}