2FA logins gatekept #1

merged
opened by baileytownsend.dev targeting main from feature/2faCodeGeneration
Changed files
+52 -49
migrations_bells_and_whistles
src
-3
migrations_bells_and_whistles/.keep
··· 1 - # This directory holds SQLx migrations for the bells_and_whistles.sqlite database. 2 - # It is intentionally empty for now; running `sqlx::migrate!` will still ensure the 3 - # migrations table exists and succeed with zero migrations.
+1
Cargo.lock
··· 1669 1669 "scrypt", 1670 1670 "serde", 1671 1671 "serde_json", 1672 + "sha2", 1672 1673 "sqlx", 1673 1674 "tokio", 1674 1675 "tower-http",
+1
Cargo.toml
··· 25 25 rand = "0.9.2" 26 26 anyhow = "1.0.99" 27 27 chrono = "0.4.41" 28 + sha2 = "0.10"
+27 -32
src/xrpc/helpers.rs
··· 1 + use anyhow::anyhow; 1 2 use crate::AppState; 2 3 use crate::xrpc::helpers::TokenCheckError::InvalidToken; 3 4 use axum::body::{Body, to_bytes}; ··· 103 104 //Just going a head and doing uppercase here. 104 105 let slice_one = &full_code[0..5].to_ascii_uppercase(); 105 106 let slice_two = &full_code[5..10].to_ascii_uppercase(); 106 - format!("{}-{}", slice_one, slice_two) 107 + format!("{slice_one}-{slice_two}") 107 108 } 108 109 109 110 pub enum TokenCheckError { ··· 151 152 let sha = hasher.finalize(); 152 153 let salt = hex::encode(&sha[..16]); 153 154 let hash_hex = scrypt_hex(password, &salt)?; 154 - Ok(format!("{}:{}", salt, hash_hex)) 155 + Ok(format!("{salt}:{hash_hex}")) 155 156 } 156 157 157 - async fn verify_password(password: &str, password_scrypt: &str) -> Result<bool, StatusCode> { 158 + async fn verify_password(password: &str, password_scrypt: &str) -> anyhow::Result<bool> { 158 159 // Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes) 159 160 let mut parts = password_scrypt.splitn(2, ':'); 160 161 let salt = match parts.next() { ··· 195 196 ) 196 197 .bind(identifier) 197 198 .fetch_optional(&state.account_pool) 198 - .await 199 - .map_err(|_| StatusCode::BAD_REQUEST)?, 199 + .await?, 200 200 IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>( 201 201 "SELECT account.did, account.passwordScrypt, account.email, actor.handle 202 202 FROM actor ··· 205 205 ) 206 206 .bind(identifier) 207 207 .fetch_optional(&state.account_pool) 208 - .await 209 - .map_err(|_| StatusCode::BAD_REQUEST)?, 208 + .await?, 210 209 IdentifierType::Did => sqlx::query_as::<_, (String, String, String, String)>( 211 210 "SELECT account.did, account.passwordScrypt, account.email, actor.handle 212 211 FROM actor ··· 215 214 ) 216 215 .bind(identifier) 217 216 .fetch_optional(&state.account_pool) 218 - .await 219 - .map_err(|_| StatusCode::BAD_REQUEST)?, 217 + .await?, 220 218 }; 221 219 222 220 if let Some((did, password_scrypt, email, handle)) = account_row { ··· 226 224 ) 227 225 .bind(did.clone()) 228 226 .fetch_optional(&state.pds_gatekeeper_pool) 229 - .await 230 - .map_err(|_| StatusCode::BAD_REQUEST)?; 227 + .await?; 231 228 232 229 let two_factor_required = match required_opt { 233 230 Some(row) => row.0 != 0, ··· 249 246 } 250 247 } 251 248 Err(err) => { 252 - log::error!("Error checking the app password: {}", err); 253 - Err(StatusCode::BAD_REQUEST) 249 + log::error!("Error checking the app password: {err}"); 250 + Err(err) 254 251 } 255 252 }; 256 253 } ··· 266 263 .await 267 264 { 268 265 Ok(_) => { 269 - let _ = delete_all_email_tokens(&state.account_pool, did.clone()).await; 266 + let result_of_cleanup = delete_all_email_tokens(&state.account_pool, did.clone()).await; 267 + if result_of_cleanup.is_err(){ 268 + log::error!("There was an error deleting the email tokens after login: {:?}", result_of_cleanup.err()) 269 + } 270 270 Ok(AuthResult::ProxyThrough) 271 271 } 272 272 Err(err) => Ok(AuthResult::TokenCheckFailed(err)), ··· 275 275 } 276 276 277 277 return match create_two_factor_token(&state.account_pool, did).await { 278 - //TODO replace unwraps with the mythical ? 279 278 Ok(code) => { 280 279 let mut email_data = Map::new(); 281 280 email_data.insert("token".to_string(), Value::from(code.clone())); 282 281 email_data.insert("handle".to_string(), Value::from(handle.clone())); 283 - //TODO bad unwrap 284 282 let email_body = state 285 283 .template_engine 286 - .render("two_factor_code.hbs", email_data) 287 - .unwrap(); 284 + .render("two_factor_code.hbs", email_data)?; 288 285 289 286 let email = Message::builder() 290 287 //TODO prob get the proper type in the state 291 - .from(state.mailer_from.parse().unwrap()) 292 - .to(email.parse().unwrap()) 288 + .from(state.mailer_from.parse()?) 289 + .to(email.parse()?) 293 290 .subject("Sign in to Bluesky") 294 291 .multipart( 295 292 MultiPart::alternative() // This is composed of two parts. 296 293 .singlepart( 297 294 SinglePart::builder() 298 295 .header(header::ContentType::TEXT_PLAIN) 299 - .body(format!("We received a sign-in request for the account @{}. Use the code: {} to sign in. If this wasn't you, we recommend taking steps to protect your account by changing your password at https://bsky.app/settings.", handle, code)), // Every message should have a plain text fallback. 296 + .body(format!("We received a sign-in request for the account @{handle}. Use the code: {code} to sign in. If this wasn't you, we recommend taking steps to protect your account by changing your password at https://bsky.app/settings.")), // Every message should have a plain text fallback. 300 297 ) 301 298 .singlepart( 302 299 SinglePart::builder() 303 300 .header(header::ContentType::TEXT_HTML) 304 301 .body(email_body), 305 302 ), 306 - ) 307 - //TODO bad 308 - .unwrap(); 303 + )?; 309 304 match state.mailer.send(email).await { 310 305 Ok(_) => Ok(AuthResult::TwoFactorRequired), 311 306 Err(err) => { 312 - log::error!("Error sending the 2FA email: {}", err); 313 - Err(StatusCode::BAD_REQUEST) 307 + log::error!("Error sending the 2FA email: {err}"); 308 + Err(anyhow!(err)) 314 309 } 315 310 } 316 311 } 317 312 Err(err) => { 318 - log::error!("error on creating a 2fa token: {}", err); 319 - Err(StatusCode::BAD_REQUEST) 313 + log::error!("error on creating a 2fa token: {err}"); 314 + Err(anyhow!(err)) 320 315 } 321 316 }; 322 317 } ··· 351 346 352 347 match res { 353 348 Ok(_) => Ok(token), 354 - Err(e) => { 355 - log::error!("Error creating a two factor token: {}", e); 356 - Err(anyhow::anyhow!(e)) 349 + Err(err) => { 350 + log::error!("Error creating a two factor token: {err}"); 351 + Err(anyhow::anyhow!(err)) 357 352 } 358 353 } 359 354 } ··· 383 378 .fetch_optional(account_db) 384 379 .await 385 380 .map_err(|err| { 386 - log::error!("Error getting the 2fa token: {}", err); 381 + log::error!("Error getting the 2fa token: {err}"); 387 382 InvalidToken 388 383 })?; 389 384
+8 -4
src/middleware.rs
··· 1 - use crate::xrpc::helpers::json_error_response; 1 + use crate::helpers::json_error_response; 2 2 use axum::extract::Request; 3 3 use axum::http::{HeaderMap, StatusCode}; 4 4 use axum::middleware::Next; ··· 23 23 match token { 24 24 Ok(token) => { 25 25 match token { 26 - None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "").expect("Error creating an error response"), 26 + None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 27 + .expect("Error creating an error response"), 27 28 Some(token) => { 28 29 let token = UntrustedToken::new(&token); 29 30 if token.is_err() { ··· 38 39 .expect("Error creating an error response"); 39 40 } 40 41 41 - let key = Hs256Key::new(env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env")); 42 + let key = Hs256Key::new( 43 + env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"), 44 + ); 42 45 let token: Result<Token<TokenClaims>, ValidationError> = 43 46 Hs256.validator(&key).validate(&parsed_token); 44 47 if token.is_err() { ··· 55 58 } 56 59 Err(err) => { 57 60 log::error!("Error extracting token: {err}"); 58 - json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "").expect("Error creating an error response") 61 + json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 62 + .expect("Error creating an error response") 59 63 } 60 64 } 61 65 }
-1
src/xrpc/mod.rs
··· 1 1 pub mod com_atproto_server; 2 - pub mod helpers;
+10 -2
src/main.rs
··· 133 133 AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); 134 134 //Email templates setup 135 135 let mut hbs = Handlebars::new(); 136 - //TODO add an override to manually load in the hbs templates 137 - let _ = hbs.register_embed_templates::<EmailTemplates>(); 136 + 137 + let users_email_directory = env::var("GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY"); 138 + if let Ok(users_email_directory) = users_email_directory { 139 + hbs.register_template_file( 140 + "two_factor_code.hbs", 141 + format!("{users_email_directory}/two_factor_code.hbs"), 142 + )?; 143 + } else { 144 + let _ = hbs.register_embed_templates::<EmailTemplates>(); 145 + } 138 146 139 147 let pds_base_url = 140 148 env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
+5 -6
README.md
··· 12 12 13 13 ## 2FA 14 14 15 - - [x] Ability to turn on/off 2FA 16 - - [x] getSession overwrite to set the `emailAuthFactor` flag if the user has 2FA turned on 17 - - [x] send an email using the `PDS_EMAIL_SMTP_URL` with a handlebar email template like Bluesky's 2FA sign in email. 18 - - [ ] generate a 2FA code 19 - - [ ] createSession gatekeeping (It does stop logins, just eh, doesn't actually send a real code or check it yet) 20 - - [ ] oauth endpoint gatekeeping 15 + - Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins 16 + - Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA 21 17 22 18 ## Captcha on Create Account 23 19 ··· 25 21 26 22 # Setup 27 23 24 + We are getting close! Testing now 25 + 28 26 Nothing here yet! If you are brave enough to try before full release, let me know and I'll help you set it up. 29 27 But I want to run it locally on my own PDS first to test run it a bit. 30 28 ··· 37 35 path /xrpc/com.atproto.server.getSession 38 36 path /xrpc/com.atproto.server.updateEmail 39 37 path /xrpc/com.atproto.server.createSession 38 + path /@atproto/oauth-provider/~api/sign-in 40 39 } 41 40 42 41 handle @gatekeeper {
-1
src/helpers.rs
··· 11 11 use lettre::message::{MultiPart, SinglePart, header}; 12 12 use lettre::{AsyncTransport, Message}; 13 13 use rand::Rng; 14 - use rand::distr::{Alphabetic, Alphanumeric, SampleString}; 15 14 use serde::de::DeserializeOwned; 16 15 use serde_json::{Map, Value}; 17 16 use sha2::{Digest, Sha256};