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