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;
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 token = match crate::auth::extract_bearer_token_from_header(
53 headers.get("Authorization").and_then(|h| h.to_str().ok()),
54 ) {
55 Some(t) => t,
56 None => return ApiError::AuthenticationRequired.into_response(),
57 };
58 let auth_user =
59 match crate::auth::validate_bearer_token_for_service_auth(&state.db, &token).await {
60 Ok(user) => user,
61 Err(e) => return ApiError::from(e).into_response(),
62 };
63 let key_bytes = match &auth_user.key_bytes {
64 Some(kb) => kb.clone(),
65 None => {
66 return ApiError::AuthenticationFailedMsg(
67 "OAuth tokens cannot create service auth".into(),
68 )
69 .into_response();
70 }
71 };
72
73 let lxm = params.lxm.as_deref();
74 let lxm_for_token = lxm.unwrap_or("*");
75
76 if let Some(method) = lxm {
77 if let Err(e) = crate::auth::scope_check::check_rpc_scope(
78 auth_user.is_oauth,
79 auth_user.scope.as_deref(),
80 ¶ms.aud,
81 method,
82 ) {
83 return e;
84 }
85 } else if auth_user.is_oauth {
86 let permissions = auth_user.permissions();
87 if !permissions.has_full_access() {
88 return (
89 StatusCode::BAD_REQUEST,
90 Json(json!({
91 "error": "InvalidRequest",
92 "message": "OAuth tokens with granular scopes must specify an lxm parameter"
93 })),
94 )
95 .into_response();
96 }
97 }
98
99 let user_status = sqlx::query!(
100 "SELECT takedown_ref FROM users WHERE did = $1",
101 auth_user.did
102 )
103 .fetch_optional(&state.db)
104 .await;
105
106 let is_takendown = match user_status {
107 Ok(Some(row)) => row.takedown_ref.is_some(),
108 _ => false,
109 };
110
111 if is_takendown && lxm != Some("com.atproto.server.createAccount") {
112 return (
113 StatusCode::BAD_REQUEST,
114 Json(json!({
115 "error": "InvalidToken",
116 "message": "Bad token scope"
117 })),
118 )
119 .into_response();
120 }
121
122 if let Some(method) = lxm
123 && PROTECTED_METHODS.contains(&method)
124 {
125 return (
126 StatusCode::BAD_REQUEST,
127 Json(json!({
128 "error": "InvalidRequest",
129 "message": format!("cannot request a service auth token for the following protected method: {}", method)
130 })),
131 )
132 .into_response();
133 }
134
135 if let Some(exp) = params.exp {
136 let now = chrono::Utc::now().timestamp();
137 let diff = exp - now;
138
139 if diff < 0 {
140 return (
141 StatusCode::BAD_REQUEST,
142 Json(json!({
143 "error": "BadExpiration",
144 "message": "expiration is in past"
145 })),
146 )
147 .into_response();
148 }
149
150 if diff > HOUR_SECS {
151 return (
152 StatusCode::BAD_REQUEST,
153 Json(json!({
154 "error": "BadExpiration",
155 "message": "cannot request a token with an expiration more than an hour in the future"
156 })),
157 )
158 .into_response();
159 }
160
161 if lxm.is_none() && diff > MINUTE_SECS {
162 return (
163 StatusCode::BAD_REQUEST,
164 Json(json!({
165 "error": "BadExpiration",
166 "message": "cannot request a method-less token with an expiration more than a minute in the future"
167 })),
168 )
169 .into_response();
170 }
171 }
172
173 let service_token = match crate::auth::create_service_token(
174 &auth_user.did,
175 ¶ms.aud,
176 lxm_for_token,
177 &key_bytes,
178 ) {
179 Ok(t) => t,
180 Err(e) => {
181 error!("Failed to create service token: {:?}", e);
182 return (
183 StatusCode::INTERNAL_SERVER_ERROR,
184 Json(json!({"error": "InternalError"})),
185 )
186 .into_response();
187 }
188 };
189 (
190 StatusCode::OK,
191 Json(GetServiceAuthOutput {
192 token: service_token,
193 }),
194 )
195 .into_response()
196}