this repo has no description
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 ¶ms.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 ¶ms.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}