Microservice to bring 2FA to self hosted PDSes

Base 32 email tokens now

Changed files
+31 -26
src
+23 -10
src/helpers.rs
··· 10 10 use chrono::Utc; 11 11 use lettre::message::{MultiPart, SinglePart, header}; 12 12 use lettre::{AsyncTransport, Message}; 13 + use rand::Rng; 13 14 use rand::distr::{Alphabetic, Alphanumeric, SampleString}; 14 15 use serde::de::DeserializeOwned; 15 16 use serde_json::{Map, Value}; 17 + use sha2::{Digest, Sha256}; 16 18 use sqlx::SqlitePool; 17 19 use tracing::{error, log}; 20 + 21 + ///Used to generate the email 2fa code 22 + const UPPERCASE_BASE32_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 18 23 19 24 /// The result of a proxied call that attempts to parse JSON. 20 25 pub enum ProxiedResult<T> { ··· 97 102 .map_err(|_| StatusCode::BAD_REQUEST) 98 103 } 99 104 105 + /// Build a JSON error response with the required Content-Type header 106 + /// Content-Type: application/json (oauth endpoint does not like utf ending) 107 + /// Body shape: { "error": string, "error_description": string } 100 108 pub fn oauth_json_error_response( 101 109 status: StatusCode, 102 110 error: impl Into<String>, ··· 118 126 } 119 127 120 128 /// Creates a random token of 10 characters for email 2FA 121 - pub fn get_random_token(oauth: bool) -> String { 122 - let full_code = match oauth { 123 - true => Alphabetic.sample_string(&mut rand::rng(), 10), 124 - false => Alphanumeric.sample_string(&mut rand::rng(), 10), 125 - }; 129 + pub fn get_random_token() -> String { 130 + let mut rng = rand::rng(); 131 + 132 + let mut full_code = String::with_capacity(10); 133 + for _ in 0..10 { 134 + let idx = rng.random_range(0..UPPERCASE_BASE32_CHARS.len()); 135 + full_code.push(UPPERCASE_BASE32_CHARS[idx] as char); 136 + } 137 + 126 138 //The PDS implementation creates in lowercase, then converts to uppercase. 127 139 //Just going a head and doing uppercase here. 128 140 let slice_one = &full_code[0..5].to_ascii_uppercase(); ··· 162 174 } 163 175 } 164 176 177 + /// Creates a hex string from the password and salt to find app passwords 165 178 fn scrypt_hex(password: &str, salt: &str) -> anyhow::Result<String> { 166 179 let params = scrypt::Params::new(14, 8, 1, 64)?; 167 180 let mut derived = [0u8; 64]; ··· 169 182 Ok(hex::encode(derived)) 170 183 } 171 184 185 + /// Hashes the app password. did is used as the salt. 172 186 pub fn hash_app_password(did: &str, password: &str) -> anyhow::Result<String> { 173 - use sha2::{Digest, Sha256}; 174 187 let mut hasher = Sha256::new(); 175 188 hasher.update(did.as_bytes()); 176 189 let sha = hasher.finalize(); ··· 253 266 let verified = verify_password(password, &password_scrypt).await?; 254 267 if !verified { 255 268 if oauth { 269 + //OAuth does not allow app password logins so just go ahead and send it along it's way 256 270 return Ok(AuthResult::WrongIdentityOrPassword); 257 271 } 258 272 //Theres a chance it could be an app password so check that as well ··· 288 302 if two_factor_required { 289 303 //Two factor is required and a taken was provided 290 304 if let Some(two_factor_code) = two_factor_code { 291 - //It seems it sends over a empty on login without it set? As in no input is shown on the ui for the first login try 305 + //if the two_factor_code is set need to see if we have a valid token 292 306 if !two_factor_code.is_empty() { 293 307 return match assert_valid_token( 294 308 &state.account_pool, ··· 313 327 } 314 328 } 315 329 316 - return match create_two_factor_token(&state.account_pool, did, oauth).await { 330 + return match create_two_factor_token(&state.account_pool, did).await { 317 331 Ok(code) => { 318 332 let mut email_data = Map::new(); 319 333 email_data.insert("token".to_string(), Value::from(code.clone())); ··· 363 377 pub async fn create_two_factor_token( 364 378 account_db: &SqlitePool, 365 379 did: String, 366 - oauth: bool, 367 380 ) -> anyhow::Result<String> { 368 381 let purpose = "2fa_code"; 369 382 370 - let token = get_random_token(oauth); 383 + let token = get_random_token(); 371 384 let right_now = Utc::now(); 372 385 373 386 let res = sqlx::query(
+8 -16
src/oauth_provider.rs
··· 1 1 use crate::AppState; 2 - use crate::helpers::{ 3 - AuthResult, TokenCheckError, json_error_response, oauth_json_error_response, preauth_check, 4 - }; 2 + use crate::helpers::{AuthResult, TokenCheckError, oauth_json_error_response, preauth_check}; 5 3 use axum::body::Body; 6 4 use axum::extract::State; 7 5 use axum::http::header::CONTENT_TYPE; ··· 90 88 91 89 Ok(proxied) 92 90 } 93 - AuthResult::TokenCheckFailed(err) => match err { 94 - TokenCheckError::InvalidToken => oauth_json_error_response( 95 - StatusCode::BAD_REQUEST, 96 - "InvalidToken", 97 - "Token is invalid", 98 - ), 99 - TokenCheckError::ExpiredToken => oauth_json_error_response( 100 - StatusCode::BAD_REQUEST, 101 - "ExpiredToken", 102 - "Token is expired", 103 - ), 104 - }, 91 + AuthResult::TokenCheckFailed(err) => oauth_json_error_response( 92 + StatusCode::BAD_REQUEST, 93 + "invalid_request", 94 + "Unable to sign-in due to an unexpected server error", 95 + ), 105 96 }, 106 97 Err(err) => { 107 98 log::error!( 108 99 "Error during pre-auth check. This happens on the create_session endpoint when trying to decide if the user has access\n {err}" 109 100 ); 110 - json_error_response( 101 + //TODO throw a hard error and test this 102 + oauth_json_error_response( 111 103 StatusCode::INTERNAL_SERVER_ERROR, 112 104 "InternalServerError", 113 105 "This error was not generated by the PDS, but PDS Gatekeeper. Please contact your PDS administrator for help and for them to review the server logs.",