Microservice to bring 2FA to self hosted PDSes

Works but I feel like it should be more secure

authored by baileytownsend.dev and committed by Tangled 86cd61e8 55f5af01

Changed files
+45 -20
src
+2 -3
Cargo.toml
··· 19 hex = "0.4" 20 jwt-compact = { version = "0.8.0", features = ["es256k"] } 21 scrypt = "0.11" 22 - #lettre = { version = "0.11.18", default-features = false, features = ["pool", "tokio1-rustls", "smtp-transport", "hostname", "builder"] } 23 - #lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 24 aws-lc-rs = "1.13.0" 25 - lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 26 rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 27 handlebars = { version = "6.3.2", features = ["rust-embed"] } 28 rust-embed = "8.7.2" 29 axum-template = { version = "3.0.0", features = ["handlebars"] }
··· 19 hex = "0.4" 20 jwt-compact = { version = "0.8.0", features = ["es256k"] } 21 scrypt = "0.11" 22 + #Leaveing these two cause I think it is needed by the 23 aws-lc-rs = "1.13.0" 24 rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 25 + lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 26 handlebars = { version = "6.3.2", features = ["rust-embed"] } 27 rust-embed = "8.7.2" 28 axum-template = { version = "3.0.0", features = ["handlebars"] }
+3
src/main.rs
··· 175 .finish() 176 .expect("failed to create governor config. this should not happen and is a bug"); 177 178 let create_session_governor_limiter = create_session_governor_conf.limiter().clone(); 179 let sign_in_governor_limiter = sign_in_governor_conf.limiter().clone(); 180 let interval = Duration::from_secs(60);
··· 175 .finish() 176 .expect("failed to create governor config. this should not happen and is a bug"); 177 178 + // let create_account_limiter_time: Option<String> = 179 + // env::var("GATEKEEPER_CREATE_ACCOUNT_LIMITER_WINDOW").unwrap_or_else(|_| None); 180 + 181 let create_session_governor_limiter = create_session_governor_conf.limiter().clone(); 182 let sign_in_governor_limiter = sign_in_governor_conf.limiter().clone(); 183 let interval = Duration::from_secs(60);
+40 -17
src/middleware.rs
··· 1 use crate::helpers::json_error_response; 2 use axum::extract::Request; 3 use axum::http::{HeaderMap, StatusCode}; 4 use axum::middleware::Next; 5 use axum::response::IntoResponse; ··· 12 #[derive(Clone, Debug)] 13 pub struct Did(pub Option<String>); 14 15 #[derive(Serialize, Deserialize)] 16 pub struct TokenClaims { 17 pub sub: String, 18 } 19 20 pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse { 21 - let token = extract_bearer(req.headers()); 22 23 - match token { 24 - Ok(token) => { 25 - match token { 26 None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 27 .expect("Error creating an error response"), 28 - Some(token) => { 29 - let token = UntrustedToken::new(&token); 30 if token.is_err() { 31 return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 32 .expect("Error creating an error response"); ··· 49 .expect("Error creating an error response"); 50 } 51 let token = token.expect("Already checked for error,"); 52 - //Not going to worry about expiration since it still goes to the PDS 53 req.extensions_mut() 54 .insert(Did(Some(token.claims().custom.sub.clone()))); 55 next.run(req).await 56 } 57 } ··· 64 } 65 } 66 67 - fn extract_bearer(headers: &HeaderMap) -> Result<Option<String>, String> { 68 match headers.get(axum::http::header::AUTHORIZATION) { 69 None => Ok(None), 70 - Some(hv) => match hv.to_str() { 71 - Err(_) => Err("Authorization header is not valid".into()), 72 - Ok(s) => { 73 - // Accept forms like: "Bearer <token>" (case-sensitive for the scheme here) 74 - let mut parts = s.splitn(2, ' '); 75 - match (parts.next(), parts.next()) { 76 - (Some("Bearer"), Some(tok)) if !tok.is_empty() => Ok(Some(tok.to_string())), 77 - _ => Err("Authorization header must be in format 'Bearer <token>'".into()), 78 } 79 } 80 - }, 81 } 82 }
··· 1 use crate::helpers::json_error_response; 2 use axum::extract::Request; 3 + use axum::http::header::AUTHORIZATION; 4 use axum::http::{HeaderMap, StatusCode}; 5 use axum::middleware::Next; 6 use axum::response::IntoResponse; ··· 13 #[derive(Clone, Debug)] 14 pub struct Did(pub Option<String>); 15 16 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 17 + pub enum AuthScheme { 18 + Bearer, 19 + DPoP, 20 + } 21 + 22 #[derive(Serialize, Deserialize)] 23 pub struct TokenClaims { 24 pub sub: String, 25 } 26 27 pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse { 28 + let auth = extract_auth(req.headers()); 29 30 + match auth { 31 + Ok(auth_opt) => { 32 + match auth_opt { 33 None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 34 .expect("Error creating an error response"), 35 + Some((scheme, token_str)) => { 36 + // For Bearer, validate JWT and extract DID from `sub`. 37 + // For DPoP, we currently only pass through and do not validate here; insert None DID. 38 + // match scheme { 39 + // AuthScheme::Bearer => { 40 + let token = UntrustedToken::new(&token_str); 41 if token.is_err() { 42 return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 43 .expect("Error creating an error response"); ··· 60 .expect("Error creating an error response"); 61 } 62 let token = token.expect("Already checked for error,"); 63 + // Not going to worry about expiration since it still goes to the PDS 64 req.extensions_mut() 65 .insert(Did(Some(token.claims().custom.sub.clone()))); 66 + // } 67 + // AuthScheme::DPoP => { 68 + // // No DID extraction from DPoP here; leave None 69 + // req.extensions_mut().insert(Did(None)); 70 + // } 71 + // } 72 + 73 next.run(req).await 74 } 75 } ··· 82 } 83 } 84 85 + fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> { 86 match headers.get(axum::http::header::AUTHORIZATION) { 87 None => Ok(None), 88 + Some(hv) => { 89 + match hv.to_str() { 90 + Err(_) => Err("Authorization header is not valid".into()), 91 + Ok(s) => { 92 + // Accept forms like: "Bearer <token>" or "DPoP <token>" (case-sensitive for the scheme here) 93 + let mut parts = s.splitn(2, ' '); 94 + match (parts.next(), parts.next()) { 95 + (Some("Bearer"), Some(tok)) if !tok.is_empty() => 96 + Ok(Some((AuthScheme::Bearer, tok.to_string()))), 97 + (Some("DPoP"), Some(tok)) if !tok.is_empty() => 98 + Ok(Some((AuthScheme::DPoP, tok.to_string()))), 99 + _ => Err("Authorization header must be in format 'Bearer <token>' or 'DPoP <token>'".into()), 100 + } 101 } 102 } 103 + } 104 } 105 }