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 ¶ms.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 ¶ms.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}