proxyauth/
auth.rs

1use crate::AppState;
2use crate::config::AuthRequest;
3use crate::crypto::{calcul_cipher, derive_key_from_secret, encrypt};
4use crate::proxy::client_ip;
5use crate::security::generate_token;
6use actix_web::{HttpRequest, HttpResponse, Responder, web};
7use argon2::Argon2;
8use argon2::password_hash::{PasswordHash, PasswordVerifier};
9use chrono::{Duration, Utc, TimeZone};
10use chrono_tz::Tz;
11use rand::rngs::OsRng;
12use rand::seq::SliceRandom;
13use blake3;
14use hex;
15use crate::AppConfig;
16use std::sync::Arc;
17use tracing::{info, warn};
18
19pub fn verify_password(input: &str, stored_hash: &str) -> bool {
20    match PasswordHash::new(stored_hash) {
21        Ok(parsed) => Argon2::default()
22            .verify_password(input.as_bytes(), &parsed)
23            .is_ok(),
24        Err(_) => false,
25    }
26}
27
28pub fn generate_random_string(len: usize) -> String {
29    let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^*()+-=";
30    let mut rng = OsRng;
31
32    let base: Vec<u8> = (0..len)
33    .map(|_| *charset.choose(&mut rng).unwrap())
34    .collect();
35
36    let now = Utc::now().timestamp() as u64;
37    let shift: u8 = (now ^ (now >> 3) ^ (now << 1)).wrapping_rem(97) as u8;
38
39    let random_char: Vec<u8> = base
40    .into_iter()
41    .map(|byte| {
42        let idx = charset.iter().position(|&c| c == byte).unwrap_or(0);
43        let new_idx = (idx as u8 + shift) as usize % charset.len();
44        charset[new_idx]
45    })
46    .collect();
47
48    let mut full_input = random_char.clone();
49    full_input.extend_from_slice(&now.to_le_bytes());
50
51    let hash = blake3::hash(&full_input);
52
53    hex::encode(hash.as_bytes())
54}
55
56fn get_expiry_with_timezone(config: Arc<AppConfig>, optional_timestamp: Option<i64>) -> String {
57    let tz: Tz = config
58        .timezone
59        .parse()
60        .expect("Invalid timezone in config");
61
62    let utc_now = optional_timestamp
63        .map(|ts| Utc.timestamp_opt(ts, 0).single().expect("Invalid timestamp"))
64        .unwrap_or_else(Utc::now);
65
66    let local_time = utc_now.with_timezone(&tz);
67    let expiry = local_time + Duration::seconds(config.token_expiry_seconds);
68
69    expiry.timestamp().to_string()
70}
71
72pub async fn auth(
73    req: HttpRequest,
74    auth: web::Json<AuthRequest>,
75    data: web::Data<AppState>,
76) -> impl Responder {
77    let ip = client_ip(&req).expect("?").to_string();
78
79    if let Some(index_user) = data
80        .config
81        .users
82        .iter()
83        .enumerate()
84        .find(|(_, user)| {
85            user.username == auth.username && verify_password(&auth.password, &user.password)
86        })
87        .map(|(i, _)| i)
88    {
89        let user = &data.config.users[index_user];
90
91
92        let expiry = get_expiry_with_timezone(data.config.clone(), None);
93
94        let id_token = generate_random_string(48);
95
96        let token = generate_token(
97            &auth.username,
98            &data.config,
99            &expiry,
100            &id_token,
101        );
102
103        let key = derive_key_from_secret(&data.config.secret);
104
105        let cipher_token = format!(
106            "{}|{}|{}|{}",
107            calcul_cipher(token.clone()),
108            expiry,
109            index_user,
110            id_token
111        );
112
113        let token_encrypt = encrypt(&cipher_token, &key);
114
115        let expiry_ts: i64 = expiry
116            .parse()
117            .expect("Invalid timestamp string");
118
119        let expiry_utc = Utc
120            .timestamp_opt(expiry_ts, 0)
121            .single()
122            .expect("Invalid timestamp value");
123
124        let tz: Tz = data.config
125            .timezone
126            .parse()
127            .expect("Invalid timezone");
128
129        let expiry_dt_local = expiry_utc.with_timezone(&tz);
130        let expires_at_str = expiry_dt_local.format("%Y-%m-%d %H:%M:%S").to_string();
131
132        info!(
133            "[{}] new token generated for user {} expirated at {}",
134            ip, user.username, expires_at_str
135        );
136        HttpResponse::Ok().json(serde_json::json!({
137            "token": token_encrypt,
138            "expires_at": expires_at_str,
139        }))
140    } else {
141        warn!("Invalid credential for enter user {}.", auth.username);
142        return HttpResponse::Unauthorized().body("Invalid credentials");
143    }
144}