Microservice to bring 2FA to self hosted PDSes

Some clippy warning clean ups

Changed files
+38 -57
src
+4 -4
src/main.rs
··· 1 use crate::xrpc::com_atproto_server::{create_session, get_session, update_email}; 2 - use axum::middleware as ax_middleware; 3 - mod middleware; 4 use axum::body::Body; 5 use axum::handler::Handler; 6 use axum::http::{Method, header}; 7 use axum::routing::post; 8 use axum::{Router, routing::get}; 9 use axum_template::engine::Engine; ··· 24 use tracing::{error, log}; 25 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 26 27 mod xrpc; 28 29 type HyperUtilClient = hyper_util::client::legacy::Client<HttpConnector, Body>; ··· 87 //TODO prod 88 dotenvy::from_path(Path::new("./pds.env"))?; 89 let pds_root = env::var("PDS_DATA_DIRECTORY")?; 90 - // let pds_root = "/home/baileytownsend/Documents/code/docker_compose/pds/pds_data"; 91 let account_db_url = format!("{}/account.sqlite", pds_root); 92 log::info!("accounts_db_url: {}", account_db_url); 93 ··· 110 .connect_with(options) 111 .await?; 112 113 - // Run migrations for the bells_and_whistles database 114 // Note: the migrations are embedded at compile time from the given directory 115 // sqlx 116 sqlx::migrate!("./migrations")
··· 1 + #![warn(clippy::unwrap_used)] 2 use crate::xrpc::com_atproto_server::{create_session, get_session, update_email}; 3 use axum::body::Body; 4 use axum::handler::Handler; 5 use axum::http::{Method, header}; 6 + use axum::middleware as ax_middleware; 7 use axum::routing::post; 8 use axum::{Router, routing::get}; 9 use axum_template::engine::Engine; ··· 24 use tracing::{error, log}; 25 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 26 27 + mod middleware; 28 mod xrpc; 29 30 type HyperUtilClient = hyper_util::client::legacy::Client<HttpConnector, Body>; ··· 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 ··· 110 .connect_with(options) 111 .await?; 112 113 + // Run migrations for the extra database 114 // Note: the migrations are embedded at compile time from the given directory 115 // sqlx 116 sqlx::migrate!("./migrations")
+11 -28
src/middleware.rs
··· 7 use jwt_compact::{AlgorithmExt, Claims, Token, UntrustedToken, ValidationError}; 8 use serde::{Deserialize, Serialize}; 9 use std::env; 10 11 #[derive(Clone, Debug)] 12 pub struct Did(pub Option<String>); ··· 22 match token { 23 Ok(token) => { 24 match token { 25 - None => { 26 - return json_error_response( 27 - StatusCode::BAD_REQUEST, 28 - "TokenRequired", 29 - "", 30 - ).unwrap(); 31 - } 32 Some(token) => { 33 let token = UntrustedToken::new(&token); 34 //Doing weird unwraps cause I can't do Result for middleware? 35 if token.is_err() { 36 - return json_error_response( 37 - StatusCode::BAD_REQUEST, 38 - "TokenRequired", 39 - "", 40 - ).unwrap(); 41 } 42 let parsed_token = token.unwrap(); 43 let claims: Result<Claims<TokenClaims>, ValidationError> = 44 parsed_token.deserialize_claims_unchecked(); 45 if claims.is_err() { 46 - return json_error_response( 47 - StatusCode::BAD_REQUEST, 48 - "TokenRequired", 49 - "", 50 - ).unwrap(); 51 } 52 53 let key = Hs256Key::new(env::var("PDS_JWT_SECRET").unwrap()); 54 let token: Result<Token<TokenClaims>, ValidationError> = 55 Hs256.validator(&key).validate(&parsed_token); 56 if token.is_err() { 57 - return json_error_response( 58 - StatusCode::BAD_REQUEST, 59 - "InvalidToken", 60 - "", 61 - ).unwrap(); 62 } 63 let token = token.unwrap(); 64 //Not going to worry about expiration since it still goes to the PDS ··· 69 } 70 } 71 } 72 - Err(_) => { 73 - return json_error_response( 74 - StatusCode::BAD_REQUEST, 75 - "InvalidToken", 76 - "", 77 - ).unwrap(); 78 } 79 } 80 }
··· 7 use jwt_compact::{AlgorithmExt, Claims, Token, UntrustedToken, ValidationError}; 8 use serde::{Deserialize, Serialize}; 9 use std::env; 10 + use tracing::log; 11 12 #[derive(Clone, Debug)] 13 pub struct Did(pub Option<String>); ··· 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 ··· 55 } 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 -25
src/xrpc/helpers.rs
··· 121 122 pub enum IdentifierType { 123 Email, 124 - DID, 125 Handle, 126 } 127 ··· 130 if identifier.contains("@") { 131 IdentifierType::Email 132 } else if identifier.contains("did:") { 133 - IdentifierType::DID 134 } else { 135 IdentifierType::Handle 136 } ··· 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 213 LEFT JOIN account ON actor.did = account.did ··· 224 let required_opt = sqlx::query_as::<_, (u8,)>( 225 "SELECT required FROM two_factor_accounts WHERE did = ? LIMIT 1", 226 ) 227 - .bind(&did.clone()) 228 .fetch_optional(&state.pds_gatekeeper_pool) 229 .await 230 .map_err(|_| StatusCode::BAD_REQUEST)?; ··· 239 let verified = verify_password(password, &password_scrypt).await?; 240 if !verified { 241 //Theres a chance it could be an app password so check that as well 242 - return match verify_app_password(&state.account_pool, &did, &password).await { 243 Ok(valid) => { 244 if valid { 245 //Was a valid app password up to the PDS now ··· 257 //Two factor is required and a taken was provided 258 if let Some(two_factor_code) = two_factor_code { 259 //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 260 - if two_factor_code != "" { 261 return match assert_valid_token( 262 &state.account_pool, 263 did.clone(), ··· 332 ) -> anyhow::Result<String> { 333 let purpose = "2fa_code"; 334 335 - loop { 336 - let token = get_random_token(); 337 - let right_now = Utc::now(); 338 339 - let res = sqlx::query( 340 - "INSERT INTO email_token (purpose, did, token, requestedAt) 341 VALUES (?, ?, ?, ?) 342 ON CONFLICT(purpose, did) DO UPDATE SET 343 token=excluded.token, 344 requestedAt=excluded.requestedAt", 345 - ) 346 - .bind(purpose) 347 - .bind(&did) 348 - .bind(&token) 349 - .bind(right_now) 350 - .execute(account_db) 351 - .await; 352 353 - return match res { 354 - Ok(_) => Ok(token), 355 - Err(e) => { 356 - log::error!("Error creating a two factor token: {}", e); 357 - Err(anyhow::anyhow!(e)) 358 - } 359 - }; 360 } 361 } 362
··· 121 122 pub enum IdentifierType { 123 Email, 124 + Did, 125 Handle, 126 } 127 ··· 130 if identifier.contains("@") { 131 IdentifierType::Email 132 } else if identifier.contains("did:") { 133 + IdentifierType::Did 134 } else { 135 IdentifierType::Handle 136 } ··· 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 213 LEFT JOIN account ON actor.did = account.did ··· 224 let required_opt = sqlx::query_as::<_, (u8,)>( 225 "SELECT required FROM two_factor_accounts WHERE did = ? LIMIT 1", 226 ) 227 + .bind(did.clone()) 228 .fetch_optional(&state.pds_gatekeeper_pool) 229 .await 230 .map_err(|_| StatusCode::BAD_REQUEST)?; ··· 239 let verified = verify_password(password, &password_scrypt).await?; 240 if !verified { 241 //Theres a chance it could be an app password so check that as well 242 + return match verify_app_password(&state.account_pool, &did, password).await { 243 Ok(valid) => { 244 if valid { 245 //Was a valid app password up to the PDS now ··· 257 //Two factor is required and a taken was provided 258 if let Some(two_factor_code) = two_factor_code { 259 //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 260 + if !two_factor_code.is_empty() { 261 return match assert_valid_token( 262 &state.account_pool, 263 did.clone(), ··· 332 ) -> anyhow::Result<String> { 333 let purpose = "2fa_code"; 334 335 + let token = get_random_token(); 336 + let right_now = Utc::now(); 337 338 + let res = sqlx::query( 339 + "INSERT INTO email_token (purpose, did, token, requestedAt) 340 VALUES (?, ?, ?, ?) 341 ON CONFLICT(purpose, did) DO UPDATE SET 342 token=excluded.token, 343 requestedAt=excluded.requestedAt", 344 + ) 345 + .bind(purpose) 346 + .bind(&did) 347 + .bind(&token) 348 + .bind(right_now) 349 + .execute(account_db) 350 + .await; 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 } 360