sysmonk/squire/
authenticator.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use actix_web::http::header::HeaderValue;
5use actix_web::{web, HttpRequest};
6use chrono::Utc;
7use fernet::Fernet;
8
9use crate::constant;
10use crate::squire;
11
12/// Represents user credentials extracted from an authorization header.
13///
14/// Contains the username, signature, and timestamp obtained by decoding and parsing the authorization header.
15struct Credentials {
16    username: String,
17    signature: String,
18    timestamp: String,
19}
20
21/// Represents the result of authentication, indicating whether it was successful or not.
22///
23/// If successful, it includes the username and a generated key for the session.
24pub struct AuthToken {
25    pub ok: bool,
26    pub detail: String,
27    pub username: String,
28}
29
30
31/// Extracts credentials from the authorization header in the following steps
32///
33/// # Arguments
34///
35/// * `authorization` - An optional `HeaderValue` containing the authorization header.
36///
37/// # See Also
38/// - Decodes the base64 encoded header
39/// - Splits it into 3 parts with first one being the username followed by the signature and timestamp
40/// - Converts the username from hex into a string.
41///
42/// # Returns
43///
44/// Returns a `Result` containing the extracted `Credentials` or an error message if extraction fails.
45fn extract_credentials(authorization: &HeaderValue) -> Result<Credentials, &'static str> {
46    let header = authorization.to_str().unwrap().to_string();
47    // base64 encoded in JavaScript using inbuilt btoa function
48    let b64_decode_response = squire::secure::base64_decode(&header);
49    match b64_decode_response {
50        Ok(decoded_auth) => {
51            if decoded_auth.is_empty() {
52                log::warn!("Authorization header was received without a value");
53                return Err("No credentials received");
54            }
55            let vector: Vec<&str> = decoded_auth.split(',').collect();
56            Ok(Credentials {
57                // Decode hex username into string to retrieve password from config file
58                username: squire::secure::hex_decode(vector.first().unwrap()),
59                signature: vector.get(1).unwrap().to_string(),
60                timestamp: vector.get(2).unwrap().to_string(),
61            })
62        }
63        Err(err) => {
64            Err(err)
65        }
66    }
67}
68
69/// Verifies user login based on extracted credentials and configuration settings.
70///
71/// # Arguments
72///
73/// * `request` - A reference to the Actix web `HttpRequest` object.
74/// * `config` - Configuration data for the application.
75/// * `session` - Session struct that holds the `session_mapping` to handle sessions.
76///
77/// # Returns
78///
79/// Returns a `Result` containing a `HashMap` with session information if authentication is successful,
80/// otherwise returns an error message.
81pub fn verify_login(
82    request: &HttpRequest,
83    config: &web::Data<Arc<squire::settings::Config>>,
84    session: &web::Data<Arc<constant::Session>>,
85) -> Result<HashMap<&'static str, String>, String> {
86    let err_response;
87    if let Some(authorization) = request.headers().get("authorization") {
88        let extracted_credentials = extract_credentials(authorization);
89        match extracted_credentials {
90            Ok(credentials) => {
91                // Check if the username is present in HashMap as key
92                let message = format!("{}{}{}",
93                                      squire::secure::hex_encode(&credentials.username),
94                                      squire::secure::hex_encode(&config.password),
95                                      credentials.timestamp);
96                // Create a new signature with hex encoded username and password stored in config file as plain text
97                let expected_signature = squire::secure::calculate_hash(message);
98                if expected_signature == credentials.signature {
99                    let key = squire::secure::keygen();
100                    session.mapping.lock().unwrap().insert(credentials.username.to_string(), key.to_string());
101                    let mut mapped = HashMap::new();
102                    mapped.insert("username", credentials.username.to_string());
103                    mapped.insert("key", key.to_string());
104                    mapped.insert("timestamp", credentials.timestamp.to_string());
105                    return Ok(mapped);
106                } else {
107                    log::warn!("{} entered bad credentials", credentials.username);
108                    err_response = "Incorrect username or password";
109                }
110            }
111            Err(err) => {
112                err_response = err;
113            }
114        }
115    } else {
116        log::warn!("Authorization header was missing");
117        err_response = "No credentials received";
118    }
119    Err(err_response.to_string())
120}
121
122/// Verifies a session token extracted from an HTTP request against stored session mappings and configuration.
123///
124/// # Arguments
125///
126/// * `request` - A reference to the Actix web `HttpRequest` object.
127/// * `config` - Configuration data for the application.
128/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
129/// * `session` - Session struct that holds the `session_mapping` to handle sessions.
130///
131/// # Returns
132///
133/// Returns an instance of the `AuthToken` struct indicating the result of the token verification.
134pub fn verify_token(
135    request: &HttpRequest,
136    config: &squire::settings::Config,
137    fernet: &Fernet,
138    session: &constant::Session,
139) -> AuthToken {
140    if session.mapping.lock().unwrap().is_empty() {
141        log::warn!("No stored sessions, no point in validating further");
142        return AuthToken {
143            ok: false,
144            detail: "Server doesn't recognize your session".to_string(),
145            username: "NA".to_string(),
146        };
147    }
148    if let Some(cookie) = request.cookie("session_token") {
149        if let Ok(decrypted) = fernet.decrypt(cookie.value()) {
150            let payload: HashMap<String, String> = serde_json::from_str(&String::from_utf8_lossy(&decrypted)).unwrap();
151            let username = payload.get("username").unwrap().to_string();
152            let cookie_key = payload.get("key").unwrap().to_string();
153            let timestamp = payload.get("timestamp").unwrap().parse::<i64>().unwrap();
154            let stored_key = session.mapping.lock().unwrap().get(&username).unwrap().to_string();
155            let current_time = Utc::now().timestamp();
156            // Max time and expiry for session token is set in the Cookie, but this is a fallback mechanism
157            if stored_key != *cookie_key {
158                return AuthToken {
159                    ok: false,
160                    detail: "Invalid session token".to_string(),
161                    username,
162                };
163            }
164            if current_time - timestamp > config.session_duration {
165                return AuthToken {
166                    ok: false,
167                    detail: "Session Expired".to_string(),
168                    username,
169                };
170            }
171            let time_left = timestamp + config.session_duration - current_time;
172            AuthToken {
173                ok: true,
174                detail: format!("Session valid for {}s", time_left),
175                username,
176            }
177        } else {
178            AuthToken {
179                ok: false,
180                detail: "Invalid session token".to_string(),
181                username: "NA".to_string(),
182            }
183        }
184    } else {
185        AuthToken {
186            ok: false,
187            detail: "Session information not found".to_string(),
188            username: "NA".to_string(),
189        }
190    }
191}