this repo has no description
1use crate::api::ApiError; 2use crate::state::AppState; 3use axum::{ 4 Json, 5 extract::{Query, State}, 6 http::StatusCode, 7 response::{IntoResponse, Response}, 8}; 9use serde::{Deserialize, Serialize}; 10use serde_json::json; 11use tracing::{error, info, warn}; 12 13const HOUR_SECS: i64 = 3600; 14const MINUTE_SECS: i64 = 60; 15 16const PROTECTED_METHODS: &[&str] = &[ 17 "com.atproto.admin.sendEmail", 18 "com.atproto.identity.requestPlcOperationSignature", 19 "com.atproto.identity.signPlcOperation", 20 "com.atproto.identity.updateHandle", 21 "com.atproto.server.activateAccount", 22 "com.atproto.server.confirmEmail", 23 "com.atproto.server.createAppPassword", 24 "com.atproto.server.deactivateAccount", 25 "com.atproto.server.getAccountInviteCodes", 26 "com.atproto.server.getSession", 27 "com.atproto.server.listAppPasswords", 28 "com.atproto.server.requestAccountDelete", 29 "com.atproto.server.requestEmailConfirmation", 30 "com.atproto.server.requestEmailUpdate", 31 "com.atproto.server.revokeAppPassword", 32 "com.atproto.server.updateEmail", 33]; 34 35#[derive(Deserialize)] 36pub struct GetServiceAuthParams { 37 pub aud: String, 38 pub lxm: Option<String>, 39 pub exp: Option<i64>, 40} 41 42#[derive(Serialize)] 43pub struct GetServiceAuthOutput { 44 pub token: String, 45} 46 47pub async fn get_service_auth( 48 State(state): State<AppState>, 49 headers: axum::http::HeaderMap, 50 Query(params): Query<GetServiceAuthParams>, 51) -> Response { 52 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 53 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 54 info!( 55 has_auth_header = auth_header.is_some(), 56 has_dpop_proof = dpop_proof.is_some(), 57 aud = %params.aud, 58 lxm = ?params.lxm, 59 "getServiceAuth called" 60 ); 61 let auth_header = match auth_header { 62 Some(h) => h.trim(), 63 None => { 64 warn!("getServiceAuth: no Authorization header"); 65 return ApiError::AuthenticationRequired.into_response(); 66 } 67 }; 68 69 let (token, is_dpop) = if auth_header.len() >= 7 70 && auth_header[..7].eq_ignore_ascii_case("bearer ") 71 { 72 (auth_header[7..].trim().to_string(), false) 73 } else if auth_header.len() >= 5 && auth_header[..5].eq_ignore_ascii_case("dpop ") { 74 (auth_header[5..].trim().to_string(), true) 75 } else { 76 warn!(auth_scheme = ?auth_header.split_whitespace().next(), "getServiceAuth: invalid auth scheme"); 77 return ApiError::AuthenticationRequired.into_response(); 78 }; 79 80 let auth_user = if is_dpop { 81 match crate::oauth::verify::verify_oauth_access_token( 82 &state.db, 83 &token, 84 dpop_proof, 85 "GET", 86 &format!( 87 "/xrpc/com.atproto.server.getServiceAuth?aud={}&lxm={}", 88 params.aud, 89 params.lxm.as_deref().unwrap_or("") 90 ), 91 ) 92 .await 93 { 94 Ok(result) => crate::auth::AuthenticatedUser { 95 did: result.did, 96 is_oauth: true, 97 is_admin: false, 98 is_takendown: false, 99 scope: result.scope, 100 key_bytes: None, 101 controller_did: None, 102 }, 103 Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 104 return ( 105 StatusCode::UNAUTHORIZED, 106 [("DPoP-Nonce", nonce)], 107 Json(json!({ 108 "error": "use_dpop_nonce", 109 "message": "DPoP nonce required" 110 })), 111 ) 112 .into_response(); 113 } 114 Err(e) => { 115 warn!(error = ?e, "getServiceAuth DPoP auth validation failed"); 116 return ( 117 StatusCode::UNAUTHORIZED, 118 Json(json!({ 119 "error": "AuthenticationFailed", 120 "message": format!("{:?}", e) 121 })), 122 ) 123 .into_response(); 124 } 125 } 126 } else { 127 match crate::auth::validate_bearer_token_for_service_auth(&state.db, &token).await { 128 Ok(user) => user, 129 Err(e) => { 130 warn!(error = ?e, "getServiceAuth auth validation failed"); 131 return ApiError::from(e).into_response(); 132 } 133 } 134 }; 135 info!( 136 did = %auth_user.did, 137 is_oauth = auth_user.is_oauth, 138 has_key = auth_user.key_bytes.is_some(), 139 "getServiceAuth auth validated" 140 ); 141 let key_bytes = match &auth_user.key_bytes { 142 Some(kb) => kb.clone(), 143 None => { 144 warn!(did = %auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB"); 145 match sqlx::query_as::<_, (Vec<u8>, Option<i32>)>( 146 "SELECT k.key_bytes, k.encryption_version 147 FROM users u 148 JOIN user_keys k ON u.id = k.user_id 149 WHERE u.did = $1", 150 ) 151 .bind(&auth_user.did) 152 .fetch_optional(&state.db) 153 .await 154 { 155 Ok(Some((key_bytes_enc, encryption_version))) => { 156 match crate::config::decrypt_key(&key_bytes_enc, encryption_version) { 157 Ok(key) => key, 158 Err(e) => { 159 error!(error = ?e, "Failed to decrypt user key for service auth"); 160 return ApiError::AuthenticationFailedMsg( 161 "Failed to get signing key".into(), 162 ) 163 .into_response(); 164 } 165 } 166 } 167 Ok(None) => { 168 return ApiError::AuthenticationFailedMsg("User has no signing key".into()) 169 .into_response(); 170 } 171 Err(e) => { 172 error!(error = ?e, "DB error fetching user key"); 173 return ApiError::AuthenticationFailedMsg("Failed to get signing key".into()) 174 .into_response(); 175 } 176 } 177 } 178 }; 179 180 let lxm = params.lxm.as_deref(); 181 let lxm_for_token = lxm.unwrap_or("*"); 182 183 if let Some(method) = lxm { 184 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 185 auth_user.is_oauth, 186 auth_user.scope.as_deref(), 187 &params.aud, 188 method, 189 ) { 190 return e; 191 } 192 } else if auth_user.is_oauth { 193 let permissions = auth_user.permissions(); 194 if !permissions.has_full_access() { 195 return ( 196 StatusCode::BAD_REQUEST, 197 Json(json!({ 198 "error": "InvalidRequest", 199 "message": "OAuth tokens with granular scopes must specify an lxm parameter" 200 })), 201 ) 202 .into_response(); 203 } 204 } 205 206 let user_status = sqlx::query!( 207 "SELECT takedown_ref FROM users WHERE did = $1", 208 auth_user.did 209 ) 210 .fetch_optional(&state.db) 211 .await; 212 213 let is_takendown = match user_status { 214 Ok(Some(row)) => row.takedown_ref.is_some(), 215 _ => false, 216 }; 217 218 if is_takendown && lxm != Some("com.atproto.server.createAccount") { 219 return ( 220 StatusCode::BAD_REQUEST, 221 Json(json!({ 222 "error": "InvalidToken", 223 "message": "Bad token scope" 224 })), 225 ) 226 .into_response(); 227 } 228 229 if let Some(method) = lxm 230 && PROTECTED_METHODS.contains(&method) 231 { 232 return ( 233 StatusCode::BAD_REQUEST, 234 Json(json!({ 235 "error": "InvalidRequest", 236 "message": format!("cannot request a service auth token for the following protected method: {}", method) 237 })), 238 ) 239 .into_response(); 240 } 241 242 if let Some(exp) = params.exp { 243 let now = chrono::Utc::now().timestamp(); 244 let diff = exp - now; 245 246 if diff < 0 { 247 return ( 248 StatusCode::BAD_REQUEST, 249 Json(json!({ 250 "error": "BadExpiration", 251 "message": "expiration is in past" 252 })), 253 ) 254 .into_response(); 255 } 256 257 if diff > HOUR_SECS { 258 return ( 259 StatusCode::BAD_REQUEST, 260 Json(json!({ 261 "error": "BadExpiration", 262 "message": "cannot request a token with an expiration more than an hour in the future" 263 })), 264 ) 265 .into_response(); 266 } 267 268 if lxm.is_none() && diff > MINUTE_SECS { 269 return ( 270 StatusCode::BAD_REQUEST, 271 Json(json!({ 272 "error": "BadExpiration", 273 "message": "cannot request a method-less token with an expiration more than a minute in the future" 274 })), 275 ) 276 .into_response(); 277 } 278 } 279 280 let service_token = match crate::auth::create_service_token( 281 &auth_user.did, 282 &params.aud, 283 lxm_for_token, 284 &key_bytes, 285 ) { 286 Ok(t) => t, 287 Err(e) => { 288 error!("Failed to create service token: {:?}", e); 289 return ( 290 StatusCode::INTERNAL_SERVER_ERROR, 291 Json(json!({"error": "InternalError"})), 292 ) 293 .into_response(); 294 } 295 }; 296 ( 297 StatusCode::OK, 298 Json(GetServiceAuthOutput { 299 token: service_token, 300 }), 301 ) 302 .into_response() 303}