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