feat: Implement magic link authentication
This commit introduces a new magic link authentication flow. The desktop client can now request a temporary, one-time-use token from the server. This token is then used to open a URL in the system browser, which redirects to the server. The server consumes the token, installs a regular web session, and redirects the user
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||
use rand::Rng;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct WebSession {
|
||||
@@ -15,3 +18,33 @@ pub type SessionStore = Arc<RwLock<HashMap<String, WebSession>>>;
|
||||
pub fn new_store() -> SessionStore {
|
||||
Arc::new(RwLock::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Cookie name shared by login and magic-link flows.
|
||||
pub const SESSION_COOKIE: &str = "session";
|
||||
|
||||
/// 43 alphanumeric chars ≈ 256 bits of entropy — enough for a session
|
||||
/// or short-lived magic-link token.
|
||||
pub const TOKEN_LEN: usize = 43;
|
||||
|
||||
/// CSPRNG-backed token. Used for both long-lived session cookies and
|
||||
/// short-lived magic-link tokens; both need the same entropy.
|
||||
pub fn generate_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(TOKEN_LEN)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Builds a `HttpOnly + SameSite=Strict + Path=/` cookie for the session
|
||||
/// token. `secure` is wired from `Config::cookie_secure` so local HTTP dev
|
||||
/// can opt out — browsers refuse `Secure` cookies on non-HTTPS origins.
|
||||
pub fn build_session_cookie(value: String, max_age: Duration, secure: bool) -> Cookie<'static> {
|
||||
Cookie::build((SESSION_COOKIE, value))
|
||||
.http_only(true)
|
||||
.secure(secure)
|
||||
.same_site(SameSite::Strict)
|
||||
.path("/")
|
||||
.max_age(time::Duration::seconds(max_age.as_secs() as i64))
|
||||
.build()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user