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