Skip to main content

rustlavel_auth/
lib.rs

1//! rustlavel-auth: sessions, API tokens, password hashing, CSRF, signed URLs.
2//!
3//! Everything an application needs to know who is talking to it and to trust
4//! what they send. Like every rustlavel package it is opt-in: an application
5//! that never adds it never compiles a line of this.
6//!
7//! ```ignore
8//! use rustlavel_auth::prelude::*;
9//!
10//! let sessions = SessionManager::from_config(&config, FileStore::new("storage/sessions"))?;
11//! router.middleware(sessions);
12//! router.middleware(Csrf::new());
13//!
14//! router.group("/dashboard", |r| {
15//!     r.middleware(Authenticate::from_config(&config));
16//!     r.get("", |req: Request| async move {
17//!         format!("hello, {}", req.identity().unwrap().id())
18//!     });
19//! });
20//! ```
21//!
22//! # Why the crypto crates are here
23//!
24//! The framework builds from scratch on principle, and this crate follows that
25//! everywhere except the primitives: argon2, AES-GCM, HMAC and SHA-256 come
26//! from the RustCrypto crates. A hand-written cipher or KDF looks like it works
27//! from the first test and is a vulnerability regardless — this is the one
28//! place where writing less code is the safer engineering decision.
29
30/// Base64 now lives in `rustlavel-core`, beside the other formats the
31/// framework writes for itself, so a crate that needs it — storage, for a
32/// `Content-MD5` header — is not made to depend on the whole of auth. Every
33/// `rustlavel_auth::base64::…` path keeps working through this re-export.
34pub mod base64 {
35    pub use rustlavel_core::base64::*;
36}
37pub mod csrf;
38pub mod encryption;
39pub mod guard;
40pub mod hashing;
41pub mod impersonation;
42pub mod key;
43pub mod middleware;
44pub mod qr;
45pub mod random;
46#[cfg(feature = "redis")]
47pub mod redis_session;
48pub mod session;
49pub mod signed_url;
50pub mod store;
51pub mod tokens;
52pub mod totp;
53
54pub use csrf::Csrf;
55pub use encryption::Encrypter;
56pub use guard::{AuthExt, Authenticatable, Authenticate, Guard, Guest, Identity};
57pub use hashing::{Cost, hash_password, hash_password_with, needs_rehash, verify_password};
58pub use impersonation::{Impersonation, ImpersonationExt, Impersonator};
59pub use key::AppKey;
60pub use middleware::{SessionExt, SessionHandle, SessionManager};
61pub use session::Session;
62pub use signed_url::{SignatureError, UrlSigner};
63pub use store::{FileStore, MemoryStore, SessionStore, SharedStore};
64pub use tokens::{
65    MemoryTokenStore, NewToken, PlainTextToken, RequireApiToken, RequireScope, Scopes,
66    SharedTokenStore, Token, TokenError, TokenExt, TokenStore,
67};
68
69pub use rustlavel_core::{Error, Result};
70
71/// What a `routes/web.rs` or a controller imports to use this package.
72pub mod prelude {
73    pub use crate::{
74        AuthExt, Authenticatable, Authenticate, Cost, Csrf, Encrypter, Guard, Guest, Identity,
75        Session, SessionExt, SessionHandle, SessionManager, SignatureError, UrlSigner,
76        hash_password, verify_password,
77    };
78    pub use crate::{FileStore, MemoryStore, SessionStore};
79    pub use crate::{
80        MemoryTokenStore, NewToken, PlainTextToken, RequireApiToken, RequireScope, Scopes, Token,
81        TokenError, TokenExt, TokenStore,
82    };
83}
84
85/// Compare two byte strings without leaking where they first differ.
86///
87/// The obvious `a == b` returns as soon as it finds a mismatch, so how long it
88/// took is a measurement of how much of the secret the caller guessed
89/// correctly. Repeat that a few thousand times and a token can be recovered one
90/// byte at a time. Every secret comparison in this crate — CSRF tokens, URL
91/// signatures, session-cookie MACs — goes through here.
92///
93/// Unequal lengths are reported as unequal immediately: a length is not a
94/// secret, and pretending otherwise would mean comparing past the end of a
95/// buffer.
96pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
97    use subtle::ConstantTimeEq;
98
99    a.len() == b.len() && bool::from(a.ct_eq(b))
100}
101
102/// The current time in unix seconds.
103///
104/// Clamped at the epoch rather than panicking: a machine whose clock is set
105/// before 1970 should expire every signed URL, not take the process down.
106pub fn unix_now() -> u64 {
107    std::time::SystemTime::now()
108        .duration_since(std::time::UNIX_EPOCH)
109        .map(|elapsed| elapsed.as_secs())
110        .unwrap_or(0)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn constant_time_comparison_agrees_with_ordinary_equality() {
119        assert!(constant_time_eq(b"", b""));
120        assert!(constant_time_eq(b"token", b"token"));
121
122        assert!(!constant_time_eq(b"token", b"tokeN"));
123        assert!(!constant_time_eq(b"token", b"Token"));
124        assert!(!constant_time_eq(b"token", b"token "));
125        assert!(!constant_time_eq(b"token", b""));
126    }
127
128    #[test]
129    fn the_clock_is_past_the_epoch_and_moves_forward() {
130        let now = unix_now();
131
132        // Somewhere after 2020; a value of 0 would mean every signed URL is
133        // already expired, which is a failure worth noticing here.
134        assert!(now > 1_577_836_800, "the system clock reported {now}");
135        assert!(unix_now() >= now);
136    }
137}