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