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
30pub mod base64;
31pub mod csrf;
32pub mod encryption;
33pub mod guard;
34pub mod hashing;
35pub mod key;
36pub mod middleware;
37pub mod random;
38pub mod session;
39pub mod signed_url;
40pub mod store;
41pub mod tokens;
42
43pub use csrf::Csrf;
44pub use encryption::Encrypter;
45pub use guard::{AuthExt, Authenticatable, Authenticate, Guard, Guest, Identity};
46pub use hashing::{Cost, hash_password, hash_password_with, needs_rehash, verify_password};
47pub use key::AppKey;
48pub use middleware::{SessionExt, SessionHandle, SessionManager};
49pub use session::Session;
50pub use signed_url::{SignatureError, UrlSigner};
51pub use store::{FileStore, MemoryStore, SessionStore, SharedStore};
52pub use tokens::{
53    MemoryTokenStore, NewToken, PlainTextToken, RequireApiToken, RequireScope, Scopes,
54    SharedTokenStore, Token, TokenError, TokenExt, TokenStore,
55};
56
57pub use rustlavel_core::{Error, Result};
58
59/// What a `routes/web.rs` or a controller imports to use this package.
60pub mod prelude {
61    pub use crate::{
62        AuthExt, Authenticatable, Authenticate, Cost, Csrf, Encrypter, Guard, Guest, Identity,
63        Session, SessionExt, SessionHandle, SessionManager, SignatureError, UrlSigner,
64        hash_password, verify_password,
65    };
66    pub use crate::{FileStore, MemoryStore, SessionStore};
67    pub use crate::{
68        MemoryTokenStore, NewToken, PlainTextToken, RequireApiToken, RequireScope, Scopes, Token,
69        TokenError, TokenExt, TokenStore,
70    };
71}
72
73/// Compare two byte strings without leaking where they first differ.
74///
75/// The obvious `a == b` returns as soon as it finds a mismatch, so how long it
76/// took is a measurement of how much of the secret the caller guessed
77/// correctly. Repeat that a few thousand times and a token can be recovered one
78/// byte at a time. Every secret comparison in this crate — CSRF tokens, URL
79/// signatures, session-cookie MACs — goes through here.
80///
81/// Unequal lengths are reported as unequal immediately: a length is not a
82/// secret, and pretending otherwise would mean comparing past the end of a
83/// buffer.
84pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
85    use subtle::ConstantTimeEq;
86
87    a.len() == b.len() && bool::from(a.ct_eq(b))
88}
89
90/// The current time in unix seconds.
91///
92/// Clamped at the epoch rather than panicking: a machine whose clock is set
93/// before 1970 should expire every signed URL, not take the process down.
94pub fn unix_now() -> u64 {
95    std::time::SystemTime::now()
96        .duration_since(std::time::UNIX_EPOCH)
97        .map(|elapsed| elapsed.as_secs())
98        .unwrap_or(0)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn constant_time_comparison_agrees_with_ordinary_equality() {
107        assert!(constant_time_eq(b"", b""));
108        assert!(constant_time_eq(b"token", b"token"));
109
110        assert!(!constant_time_eq(b"token", b"tokeN"));
111        assert!(!constant_time_eq(b"token", b"Token"));
112        assert!(!constant_time_eq(b"token", b"token "));
113        assert!(!constant_time_eq(b"token", b""));
114    }
115
116    #[test]
117    fn the_clock_is_past_the_epoch_and_moves_forward() {
118        let now = unix_now();
119
120        // Somewhere after 2020; a value of 0 would mean every signed URL is
121        // already expired, which is a failure worth noticing here.
122        assert!(now > 1_577_836_800, "the system clock reported {now}");
123        assert!(unix_now() >= now);
124    }
125}