use crate::AppState; use crate::helpers::{AuthResult, oauth_json_error_response, preauth_check}; use axum::body::Body; use axum::extract::State; use axum::http::header::CONTENT_TYPE; use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::{Json, extract}; use serde::{Deserialize, Serialize}; use tracing::log; #[derive(Serialize, Deserialize, Clone)] pub struct SignInRequest { pub username: String, pub password: String, #[serde(skip_serializing_if = "Option::is_none")] pub remember: Option, pub locale: String, #[serde(skip_serializing_if = "Option::is_none", rename = "emailOtp")] pub email_otp: Option, } pub async fn sign_in( State(state): State, headers: HeaderMap, Json(mut payload): extract::Json, ) -> Result, StatusCode> { let identifier = payload.username.clone(); let password = payload.password.clone(); let auth_factor_token = payload.email_otp.clone(); match preauth_check(&state, &identifier, &password, auth_factor_token, true).await { Ok(result) => match result { AuthResult::WrongIdentityOrPassword => oauth_json_error_response( StatusCode::BAD_REQUEST, "invalid_request", "Invalid identifier or password", ), AuthResult::TwoFactorRequired(masked_email) => { let body_str = match serde_json::to_string(&serde_json::json!({ "error": "second_authentication_factor_required", "error_description": format!("emailOtp authentication factor required (hint: {})", masked_email), "type": "emailOtp", "hint": masked_email, })) { Ok(s) => s, Err(_) => return Err(StatusCode::BAD_REQUEST), }; Response::builder() .status(StatusCode::BAD_REQUEST) .header(CONTENT_TYPE, "application/json") .body(Body::from(body_str)) .map_err(|_| StatusCode::BAD_REQUEST) } AuthResult::ProxyThrough => { //No 2FA or already passed let uri = format!( "{}{}", state.app_config.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" ); let mut req = axum::http::Request::post(uri); if let Some(req_headers) = req.headers_mut() { // Copy headers but remove problematic ones. There was an issue with the PDS not parsing the body fully if i forwarded all headers copy_filtered_headers(&headers, req_headers); //Setting the content type to application/json manually req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); } //Clears the email_otp because the pds will reject a request with it. payload.email_otp = None; let payload_bytes = serde_json::to_vec(&payload).map_err(|_| StatusCode::BAD_REQUEST)?; let req = req .body(Body::from(payload_bytes)) .map_err(|_| StatusCode::BAD_REQUEST)?; let proxied = state .reverse_proxy_client .request(req) .await .map_err(|_| StatusCode::BAD_REQUEST)? .into_response(); Ok(proxied) } //Ignoring the type of token check failure. Looks like oauth on the entry treads them the same. AuthResult::TokenCheckFailed(_) => oauth_json_error_response( StatusCode::BAD_REQUEST, "invalid_request", "Unable to sign-in due to an unexpected server error", ), }, Err(err) => { log::error!( "Error during pre-auth check. This happens on the oauth signin endpoint when trying to decide if the user has access:\n {err}" ); oauth_json_error_response( StatusCode::BAD_REQUEST, "pds_gatekeeper_error", "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.", ) } } } fn is_disallowed_header(name: &HeaderName) -> bool { // possible problematic headers with proxying matches!( name.as_str(), "connection" | "keep-alive" | "proxy-authenticate" | "proxy-authorization" | "te" | "trailer" | "transfer-encoding" | "upgrade" | "host" | "content-length" | "content-encoding" | "expect" | "accept-encoding" ) } fn copy_filtered_headers(src: &HeaderMap, dst: &mut HeaderMap) { for (name, value) in src.iter() { if is_disallowed_header(name) { continue; } // Only copy valid headers if let Ok(hv) = HeaderValue::from_bytes(value.as_bytes()) { dst.insert(name.clone(), hv); } } }