1pub(crate) mod account;
36pub mod events;
37mod external;
38mod inbox;
39pub(crate) mod module;
40pub mod notifications;
41mod passwords;
42pub mod permissions;
43pub mod second_factor;
44mod throttle;
45mod tokens;
46mod user;
47mod verification;
48
49use std::collections::HashMap;
50use std::convert::Infallible;
51use std::ops::Deref;
52use std::sync::Arc;
53
54use axum::extract::{FromRequestParts, OptionalFromRequestParts, Request};
55use axum::http::header::{ACCEPT, AUTHORIZATION};
56use axum::http::request::Parts;
57use axum::http::{Method, StatusCode};
58use axum::middleware::Next;
59use axum::response::{IntoResponse, Redirect, Response};
60
61pub(crate) use account::require_password_confirmed;
62pub use external::{confirm_identity, register_verified, registration_open, sign_in};
63pub use module::{Auth, Registration};
64pub use notifications::{
65 Channel, DatabaseMessage, DatabaseNotification, Notification, Recipient,
66 prune_read_notifications,
67};
68pub(crate) use permissions::Grants;
69pub use permissions::Permissions;
70pub use second_factor::{PendingLogin, complete_login, pending_login};
71pub(crate) use throttle::LoginThrottle;
72pub use tokens::{AccessToken, NewToken, prune_expired_tokens};
73pub use user::{User, hash_password, needs_rehash, verify_password};
74pub use verification::send_verification;
75
76use crate::crypto::constant_time_eq;
77use crate::db::Db;
78use crate::{AppState, Error, Htmx, HxRedirect, Result, Session};
79
80pub(crate) const AUTH_ID: &str = "_auth_user_id";
81const AUTH_HASH: &str = "_auth_password_hash";
82const AUTH_AT: &str = "_auth_at";
84const AUTH_SID: &str = "_auth_session_id";
86const INTENDED: &str = "_intended";
87
88pub trait Policy {
105 fn allows(&self, user: &User, ability: &str) -> bool;
107}
108
109#[derive(Debug, Clone, serde::Serialize)]
126#[non_exhaustive]
127pub struct Can<T> {
128 #[serde(flatten)]
130 pub item: T,
131 #[serde(rename = "_can")]
133 pub abilities: std::collections::BTreeMap<String, bool>,
134}
135
136pub trait Viewer: viewer::Sealed {
140 fn as_user(&self) -> &User;
142
143 fn before(&self, _ability: &str) -> Option<bool> {
145 None
146 }
147}
148
149mod viewer {
150 pub trait Sealed {}
151 impl Sealed for super::User {}
152 impl Sealed for super::AuthUser {}
153}
154
155impl Viewer for User {
156 fn as_user(&self) -> &User {
157 self
158 }
159}
160
161impl Viewer for AuthUser {
162 fn as_user(&self) -> &User {
163 &self.user
164 }
165
166 fn before(&self, ability: &str) -> Option<bool> {
167 AuthUser::before(self, ability)
168 }
169}
170
171impl<T: Policy> Can<T> {
172 pub fn new<V: Viewer + ?Sized>(item: T, user: Option<&V>, abilities: &[&str]) -> Self {
174 let abilities = abilities
175 .iter()
176 .map(|ability| {
177 let allowed = user.is_some_and(|user| {
178 user.before(ability)
179 .unwrap_or_else(|| item.allows(user.as_user(), ability))
180 });
181 ((*ability).to_owned(), allowed)
182 })
183 .collect();
184 Self { item, abilities }
185 }
186}
187
188pub(crate) type Gate = Arc<dyn Fn(&User) -> bool + Send + Sync>;
189pub(crate) type GateBefore = Arc<dyn Fn(&User, &str) -> Option<bool> + Send + Sync>;
190pub(crate) type Gates = Arc<Access>;
191
192#[derive(Default)]
195pub(crate) struct Access {
196 pub gates: HashMap<String, Gate>,
197 pub before: Option<GateBefore>,
198 pub permissions: bool,
199}
200
201impl Access {
202 pub(crate) fn check(&self, user: &User, grants: &Grants, name: &str) -> bool {
205 if let Some(allowed) = self.before.as_ref().and_then(|before| before(user, name)) {
206 return allowed;
207 }
208 match self.gates.get(name) {
209 Some(check) => check(user),
210 None => grants.has_permission(name),
211 }
212 }
213}
214
215#[derive(Clone)]
217pub(crate) struct CurrentGrants {
218 user_id: i64,
219 grants: Arc<Grants>,
220}
221
222impl User {
223 pub fn has_role(&self, role: &str) -> bool {
232 current_grants(self.id).is_some_and(|g| g.has_role(role))
233 }
234
235 pub fn has_permission(&self, permission: &str) -> bool {
238 current_grants(self.id).is_some_and(|g| g.has_permission(permission))
239 }
240}
241
242pub(crate) fn current_user_id() -> Option<i64> {
244 crate::context::get::<CurrentGrants>().map(|current| current.user_id)
245}
246
247fn current_grants(user_id: i64) -> Option<Arc<Grants>> {
248 crate::context::get::<CurrentGrants>()
249 .filter(|current| current.user_id == user_id)
250 .map(|current| current.grants)
251}
252
253pub(crate) type AsyncGate = Arc<
254 dyn Fn(
255 User,
256 AppState,
257 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send>>
258 + Send
259 + Sync,
260>;
261
262#[derive(Clone)]
265pub(crate) struct CurrentUser {
266 pub user: Option<Arc<User>>,
267 pub gates: Gates,
268 pub token_id: Option<i64>,
271 pub abilities: Option<Arc<Vec<String>>>,
274 pub grants: Arc<Grants>,
275}
276
277#[derive(Clone)]
281pub struct AuthUser {
282 user: Arc<User>,
283 gates: Gates,
284 state: Option<AppState>,
285 token_id: Option<i64>,
286 abilities: Option<Arc<Vec<String>>>,
287 grants: Arc<Grants>,
288 role_names: std::sync::OnceLock<Vec<String>>,
290}
291
292impl Deref for AuthUser {
293 type Target = User;
294
295 fn deref(&self) -> &User {
296 &self.user
297 }
298}
299
300impl AuthUser {
301 pub fn token_id(&self) -> Option<i64> {
305 self.token_id
306 }
307
308 pub fn token_can(&self, ability: &str) -> bool {
312 self.abilities
313 .as_ref()
314 .is_none_or(|list| list.iter().any(|a| a == ability || a == "*"))
315 }
316
317 pub fn has_role(&self, role: &str) -> bool {
321 self.grants.has_role(role)
322 }
323
324 pub fn has_permission(&self, permission: &str) -> bool {
328 self.grants.has_permission(permission)
329 }
330
331 pub fn has_role_in(&self, role: &str, scope: &permissions::Scope) -> bool {
334 self.grants.has_role_in(role, Some(scope))
335 }
336
337 pub fn has_permission_in(&self, permission: &str, scope: &permissions::Scope) -> bool {
341 self.grants.has_permission_in(permission, Some(scope))
342 }
343
344 pub fn scopes_with<M: crate::db::Model>(
348 &self,
349 permission: &str,
350 ) -> permissions::Scopes<M::Key> {
351 self.grants.scopes_with::<M>(permission)
352 }
353
354 pub fn role_names(&self) -> &[String] {
358 self.role_names.get_or_init(|| self.grants.roles())
359 }
360
361 pub fn can(&self, ability: &str, target: &impl Policy) -> bool {
364 self.before(ability)
365 .unwrap_or_else(|| target.allows(&self.user, ability))
366 }
367
368 fn before(&self, ability: &str) -> Option<bool> {
369 self.gates
370 .before
371 .as_ref()
372 .and_then(|before| before(&self.user, ability))
373 }
374
375 pub fn authorize(&self, ability: &str, target: &impl Policy) -> Result {
377 if self.can(ability, target) {
378 Ok(())
379 } else {
380 Err(Error::Forbidden)
381 }
382 }
383
384 pub fn allows(&self, gate: &str) -> bool {
388 self.gates.check(&self.user, &self.grants, gate)
389 }
390
391 pub fn gate(&self, gate: &str) -> Result {
393 if self.allows(gate) {
394 Ok(())
395 } else {
396 Err(Error::Forbidden)
397 }
398 }
399
400 pub async fn allows_async(&self, gate: &str) -> Result<bool> {
404 if let Some(allowed) = self.before(gate) {
405 return Ok(allowed);
406 }
407 if self.gates.gates.contains_key(gate) || self.grants.has_permission(gate) {
408 return Ok(self.allows(gate));
409 }
410 let Some(state) = &self.state else {
411 return Ok(false);
412 };
413 match state.async_gates.get(gate) {
414 Some(check) => check(self.user.as_ref().clone(), state.clone()).await,
415 None => Ok(false),
416 }
417 }
418
419 pub async fn gate_async(&self, gate: &str) -> Result {
421 if self.allows_async(gate).await? {
422 Ok(())
423 } else {
424 Err(Error::Forbidden)
425 }
426 }
427
428 pub fn user(&self) -> &User {
430 &self.user
431 }
432}
433
434impl<S: Send + Sync> FromRequestParts<S> for AuthUser {
435 type Rejection = Response;
436
437 async fn from_request_parts(parts: &mut Parts, _: &S) -> std::result::Result<Self, Response> {
438 match current(&parts.extensions) {
439 Some(user) => Ok(user),
440 None => Err(unauthenticated(parts)),
441 }
442 }
443}
444
445impl<S: Send + Sync> OptionalFromRequestParts<S> for AuthUser {
446 type Rejection = Infallible;
447
448 async fn from_request_parts(
449 parts: &mut Parts,
450 _: &S,
451 ) -> std::result::Result<Option<Self>, Infallible> {
452 Ok(current(&parts.extensions))
453 }
454}
455
456fn current(extensions: &axum::http::Extensions) -> Option<AuthUser> {
457 let current = extensions.get::<CurrentUser>()?;
458 Some(AuthUser {
459 user: current.user.clone()?,
460 gates: current.gates.clone(),
461 state: extensions.get::<AppState>().cloned(),
462 token_id: current.token_id,
463 abilities: current.abilities.clone(),
464 grants: current.grants.clone(),
465 role_names: std::sync::OnceLock::new(),
466 })
467}
468
469pub fn login(session: &Session, user: &User, remember: Option<std::time::Duration>) -> Result {
473 session.regenerate_token();
474 session.put(AUTH_ID, user.id)?;
475 session.put(AUTH_HASH, fingerprint(&user.password))?;
476 session.put(AUTH_AT, unix_millis())?;
477 session.put(AUTH_SID, crate::crypto::random_token())?;
478 if let Some(lifetime) = remember {
479 session.set_lifetime(lifetime);
480 }
481 Ok(())
482}
483
484pub async fn logout(db: &Db, session: &Session) -> Result {
489 match (session.get::<i64>(AUTH_ID), session.get::<String>(AUTH_SID)) {
490 (Some(_), Some(sid)) => revoke_session(db, session, &sid).await?,
491 (Some(id), None) => {
493 user::revoke_sessions(db, id).await?;
494 }
495 _ => {}
496 }
497 session.flush();
498 Ok(())
499}
500
501pub async fn logout_other_devices(db: &Db, session: &Session, user: &User) -> Result {
504 let cut_off = user::revoke_sessions(db, user.id).await?;
505 let lifetime = session
508 .lifetime()
509 .map(|minutes| std::time::Duration::from_secs(minutes * 60));
510 login(session, user, lifetime)?;
511 session.put(AUTH_AT, cut_off + 1)?;
512 Ok(())
513}
514
515pub async fn change_password(
518 db: &Db,
519 session: &Session,
520 user: &mut User,
521 password: &str,
522) -> Result {
523 user.set_password(db, password).await?;
524 logout_other_devices(db, session, user).await
525}
526
527async fn revoke_session(db: &Db, session: &Session, sid: &str) -> Result {
529 let minutes = session.lifetime().unwrap_or(60 * 24 * 30);
530 let expires = crate::db::now() + chrono::Duration::minutes(minutes as i64);
531 crate::db::sql("DELETE FROM revoked_sessions WHERE expires_at < ?")
532 .bind(crate::db::now())
533 .execute(db)
534 .await?;
535 crate::db::sql(
536 "INSERT INTO revoked_sessions (id, expires_at) SELECT ?, ? \
537 WHERE NOT EXISTS (SELECT 1 FROM revoked_sessions WHERE id = ?)",
538 )
539 .bind(sid)
540 .bind(expires)
541 .bind(sid)
542 .execute(db)
543 .await?;
544 Ok(())
545}
546
547pub(crate) fn unix_millis() -> i64 {
548 crate::clock::unix_millis()
549}
550
551fn fingerprint(password_hash: &str) -> String {
554 use sha2::{Digest, Sha256};
555 let digest = Sha256::digest(password_hash.as_bytes());
556 digest.iter().take(16).map(|b| format!("{b:02x}")).collect()
557}
558
559pub(crate) async fn middleware(
561 axum::extract::State(state): axum::extract::State<AppState>,
562 mut req: Request,
563 next: Next,
564) -> Response {
565 let bearer = req
566 .headers()
567 .get(AUTHORIZATION)
568 .and_then(|v| v.to_str().ok())
569 .and_then(|v| v.strip_prefix("Bearer "))
570 .map(str::to_owned);
571 let session = req.extensions().get::<Session>().cloned();
572 let (user, token) = match (&bearer, &session) {
573 (Some(bearer), _) => match tokens::authenticate(&state.db, bearer.trim()).await {
576 Ok(Some((user, token))) => (Some(user), Some(token)),
577 Ok(None) => (None, None),
578 Err(err) => {
579 tracing::error!(error = ?err, "could not check the API token");
580 (None, None)
581 }
582 },
583 (None, Some(session)) => (resolve(&state, session).await, None),
584 (None, None) => (None, None),
585 };
586 let grants = match &user {
587 Some(user) if state.gates.permissions => {
588 match permissions::grants(&state.db, user.id).await {
589 Ok(grants) => grants,
590 Err(err) => {
591 tracing::error!(error = ?err, "could not load the user's roles");
592 Grants::default()
593 }
594 }
595 }
596 _ => Grants::default(),
597 };
598 let grants = Arc::new(grants);
599 if let Some(user) = &user {
600 crate::context::set(CurrentGrants {
603 user_id: user.id,
604 grants: grants.clone(),
605 });
606 }
607 req.extensions_mut().insert(CurrentUser {
608 user: user.map(Arc::new),
609 gates: state.gates.clone(),
610 token_id: token.as_ref().map(|t| t.0),
611 abilities: token.and_then(|t| t.1).map(Arc::new),
612 grants,
613 });
614 req.extensions_mut().insert(state);
615 next.run(req).await
616}
617
618async fn resolve(state: &AppState, session: &Session) -> Option<User> {
619 let id: i64 = session.get(AUTH_ID)?;
620 let hash: String = session.get(AUTH_HASH).unwrap_or_default();
621 let logged_in_at: i64 = session.get(AUTH_AT).unwrap_or(0);
622 let sid: String = session.get(AUTH_SID).unwrap_or_default();
623 match User::find_with_revocation(&state.db, id, &sid).await {
624 Ok(Some((user, revoked_at, session_revoked)))
625 if constant_time_eq(&fingerprint(&user.password), &hash)
626 && (revoked_at == 0 || logged_in_at > revoked_at)
627 && !session_revoked =>
628 {
629 Some(user)
630 }
631 Ok(_) => {
632 session.remove(AUTH_ID);
635 session.remove(AUTH_HASH);
636 session.remove(AUTH_AT);
637 session.remove(AUTH_SID);
638 None
639 }
640 Err(err) => {
641 tracing::error!(error = ?err, "could not load the logged-in user");
642 None
643 }
644 }
645}
646
647fn wants_json(headers: &axum::http::HeaderMap) -> bool {
648 headers
649 .get(ACCEPT)
650 .and_then(|v| v.to_str().ok())
651 .is_some_and(|v| v.contains("application/json"))
652}
653
654fn path_or(state: Option<&AppState>, route: &str, fallback: &str) -> String {
655 state
656 .and_then(|s| s.url(route, &[]).ok())
657 .unwrap_or_else(|| fallback.to_owned())
658}
659
660fn unauthenticated(parts: &Parts) -> Response {
662 if wants_json(&parts.headers) || parts.headers.contains_key(AUTHORIZATION) {
663 let body = serde_json::json!({ "message": "Unauthenticated." });
664 return (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response();
665 }
666 let login = path_or(parts.extensions.get::<AppState>(), "login", "/login");
667 if let (Some(session), &Method::GET) = (parts.extensions.get::<Session>(), &parts.method) {
668 let intended = parts
669 .uri
670 .path_and_query()
671 .map(|p| p.as_str().to_owned())
672 .unwrap_or_else(|| "/".into());
673 let _ = session.put(INTENDED, intended);
674 }
675 if Htmx::from_headers(&parts.headers).request {
676 return HxRedirect(login).into_response();
677 }
678 Redirect::to(&login).into_response()
679}
680
681pub(crate) async fn require_auth(req: Request, next: Next) -> Response {
683 if current(req.extensions()).is_some() {
684 return next.run(req).await;
685 }
686 let (parts, _) = req.into_parts();
687 unauthenticated(&parts)
688}
689
690#[derive(Clone)]
693pub(crate) enum Requirement {
694 Gate(String),
695 Role(String),
696 Permission(String),
697 Ability(String),
698}
699
700pub(crate) async fn require(requirement: Arc<Requirement>, req: Request, next: Next) -> Response {
703 let Some(user) = current(req.extensions()) else {
704 let (parts, _) = req.into_parts();
705 return unauthenticated(&parts);
706 };
707 let allowed = match requirement.as_ref() {
708 Requirement::Gate(gate) => match user.allows_async(gate).await {
709 Ok(allowed) => allowed,
710 Err(err) => return err.into_response(),
711 },
712 Requirement::Role(role) => user.has_role(role),
713 Requirement::Permission(permission) => user.allows(permission),
714 Requirement::Ability(ability) => user.token_can(ability),
715 };
716 if allowed {
717 next.run(req).await
718 } else {
719 Error::Forbidden.into_response()
720 }
721}
722
723pub(crate) async fn require_verified(req: Request, next: Next) -> Response {
725 let Some(user) = current(req.extensions()) else {
726 let (parts, _) = req.into_parts();
727 return unauthenticated(&parts);
728 };
729 if user.email_verified_at.is_some() {
730 return next.run(req).await;
731 }
732 if wants_json(req.headers()) || user_via_token(req.extensions()) {
733 let body = serde_json::json!({ "message": "Your email address is not verified." });
734 return (StatusCode::FORBIDDEN, axum::Json(body)).into_response();
735 }
736 let notice = path_or(
737 req.extensions().get::<AppState>(),
738 "verification.notice",
739 "/verify-email",
740 );
741 if Htmx::from_headers(req.headers()).request {
742 return HxRedirect(notice).into_response();
743 }
744 Redirect::to(¬ice).into_response()
745}
746
747pub(crate) fn user_via_token(extensions: &axum::http::Extensions) -> bool {
748 extensions
749 .get::<CurrentUser>()
750 .is_some_and(|c| c.token_id.is_some())
751}
752
753pub(crate) async fn guest_only(req: Request, next: Next) -> Response {
755 if current(req.extensions()).is_none() {
756 return next.run(req).await;
757 }
758 Redirect::to(&path_or(req.extensions().get::<AppState>(), "home", "/")).into_response()
759}
760
761pub(crate) fn intended(session: &Session, fallback: String) -> String {
763 session
764 .pull::<String>(INTENDED)
765 .filter(|path| crate::htmx::is_local_path(path))
766 .unwrap_or(fallback)
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772
773 #[test]
774 fn fingerprints_are_short_and_stable() {
775 let a = fingerprint("$argon2id$v=19$abc");
776 assert_eq!(a.len(), 32);
777 assert_eq!(a, fingerprint("$argon2id$v=19$abc"));
778 assert_ne!(a, fingerprint("$argon2id$v=19$abd"));
779 }
780}