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