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 && auth_header[..7].eq_ignore_ascii_case("bearer ") { 70 (auth_header[7..].trim().to_string(), false) 71 } else if auth_header.len() >= 5 && auth_header[..5].eq_ignore_ascii_case("dpop ") { 72 (auth_header[5..].trim().to_string(), true) 73 } else { 74 warn!(auth_scheme = ?auth_header.split_whitespace().next(), "getServiceAuth: invalid auth scheme"); 75 return ApiError::AuthenticationRequired.into_response(); 76 }; 77 78 let auth_user = if is_dpop { 79 match crate::oauth::verify::verify_oauth_access_token( 80 &state.db, 81 &token, 82 dpop_proof, 83 "GET", 84 &format!("/xrpc/com.atproto.server.getServiceAuth?aud={}&lxm={}", 85 params.aud, 86 params.lxm.as_deref().unwrap_or("")), 87 ).await { 88 Ok(result) => crate::auth::AuthenticatedUser { 89 did: result.did, 90 is_oauth: true, 91 is_admin: false, 92 scope: result.scope, 93 key_bytes: None, 94 }, 95 Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 96 return ( 97 StatusCode::UNAUTHORIZED, 98 [("DPoP-Nonce", nonce)], 99 Json(json!({ 100 "error": "use_dpop_nonce", 101 "message": "DPoP nonce required" 102 })), 103 ).into_response(); 104 } 105 Err(e) => { 106 warn!(error = ?e, "getServiceAuth DPoP auth validation failed"); 107 return ( 108 StatusCode::UNAUTHORIZED, 109 Json(json!({ 110 "error": "AuthenticationFailed", 111 "message": format!("{:?}", e) 112 })), 113 ).into_response(); 114 } 115 } 116 } else { 117 match crate::auth::validate_bearer_token_for_service_auth(&state.db, &token).await { 118 Ok(user) => user, 119 Err(e) => { 120 warn!(error = ?e, "getServiceAuth auth validation failed"); 121 return ApiError::from(e).into_response(); 122 } 123 } 124 }; 125 info!( 126 did = %auth_user.did, 127 is_oauth = auth_user.is_oauth, 128 has_key = auth_user.key_bytes.is_some(), 129 "getServiceAuth auth validated" 130 ); 131 let key_bytes = match &auth_user.key_bytes { 132 Some(kb) => kb.clone(), 133 None => { 134 warn!(did = %auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB"); 135 match sqlx::query_as::<_, (Vec<u8>, Option<i32>)>( 136 "SELECT k.key_bytes, k.encryption_version 137 FROM users u 138 JOIN user_keys k ON u.id = k.user_id 139 WHERE u.did = $1" 140 ) 141 .bind(&auth_user.did) 142 .fetch_optional(&state.db) 143 .await 144 { 145 Ok(Some((key_bytes_enc, encryption_version))) => { 146 match crate::config::decrypt_key(&key_bytes_enc, encryption_version) { 147 Ok(key) => key, 148 Err(e) => { 149 error!(error = ?e, "Failed to decrypt user key for service auth"); 150 return ApiError::AuthenticationFailedMsg( 151 "Failed to get signing key".into(), 152 ) 153 .into_response(); 154 } 155 } 156 } 157 Ok(None) => { 158 return ApiError::AuthenticationFailedMsg( 159 "User has no signing key".into(), 160 ) 161 .into_response(); 162 } 163 Err(e) => { 164 error!(error = ?e, "DB error fetching user key"); 165 return ApiError::AuthenticationFailedMsg( 166 "Failed to get signing key".into(), 167 ) 168 .into_response(); 169 } 170 } 171 } 172 }; 173 174 let lxm = params.lxm.as_deref(); 175 let lxm_for_token = lxm.unwrap_or("*"); 176 177 if let Some(method) = lxm { 178 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 179 auth_user.is_oauth, 180 auth_user.scope.as_deref(), 181 &params.aud, 182 method, 183 ) { 184 return e; 185 } 186 } else if auth_user.is_oauth { 187 let permissions = auth_user.permissions(); 188 if !permissions.has_full_access() { 189 return ( 190 StatusCode::BAD_REQUEST, 191 Json(json!({ 192 "error": "InvalidRequest", 193 "message": "OAuth tokens with granular scopes must specify an lxm parameter" 194 })), 195 ) 196 .into_response(); 197 } 198 } 199 200 let user_status = sqlx::query!( 201 "SELECT takedown_ref FROM users WHERE did = $1", 202 auth_user.did 203 ) 204 .fetch_optional(&state.db) 205 .await; 206 207 let is_takendown = match user_status { 208 Ok(Some(row)) => row.takedown_ref.is_some(), 209 _ => false, 210 }; 211 212 if is_takendown && lxm != Some("com.atproto.server.createAccount") { 213 return ( 214 StatusCode::BAD_REQUEST, 215 Json(json!({ 216 "error": "InvalidToken", 217 "message": "Bad token scope" 218 })), 219 ) 220 .into_response(); 221 } 222 223 if let Some(method) = lxm 224 && PROTECTED_METHODS.contains(&method) 225 { 226 return ( 227 StatusCode::BAD_REQUEST, 228 Json(json!({ 229 "error": "InvalidRequest", 230 "message": format!("cannot request a service auth token for the following protected method: {}", method) 231 })), 232 ) 233 .into_response(); 234 } 235 236 if let Some(exp) = params.exp { 237 let now = chrono::Utc::now().timestamp(); 238 let diff = exp - now; 239 240 if diff < 0 { 241 return ( 242 StatusCode::BAD_REQUEST, 243 Json(json!({ 244 "error": "BadExpiration", 245 "message": "expiration is in past" 246 })), 247 ) 248 .into_response(); 249 } 250 251 if diff > HOUR_SECS { 252 return ( 253 StatusCode::BAD_REQUEST, 254 Json(json!({ 255 "error": "BadExpiration", 256 "message": "cannot request a token with an expiration more than an hour in the future" 257 })), 258 ) 259 .into_response(); 260 } 261 262 if lxm.is_none() && diff > MINUTE_SECS { 263 return ( 264 StatusCode::BAD_REQUEST, 265 Json(json!({ 266 "error": "BadExpiration", 267 "message": "cannot request a method-less token with an expiration more than a minute in the future" 268 })), 269 ) 270 .into_response(); 271 } 272 } 273 274 let service_token = match crate::auth::create_service_token( 275 &auth_user.did, 276 &params.aud, 277 lxm_for_token, 278 &key_bytes, 279 ) { 280 Ok(t) => t, 281 Err(e) => { 282 error!("Failed to create service token: {:?}", e); 283 return ( 284 StatusCode::INTERNAL_SERVER_ERROR, 285 Json(json!({"error": "InternalError"})), 286 ) 287 .into_response(); 288 } 289 }; 290 ( 291 StatusCode::OK, 292 Json(GetServiceAuthOutput { 293 token: service_token, 294 }), 295 ) 296 .into_response() 297}