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}