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