summaryrefslogtreecommitdiff
path: root/server/src/auth.rs
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2024-12-29 20:37:26 +0100
committerJoel Klinghed <the_jk@spawned.biz>2024-12-29 20:37:26 +0100
commit7bc8e8b7262a3f3abe3222b3b434838e85cdb2bb (patch)
tree4f5abb6180a069126cd787310942d5d7f8436768 /server/src/auth.rs
parent0aa2545b703f5240a8208a07da8ab20b8bc6d1aa (diff)
Rework auth to include session
The actual authentication is still fake.
Diffstat (limited to 'server/src/auth.rs')
-rw-r--r--server/src/auth.rs207
1 files changed, 207 insertions, 0 deletions
diff --git a/server/src/auth.rs b/server/src/auth.rs
new file mode 100644
index 0000000..31e18a0
--- /dev/null
+++ b/server/src/auth.rs
@@ -0,0 +1,207 @@
+use core::net::IpAddr;
+use rocket::fairing::AdHoc;
+use rocket::form::Form;
+use rocket::http::{Cookie, CookieJar, Status};
+use rocket::outcome::{try_outcome, IntoOutcome};
+use rocket::request::{FromRequest, Outcome, Request};
+use rocket::response::status::Unauthorized;
+use rocket::serde::json::{self, Json};
+use rocket::serde::{Deserialize, Serialize};
+use rocket::State;
+use std::collections::BTreeMap;
+use std::sync::Mutex;
+use std::time::Instant;
+use time::Duration;
+
+use crate::api_model;
+
+#[derive(FromForm)]
+struct Login<'r> {
+ username: &'r str,
+ password: &'r str,
+}
+
+#[derive(Debug, Deserialize)]
+#[allow(dead_code)]
+struct AuthConfig {
+ session_max_age_days: u32,
+}
+
+struct SessionsData {
+ active_ids: BTreeMap<u32, Instant>,
+ next_id: u32,
+}
+
+struct Sessions {
+ data: Mutex<SessionsData>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Session {
+ pub user_id: u64,
+ session_id: u32,
+ remote: String,
+}
+
+#[derive(Debug)]
+pub enum SessionError {
+ Invalid,
+}
+
+const SESSION_COOKIE: &str = "s";
+
+fn validate(sessions: &State<Sessions>, session: &Session, request: &Request<'_>) -> bool {
+ match request.client_ip() {
+ Some(addr) => {
+ if session.remote == addr.to_string() {
+ {
+ let sessions_data = sessions.data.lock().unwrap();
+ match sessions_data.active_ids.get(&session.session_id) {
+ // We could remove the expired session here, but it will be cleaned
+ // next time anyone logs in anyway.
+ Some(&expire) => expire > Instant::now(),
+ None => false,
+ }
+ }
+ } else {
+ false
+ }
+ }
+ None => false,
+ }
+}
+
+#[rocket::async_trait]
+impl<'r> FromRequest<'r> for Session {
+ type Error = SessionError;
+
+ async fn from_request(request: &'r Request<'_>) -> Outcome<Session, SessionError> {
+ let sessions = try_outcome!(request
+ .guard::<&State<Sessions>>()
+ .await
+ .map_error(|_| (Status::Unauthorized, SessionError::Invalid)));
+
+ request
+ .cookies()
+ .get_private(SESSION_COOKIE)
+ .and_then(|cookie| -> Option<Session> { json::from_str(cookie.value()).ok() })
+ .and_then(|session| {
+ if validate(&sessions, &session, request) {
+ Some(session)
+ } else {
+ None
+ }
+ })
+ .or_error((Status::Unauthorized, SessionError::Invalid))
+ }
+}
+
+fn new_session(
+ sessions: &State<Sessions>,
+ user_id: u64,
+ remote: String,
+ max_age: Duration,
+) -> Session {
+ let session_id;
+ {
+ let mut sessions_data = sessions.data.lock().unwrap();
+ session_id = sessions_data.next_id;
+ sessions_data.next_id += 1;
+
+ let now = Instant::now();
+ // Remove expired sessions first
+ sessions_data
+ .active_ids
+ .retain(|_, &mut expire| expire > now);
+
+ sessions_data.active_ids.insert(session_id, now + max_age);
+ }
+ Session {
+ user_id: user_id,
+ session_id: session_id,
+ remote: remote,
+ }
+}
+
+#[post("/login", data = "<login>")]
+fn login(
+ auth_config: &State<AuthConfig>,
+ sessions: &State<Sessions>,
+ ipaddr: IpAddr,
+ cookies: &CookieJar<'_>,
+ login: Form<Login<'_>>,
+) -> Result<Json<api_model::StatusResponse>, Unauthorized<&'static str>> {
+ if login.username == "user" && login.password == "password" {
+ let max_age = Duration::days(i64::from(auth_config.session_max_age_days));
+ let session = new_session(&sessions, 1u64, ipaddr.to_string(), max_age);
+
+ let cookie = Cookie::build((SESSION_COOKIE, json::to_string(&session).unwrap()))
+ .path("/api")
+ .max_age(max_age)
+ .http_only(true)
+ .build();
+
+ cookies.add_private(cookie);
+ Ok(Json(api_model::StatusResponse {
+ ok: true,
+ error: None,
+ }))
+ } else {
+ Err(Unauthorized("Unknown username or password"))
+ }
+}
+
+#[get("/logout")]
+fn logout(
+ session: Session,
+ sessions: &State<Sessions>,
+ cookies: &CookieJar<'_>,
+) -> Json<api_model::StatusResponse> {
+ {
+ let mut sessions_data = sessions.data.lock().unwrap();
+ sessions_data.active_ids.remove(&session.session_id);
+ }
+
+ let cookie = Cookie::build((SESSION_COOKIE, ""))
+ .path("/api")
+ .http_only(true)
+ .build();
+
+ cookies.remove_private(cookie);
+
+ Json(api_model::StatusResponse {
+ ok: true,
+ error: None,
+ })
+}
+
+#[get("/status")]
+fn status(_session: Session) -> Json<api_model::StatusResponse> {
+ Json(api_model::StatusResponse {
+ ok: true,
+ error: None,
+ })
+}
+
+#[catch(401)]
+fn unauthorized() -> Json<api_model::StatusResponse> {
+ Json(api_model::StatusResponse {
+ ok: false,
+ error: Some("Unauthorized".to_string()),
+ })
+}
+
+pub fn stage(basepath: String) -> AdHoc {
+ AdHoc::on_ignite("Auth Stage", |rocket| async {
+ rocket
+ .manage(Sessions {
+ data: Mutex::new(SessionsData {
+ active_ids: BTreeMap::new(),
+ next_id: 1,
+ }),
+ })
+ .attach(AdHoc::config::<AuthConfig>())
+ .mount(basepath.clone(), routes![login, logout, status])
+ .register(basepath, catchers![unauthorized])
+ })
+}