lab_presence

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

auth.rs (3144B)


      1 use actix_web::{
      2     FromRequest, HttpRequest, dev::Payload, error::ResponseError, http::header::AUTHORIZATION,
      3 };
      4 use rand::RngExt;
      5 use rand::distr::Alphanumeric;
      6 use serde_json::Value;
      7 use shared::ErrorResponse;
      8 use std::env;
      9 use std::future::{Ready, ready};
     10 
     11 pub struct AuthenticatedDevice {
     12     #[allow(dead_code)]
     13     pub device_id: String,
     14 }
     15 
     16 #[derive(Debug)]
     17 pub enum AuthError {
     18     MissingHeader,
     19     InvalidFormat,
     20     Unauthorized,
     21 }
     22 
     23 impl std::fmt::Display for AuthError {
     24     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     25         match self {
     26             AuthError::MissingHeader => write!(f, "Missing Authorization Header"),
     27             AuthError::InvalidFormat => write!(f, "Invalid Bearer Format"),
     28             AuthError::Unauthorized => write!(f, "Authorization failed"),
     29         }
     30     }
     31 }
     32 
     33 impl std::error::Error for AuthError {}
     34 
     35 impl ResponseError for AuthError {
     36     fn error_response(&self) -> actix_web::HttpResponse {
     37         actix_web::HttpResponse::Forbidden().json(ErrorResponse {
     38             errcode: "M_FORBIDDEN".to_string(),
     39             error: self.to_string(),
     40         })
     41     }
     42 }
     43 
     44 impl FromRequest for AuthenticatedDevice {
     45     type Error = AuthError;
     46     type Future = Ready<Result<Self, Self::Error>>;
     47 
     48     fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
     49         let auth_header = match req.headers().get(AUTHORIZATION) {
     50             Some(h) => h,
     51             None => return ready(Err(AuthError::MissingHeader)),
     52         };
     53 
     54         let auth_str = match auth_header.to_str() {
     55             Ok(s) => s,
     56             Err(_) => return ready(Err(AuthError::InvalidFormat)),
     57         };
     58 
     59         if !auth_str.starts_with("Bearer ") {
     60             return ready(Err(AuthError::InvalidFormat));
     61         }
     62 
     63         let token = auth_str.trim_start_matches("Bearer ");
     64 
     65         let state = match req.app_data::<actix_web::web::Data<crate::AppState>>() {
     66             Some(s) => s,
     67             None => return ready(Err(AuthError::Unauthorized)),
     68         };
     69 
     70         let tokens = state.tokens.lock().unwrap();
     71 
     72         if let Some(device_id) = tokens.get(token) {
     73             ready(Ok(AuthenticatedDevice {
     74                 device_id: device_id.clone(),
     75             }))
     76         } else {
     77             ready(Err(AuthError::Unauthorized))
     78         }
     79     }
     80 }
     81 
     82 pub fn validate_device(device_id: &str, secret: &str) -> bool {
     83     let _ = dotenv::dotenv();
     84     let devices_raw = env::var("ALLOWED_DEVICES").unwrap_or_else(|_| "{}".to_string());
     85 
     86     if let Ok(Value::Object(map)) = serde_json::from_str(&devices_raw) {
     87         if let Some(Value::String(stored_secret)) = map.get(device_id) {
     88             return stored_secret == secret;
     89         }
     90     }
     91     false
     92 }
     93 
     94 pub fn generate_secure_token() -> String {
     95     use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
     96 
     97     const LENGTH: usize = 32;
     98 
     99     let token: String = rand::rng()
    100         .sample_iter(&Alphanumeric)
    101         .take(LENGTH)
    102         .map(char::from)
    103         .collect();
    104     URL_SAFE_NO_PAD.encode(token)
    105 }