Microservice to bring 2FA to self hosted PDSes

clippy warnings and unwrap cleanups

+7 -7
src/main.rs
··· 21 use tower_governor::governor::GovernorConfigBuilder; 22 use tower_http::compression::CompressionLayer; 23 use tower_http::cors::{Any, CorsLayer}; 24 - use tracing::{error, log}; 25 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 26 27 mod middleware; ··· 74 75 let intro = "\n\nThis is a PDS gatekeeper\n\nCode: https://tangled.sh/@baileytownsend.dev/pds-gatekeeper\n"; 76 77 - let banner = format!(" {}\n{}", body, intro); 78 79 ( 80 [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], ··· 85 #[tokio::main] 86 async fn main() -> Result<(), Box<dyn std::error::Error>> { 87 setup_tracing(); 88 - //TODO prod 89 dotenvy::from_path(Path::new("./pds.env"))?; 90 let pds_root = env::var("PDS_DATA_DIRECTORY")?; 91 - let account_db_url = format!("{}/account.sqlite", pds_root); 92 - log::info!("accounts_db_url: {}", account_db_url); 93 94 let account_options = SqliteConnectOptions::new() 95 .journal_mode(SqliteJournalMode::Wal) ··· 100 .connect_with(account_options) 101 .await?; 102 103 - let bells_db_url = format!("{}/pds_gatekeeper.sqlite", pds_root); 104 let options = SqliteConnectOptions::new() 105 .journal_mode(SqliteJournalMode::Wal) 106 .filename(bells_db_url) ··· 149 .per_second(60) 150 .burst_size(5) 151 .finish() 152 - .unwrap(); 153 let governor_limiter = governor_conf.limiter().clone(); 154 let interval = Duration::from_secs(60); 155 // a separate background task to clean up
··· 21 use tower_governor::governor::GovernorConfigBuilder; 22 use tower_http::compression::CompressionLayer; 23 use tower_http::cors::{Any, CorsLayer}; 24 + use tracing::{error}; 25 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 26 27 mod middleware; ··· 74 75 let intro = "\n\nThis is a PDS gatekeeper\n\nCode: https://tangled.sh/@baileytownsend.dev/pds-gatekeeper\n"; 76 77 + let banner = format!(" {body}\n{intro}"); 78 79 ( 80 [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], ··· 85 #[tokio::main] 86 async fn main() -> Result<(), Box<dyn std::error::Error>> { 87 setup_tracing(); 88 + //TODO may need to change where this reads from? Like an env variable for it's location? 89 dotenvy::from_path(Path::new("./pds.env"))?; 90 let pds_root = env::var("PDS_DATA_DIRECTORY")?; 91 + let account_db_url = format!("{pds_root}/account.sqlite"); 92 93 let account_options = SqliteConnectOptions::new() 94 .journal_mode(SqliteJournalMode::Wal) ··· 99 .connect_with(account_options) 100 .await?; 101 102 + let bells_db_url = format!("{pds_root}/pds_gatekeeper.sqlite"); 103 let options = SqliteConnectOptions::new() 104 .journal_mode(SqliteJournalMode::Wal) 105 .filename(bells_db_url) ··· 148 .per_second(60) 149 .burst_size(5) 150 .finish() 151 + .expect("failed to create governor config. this hsould not happen and is a bug"); 152 + 153 let governor_limiter = governor_conf.limiter().clone(); 154 let interval = Duration::from_secs(60); 155 // a separate background task to clean up
+9 -11
src/middleware.rs
··· 23 match token { 24 Ok(token) => { 25 match token { 26 - None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "").unwrap(), 27 Some(token) => { 28 let token = UntrustedToken::new(&token); 29 - //Doing weird unwraps cause I can't do Result for middleware? 30 if token.is_err() { 31 return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 32 - .unwrap(); 33 } 34 - let parsed_token = token.unwrap(); 35 let claims: Result<Claims<TokenClaims>, ValidationError> = 36 parsed_token.deserialize_claims_unchecked(); 37 if claims.is_err() { 38 return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 39 - .unwrap(); 40 } 41 42 - let key = Hs256Key::new(env::var("PDS_JWT_SECRET").unwrap()); 43 let token: Result<Token<TokenClaims>, ValidationError> = 44 Hs256.validator(&key).validate(&parsed_token); 45 if token.is_err() { 46 return json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 47 - .unwrap(); 48 } 49 - let token = token.unwrap(); 50 //Not going to worry about expiration since it still goes to the PDS 51 - 52 req.extensions_mut() 53 .insert(Did(Some(token.claims().custom.sub.clone()))); 54 next.run(req).await ··· 56 } 57 } 58 Err(err) => { 59 - log::error!("Error extracting token: {}", err); 60 - json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "").unwrap() 61 } 62 } 63 }
··· 23 match token { 24 Ok(token) => { 25 match token { 26 + None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "").expect("Error creating an error response"), 27 Some(token) => { 28 let token = UntrustedToken::new(&token); 29 if token.is_err() { 30 return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 31 + .expect("Error creating an error response"); 32 } 33 + let parsed_token = token.expect("Already checked for error"); 34 let claims: Result<Claims<TokenClaims>, ValidationError> = 35 parsed_token.deserialize_claims_unchecked(); 36 if claims.is_err() { 37 return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 38 + .expect("Error creating an error response"); 39 } 40 41 + let key = Hs256Key::new(env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env")); 42 let token: Result<Token<TokenClaims>, ValidationError> = 43 Hs256.validator(&key).validate(&parsed_token); 44 if token.is_err() { 45 return json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 46 + .expect("Error creating an error response"); 47 } 48 + let token = token.expect("Already checked for error,"); 49 //Not going to worry about expiration since it still goes to the PDS 50 req.extensions_mut() 51 .insert(Did(Some(token.claims().custom.sub.clone()))); 52 next.run(req).await ··· 54 } 55 } 56 Err(err) => { 57 + log::error!("Error extracting token: {err}"); 58 + json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "").expect("Error creating an error response") 59 } 60 } 61 }
+2 -2
src/xrpc/com_atproto_server.rs
··· 122 }, 123 Err(err) => { 124 log::error!( 125 - "Error during pre-auth check. This happens on the create_session endpoint when trying to decide if the user has access\n {}", 126 - err 127 ); 128 json_error_response( 129 StatusCode::INTERNAL_SERVER_ERROR,
··· 122 }, 123 Err(err) => { 124 log::error!( 125 + "Error during pre-auth check. This happens on the create_session endpoint when trying to decide if the user has access\n {err}" 126 + 127 ); 128 json_error_response( 129 StatusCode::INTERNAL_SERVER_ERROR,
+27 -32
src/xrpc/helpers.rs
··· 1 use crate::AppState; 2 use crate::xrpc::helpers::TokenCheckError::InvalidToken; 3 use axum::body::{Body, to_bytes}; ··· 103 //Just going a head and doing uppercase here. 104 let slice_one = &full_code[0..5].to_ascii_uppercase(); 105 let slice_two = &full_code[5..10].to_ascii_uppercase(); 106 - format!("{}-{}", slice_one, slice_two) 107 } 108 109 pub enum TokenCheckError { ··· 151 let sha = hasher.finalize(); 152 let salt = hex::encode(&sha[..16]); 153 let hash_hex = scrypt_hex(password, &salt)?; 154 - Ok(format!("{}:{}", salt, hash_hex)) 155 } 156 157 - async fn verify_password(password: &str, password_scrypt: &str) -> Result<bool, StatusCode> { 158 // Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes) 159 let mut parts = password_scrypt.splitn(2, ':'); 160 let salt = match parts.next() { ··· 195 ) 196 .bind(identifier) 197 .fetch_optional(&state.account_pool) 198 - .await 199 - .map_err(|_| StatusCode::BAD_REQUEST)?, 200 IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>( 201 "SELECT account.did, account.passwordScrypt, account.email, actor.handle 202 FROM actor ··· 205 ) 206 .bind(identifier) 207 .fetch_optional(&state.account_pool) 208 - .await 209 - .map_err(|_| StatusCode::BAD_REQUEST)?, 210 IdentifierType::Did => sqlx::query_as::<_, (String, String, String, String)>( 211 "SELECT account.did, account.passwordScrypt, account.email, actor.handle 212 FROM actor ··· 215 ) 216 .bind(identifier) 217 .fetch_optional(&state.account_pool) 218 - .await 219 - .map_err(|_| StatusCode::BAD_REQUEST)?, 220 }; 221 222 if let Some((did, password_scrypt, email, handle)) = account_row { ··· 226 ) 227 .bind(did.clone()) 228 .fetch_optional(&state.pds_gatekeeper_pool) 229 - .await 230 - .map_err(|_| StatusCode::BAD_REQUEST)?; 231 232 let two_factor_required = match required_opt { 233 Some(row) => row.0 != 0, ··· 249 } 250 } 251 Err(err) => { 252 - log::error!("Error checking the app password: {}", err); 253 - Err(StatusCode::BAD_REQUEST) 254 } 255 }; 256 } ··· 266 .await 267 { 268 Ok(_) => { 269 - let _ = delete_all_email_tokens(&state.account_pool, did.clone()).await; 270 Ok(AuthResult::ProxyThrough) 271 } 272 Err(err) => Ok(AuthResult::TokenCheckFailed(err)), ··· 275 } 276 277 return match create_two_factor_token(&state.account_pool, did).await { 278 - //TODO replace unwraps with the mythical ? 279 Ok(code) => { 280 let mut email_data = Map::new(); 281 email_data.insert("token".to_string(), Value::from(code.clone())); 282 email_data.insert("handle".to_string(), Value::from(handle.clone())); 283 - //TODO bad unwrap 284 let email_body = state 285 .template_engine 286 - .render("two_factor_code.hbs", email_data) 287 - .unwrap(); 288 289 let email = Message::builder() 290 //TODO prob get the proper type in the state 291 - .from(state.mailer_from.parse().unwrap()) 292 - .to(email.parse().unwrap()) 293 .subject("Sign in to Bluesky") 294 .multipart( 295 MultiPart::alternative() // This is composed of two parts. 296 .singlepart( 297 SinglePart::builder() 298 .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. 300 ) 301 .singlepart( 302 SinglePart::builder() 303 .header(header::ContentType::TEXT_HTML) 304 .body(email_body), 305 ), 306 - ) 307 - //TODO bad 308 - .unwrap(); 309 match state.mailer.send(email).await { 310 Ok(_) => Ok(AuthResult::TwoFactorRequired), 311 Err(err) => { 312 - log::error!("Error sending the 2FA email: {}", err); 313 - Err(StatusCode::BAD_REQUEST) 314 } 315 } 316 } 317 Err(err) => { 318 - log::error!("error on creating a 2fa token: {}", err); 319 - Err(StatusCode::BAD_REQUEST) 320 } 321 }; 322 } ··· 351 352 match res { 353 Ok(_) => Ok(token), 354 - Err(e) => { 355 - log::error!("Error creating a two factor token: {}", e); 356 - Err(anyhow::anyhow!(e)) 357 } 358 } 359 } ··· 383 .fetch_optional(account_db) 384 .await 385 .map_err(|err| { 386 - log::error!("Error getting the 2fa token: {}", err); 387 InvalidToken 388 })?; 389
··· 1 + use anyhow::anyhow; 2 use crate::AppState; 3 use crate::xrpc::helpers::TokenCheckError::InvalidToken; 4 use axum::body::{Body, to_bytes}; ··· 104 //Just going a head and doing uppercase here. 105 let slice_one = &full_code[0..5].to_ascii_uppercase(); 106 let slice_two = &full_code[5..10].to_ascii_uppercase(); 107 + format!("{slice_one}-{slice_two}") 108 } 109 110 pub enum TokenCheckError { ··· 152 let sha = hasher.finalize(); 153 let salt = hex::encode(&sha[..16]); 154 let hash_hex = scrypt_hex(password, &salt)?; 155 + Ok(format!("{salt}:{hash_hex}")) 156 } 157 158 + async fn verify_password(password: &str, password_scrypt: &str) -> anyhow::Result<bool> { 159 // Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes) 160 let mut parts = password_scrypt.splitn(2, ':'); 161 let salt = match parts.next() { ··· 196 ) 197 .bind(identifier) 198 .fetch_optional(&state.account_pool) 199 + .await?, 200 IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>( 201 "SELECT account.did, account.passwordScrypt, account.email, actor.handle 202 FROM actor ··· 205 ) 206 .bind(identifier) 207 .fetch_optional(&state.account_pool) 208 + .await?, 209 IdentifierType::Did => sqlx::query_as::<_, (String, String, String, String)>( 210 "SELECT account.did, account.passwordScrypt, account.email, actor.handle 211 FROM actor ··· 214 ) 215 .bind(identifier) 216 .fetch_optional(&state.account_pool) 217 + .await?, 218 }; 219 220 if let Some((did, password_scrypt, email, handle)) = account_row { ··· 224 ) 225 .bind(did.clone()) 226 .fetch_optional(&state.pds_gatekeeper_pool) 227 + .await?; 228 229 let two_factor_required = match required_opt { 230 Some(row) => row.0 != 0, ··· 246 } 247 } 248 Err(err) => { 249 + log::error!("Error checking the app password: {err}"); 250 + Err(err) 251 } 252 }; 253 } ··· 263 .await 264 { 265 Ok(_) => { 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 Ok(AuthResult::ProxyThrough) 271 } 272 Err(err) => Ok(AuthResult::TokenCheckFailed(err)), ··· 275 } 276 277 return match create_two_factor_token(&state.account_pool, did).await { 278 Ok(code) => { 279 let mut email_data = Map::new(); 280 email_data.insert("token".to_string(), Value::from(code.clone())); 281 email_data.insert("handle".to_string(), Value::from(handle.clone())); 282 let email_body = state 283 .template_engine 284 + .render("two_factor_code.hbs", email_data)?; 285 286 let email = Message::builder() 287 //TODO prob get the proper type in the state 288 + .from(state.mailer_from.parse()?) 289 + .to(email.parse()?) 290 .subject("Sign in to Bluesky") 291 .multipart( 292 MultiPart::alternative() // This is composed of two parts. 293 .singlepart( 294 SinglePart::builder() 295 .header(header::ContentType::TEXT_PLAIN) 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. 297 ) 298 .singlepart( 299 SinglePart::builder() 300 .header(header::ContentType::TEXT_HTML) 301 .body(email_body), 302 ), 303 + )?; 304 match state.mailer.send(email).await { 305 Ok(_) => Ok(AuthResult::TwoFactorRequired), 306 Err(err) => { 307 + log::error!("Error sending the 2FA email: {err}"); 308 + Err(anyhow!(err)) 309 } 310 } 311 } 312 Err(err) => { 313 + log::error!("error on creating a 2fa token: {err}"); 314 + Err(anyhow!(err)) 315 } 316 }; 317 } ··· 346 347 match res { 348 Ok(_) => Ok(token), 349 + Err(err) => { 350 + log::error!("Error creating a two factor token: {err}"); 351 + Err(anyhow::anyhow!(err)) 352 } 353 } 354 } ··· 378 .fetch_optional(account_db) 379 .await 380 .map_err(|err| { 381 + log::error!("Error getting the 2fa token: {err}"); 382 InvalidToken 383 })?; 384