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