+7
Cargo.lock
+7
Cargo.lock
···
287
287
dependencies = [
288
288
"android-tzdata",
289
289
"iana-time-zone",
290
+
"js-sys",
290
291
"num-traits",
292
+
"wasm-bindgen",
291
293
"windows-link",
292
294
]
293
295
···
1655
1657
"anyhow",
1656
1658
"axum",
1657
1659
"axum-template",
1660
+
"chrono",
1658
1661
"dotenvy",
1659
1662
"handlebars",
1660
1663
"hex",
···
2395
2398
dependencies = [
2396
2399
"base64",
2397
2400
"bytes",
2401
+
"chrono",
2398
2402
"crc",
2399
2403
"crossbeam-queue",
2400
2404
"either",
···
2472
2476
"bitflags",
2473
2477
"byteorder",
2474
2478
"bytes",
2479
+
"chrono",
2475
2480
"crc",
2476
2481
"digest",
2477
2482
"dotenvy",
···
2513
2518
"base64",
2514
2519
"bitflags",
2515
2520
"byteorder",
2521
+
"chrono",
2516
2522
"crc",
2517
2523
"dotenvy",
2518
2524
"etcetera",
···
2547
2553
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
2548
2554
dependencies = [
2549
2555
"atoi",
2556
+
"chrono",
2550
2557
"flume",
2551
2558
"futures-channel",
2552
2559
"futures-core",
+2
-1
Cargo.toml
+2
-1
Cargo.toml
···
6
6
[dependencies]
7
7
axum = { version = "0.8.4", features = ["macros", "json"] }
8
8
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "signal"] }
9
-
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "migrate"] }
9
+
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "migrate", "chrono"] }
10
10
dotenvy = "0.15.7"
11
11
serde = { version = "1.0", features = ["derive"] }
12
12
serde_json = "1.0"
···
24
24
axum-template = { version = "3.0.0", features = ["handlebars"] }
25
25
rand = "0.9.2"
26
26
anyhow = "1.0.99"
27
+
chrono = "0.4.41"
+17
-166
src/xrpc/com_atproto_server.rs
+17
-166
src/xrpc/com_atproto_server.rs
···
1
1
use crate::AppState;
2
2
use crate::middleware::Did;
3
-
use crate::xrpc::helpers::{ProxiedResult, json_error_response, proxy_get_json};
3
+
use crate::xrpc::helpers::{
4
+
AuthResult, ProxiedResult, TokenCheckError, json_error_response, preauth_check, proxy_get_json,
5
+
};
4
6
use axum::body::Body;
5
7
use axum::extract::State;
6
8
use axum::http::{HeaderMap, StatusCode};
···
58
60
pub struct CreateSessionRequest {
59
61
identifier: String,
60
62
password: String,
61
-
auth_factor_token: String,
62
-
allow_takendown: bool,
63
-
}
64
-
65
-
pub enum AuthResult {
66
-
WrongIdentityOrPassword,
67
-
TwoFactorRequired,
68
-
TwoFactorFailed,
69
-
/// User does not have 2FA enabled, or passes it
70
-
ProxyThrough,
71
-
}
72
-
73
-
pub enum IdentifierType {
74
-
Email,
75
-
DID,
76
-
Handle,
77
-
}
78
-
79
-
impl IdentifierType {
80
-
fn what_is_it(identifier: String) -> Self {
81
-
if identifier.contains("@") {
82
-
IdentifierType::Email
83
-
} else if identifier.contains("did:") {
84
-
IdentifierType::DID
85
-
} else {
86
-
IdentifierType::Handle
87
-
}
88
-
}
89
-
}
90
-
91
-
async fn verify_password(password: &str, password_scrypt: &str) -> Result<bool, StatusCode> {
92
-
// Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes)
93
-
let mut parts = password_scrypt.splitn(2, ':');
94
-
let salt = match parts.next() {
95
-
Some(s) if !s.is_empty() => s,
96
-
_ => return Ok(false),
97
-
};
98
-
let stored_hash_hex = match parts.next() {
99
-
Some(h) if !h.is_empty() => h,
100
-
_ => return Ok(false),
101
-
};
102
-
103
-
//Sets up scrypt to mimic node's scrypt
104
-
let params = match scrypt::Params::new(14, 8, 1, 64) {
105
-
Ok(p) => p,
106
-
Err(_) => return Ok(false),
107
-
};
108
-
let mut derived = [0u8; 64];
109
-
if scrypt::scrypt(password.as_bytes(), salt.as_bytes(), ¶ms, &mut derived).is_err() {
110
-
return Ok(false);
111
-
}
112
-
113
-
let stored_bytes = match hex::decode(stored_hash_hex) {
114
-
Ok(b) => b,
115
-
Err(e) => {
116
-
log::error!("Error decoding stored hash: {}", e);
117
-
return Ok(false);
118
-
}
119
-
};
120
-
121
-
Ok(derived.as_slice() == stored_bytes.as_slice())
122
-
}
123
-
124
-
async fn preauth_check(
125
-
state: &AppState,
126
-
identifier: &str,
127
-
password: &str,
128
-
) -> Result<AuthResult, StatusCode> {
129
-
// Determine identifier type
130
-
let id_type = IdentifierType::what_is_it(identifier.to_string());
131
-
132
-
// Query account DB for did and passwordScrypt based on identifier type
133
-
let account_row: Option<(String, String, String)> = match id_type {
134
-
IdentifierType::Email => sqlx::query_as::<_, (String, String, String)>(
135
-
"SELECT did, passwordScrypt, account.email FROM account WHERE email = ? LIMIT 1",
136
-
)
137
-
.bind(identifier)
138
-
.fetch_optional(&state.account_pool)
139
-
.await
140
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
141
-
IdentifierType::Handle => sqlx::query_as::<_, (String, String, String)>(
142
-
"SELECT account.did, account.passwordScrypt, account.email
143
-
FROM actor
144
-
LEFT JOIN account ON actor.did = account.did
145
-
where actor.handle =? LIMIT 1",
146
-
)
147
-
.bind(identifier)
148
-
.fetch_optional(&state.account_pool)
149
-
.await
150
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
151
-
IdentifierType::DID => sqlx::query_as::<_, (String, String, String)>(
152
-
"SELECT did, passwordScrypt, account.email FROM account WHERE did = ? LIMIT 1",
153
-
)
154
-
.bind(identifier)
155
-
.fetch_optional(&state.account_pool)
156
-
.await
157
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
158
-
};
159
-
160
-
if let Some((did, password_scrypt, email)) = account_row {
161
-
// Check two-factor requirement for this DID in the gatekeeper DB
162
-
let required_opt = sqlx::query_as::<_, (u8,)>(
163
-
"SELECT required FROM two_factor_accounts WHERE did = ? LIMIT 1",
164
-
)
165
-
.bind(&did)
166
-
.fetch_optional(&state.pds_gatekeeper_pool)
167
-
.await
168
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
169
-
170
-
let two_factor_required = match required_opt {
171
-
Some(row) => row.0 != 0,
172
-
None => false,
173
-
};
174
-
175
-
if two_factor_required {
176
-
// Verify password before proceeding to 2FA email step
177
-
let verified = verify_password(password, &password_scrypt).await?;
178
-
if !verified {
179
-
return Ok(AuthResult::WrongIdentityOrPassword);
180
-
}
181
-
let mut email_data = Map::new();
182
-
//TODO these need real values
183
-
let token = "test".to_string();
184
-
let handle = "baileytownsend.dev".to_string();
185
-
email_data.insert("token".to_string(), Value::from(token.clone()));
186
-
email_data.insert("handle".to_string(), Value::from(handle.clone()));
187
-
//TODO bad unwrap
188
-
let email_body = state
189
-
.template_engine
190
-
.render("two_factor_code.hbs", email_data)
191
-
.unwrap();
192
-
193
-
let email = Message::builder()
194
-
//TODO prob get the proper type in the state
195
-
.from(state.mailer_from.parse().unwrap())
196
-
.to(email.parse().unwrap())
197
-
.subject("Sign in to Bluesky")
198
-
.multipart(
199
-
MultiPart::alternative() // This is composed of two parts.
200
-
.singlepart(
201
-
SinglePart::builder()
202
-
.header(header::ContentType::TEXT_PLAIN)
203
-
.body(format!("We received a sign-in request for the account @{}. Use the code: {} to sign in. If this wasn't you, we recommend taking steps to protect your account by changing your password at https://bsky.app/settings.", handle, token)), // Every message should have a plain text fallback.
204
-
)
205
-
.singlepart(
206
-
SinglePart::builder()
207
-
.header(header::ContentType::TEXT_HTML)
208
-
.body(email_body),
209
-
),
210
-
)
211
-
//TODO bad
212
-
.unwrap();
213
-
return match state.mailer.send(email).await {
214
-
Ok(_) => Ok(AuthResult::TwoFactorRequired),
215
-
Err(err) => {
216
-
log::error!("Error sending the 2FA email: {}", err);
217
-
Err(StatusCode::BAD_REQUEST)
218
-
}
219
-
};
220
-
}
221
-
}
222
-
223
-
// No local 2FA requirement (or account not found)
224
-
Ok(AuthResult::ProxyThrough)
63
+
auth_factor_token: Option<String>,
64
+
allow_takendown: Option<bool>,
225
65
}
226
66
227
67
pub async fn create_session(
···
231
71
) -> Result<Response<Body>, StatusCode> {
232
72
let identifier = payload.identifier.clone();
233
73
let password = payload.password.clone();
74
+
let auth_factor_token = payload.auth_factor_token.clone();
234
75
235
76
// Run the shared pre-auth logic to validate and check 2FA requirement
236
-
match preauth_check(&state, &identifier, &password).await? {
77
+
match preauth_check(&state, &identifier, &password, auth_factor_token).await? {
237
78
AuthResult::WrongIdentityOrPassword => json_error_response(
238
79
StatusCode::UNAUTHORIZED,
239
80
"AuthenticationRequired",
···
278
119
279
120
Ok(proxied)
280
121
}
122
+
AuthResult::TokenCheckFailed(err) => match err {
123
+
TokenCheckError::InvalidToken => json_error_response(
124
+
StatusCode::BAD_REQUEST,
125
+
"InvalidToken",
126
+
"Hey this token is invalid and this is a custom message to show it's bot a normal PDS",
127
+
),
128
+
TokenCheckError::ExpiredToken => {
129
+
json_error_response(StatusCode::BAD_REQUEST, "ExpiredToken", "Token is expired")
130
+
}
131
+
},
281
132
}
282
133
}
283
134
+293
-3
src/xrpc/helpers.rs
+293
-3
src/xrpc/helpers.rs
···
1
1
use crate::AppState;
2
+
use crate::xrpc::helpers::TokenCheckError::InvalidToken;
2
3
use axum::body::{Body, to_bytes};
3
4
use axum::extract::Request;
4
5
use axum::http::header::CONTENT_TYPE;
5
6
use axum::http::{HeaderMap, Method, StatusCode, Uri};
6
7
use axum::response::{IntoResponse, Response};
8
+
use axum_template::TemplateEngine;
9
+
use chrono::Utc;
10
+
use lettre::message::{MultiPart, SinglePart, header};
11
+
use lettre::{AsyncTransport, Message};
7
12
use rand::distr::{Alphanumeric, SampleString};
8
13
use serde::de::DeserializeOwned;
14
+
use serde_json::{Map, Value};
9
15
use sqlx::SqlitePool;
10
-
use sqlx::sqlite::SqliteError;
11
-
use tracing::error;
16
+
use tracing::{error, log};
12
17
13
18
/// The result of a proxied call that attempts to parse JSON.
14
19
pub enum ProxiedResult<T> {
···
159
164
format!("{}-{}", slice_one, slice_two)
160
165
}
161
166
162
-
pub fn create_two_factor_token(account_db: &SqlitePool, did: String) -> anyhow::Result<String> {}
167
+
pub enum TokenCheckError {
168
+
InvalidToken,
169
+
ExpiredToken,
170
+
}
171
+
pub enum AuthResult {
172
+
WrongIdentityOrPassword,
173
+
TwoFactorRequired,
174
+
TwoFactorFailed,
175
+
/// User does not have 2FA enabled, or passes it
176
+
ProxyThrough,
177
+
TokenCheckFailed(TokenCheckError),
178
+
}
179
+
180
+
pub enum IdentifierType {
181
+
Email,
182
+
DID,
183
+
Handle,
184
+
}
185
+
186
+
impl IdentifierType {
187
+
fn what_is_it(identifier: String) -> Self {
188
+
if identifier.contains("@") {
189
+
IdentifierType::Email
190
+
} else if identifier.contains("did:") {
191
+
IdentifierType::DID
192
+
} else {
193
+
IdentifierType::Handle
194
+
}
195
+
}
196
+
}
197
+
198
+
async fn verify_password(password: &str, password_scrypt: &str) -> Result<bool, StatusCode> {
199
+
// Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes)
200
+
let mut parts = password_scrypt.splitn(2, ':');
201
+
let salt = match parts.next() {
202
+
Some(s) if !s.is_empty() => s,
203
+
_ => return Ok(false),
204
+
};
205
+
let stored_hash_hex = match parts.next() {
206
+
Some(h) if !h.is_empty() => h,
207
+
_ => return Ok(false),
208
+
};
209
+
210
+
//Sets up scrypt to mimic node's scrypt
211
+
let params = match scrypt::Params::new(14, 8, 1, 64) {
212
+
Ok(p) => p,
213
+
Err(_) => return Ok(false),
214
+
};
215
+
let mut derived = [0u8; 64];
216
+
if scrypt::scrypt(password.as_bytes(), salt.as_bytes(), ¶ms, &mut derived).is_err() {
217
+
return Ok(false);
218
+
}
219
+
220
+
let stored_bytes = match hex::decode(stored_hash_hex) {
221
+
Ok(b) => b,
222
+
Err(e) => {
223
+
log::error!("Error decoding stored hash: {}", e);
224
+
return Ok(false);
225
+
}
226
+
};
227
+
228
+
Ok(derived.as_slice() == stored_bytes.as_slice())
229
+
}
230
+
231
+
pub async fn preauth_check(
232
+
state: &AppState,
233
+
identifier: &str,
234
+
password: &str,
235
+
two_factor_code: Option<String>,
236
+
) -> Result<AuthResult, StatusCode> {
237
+
// Determine identifier type
238
+
let id_type = IdentifierType::what_is_it(identifier.to_string());
239
+
240
+
// Query account DB for did and passwordScrypt based on identifier type
241
+
let account_row: Option<(String, String, String, String)> = match id_type {
242
+
IdentifierType::Email => sqlx::query_as::<_, (String, String, String, String)>(
243
+
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
244
+
FROM actor
245
+
LEFT JOIN account ON actor.did = account.did
246
+
where account.email = ? LIMIT 1",
247
+
)
248
+
.bind(identifier)
249
+
.fetch_optional(&state.account_pool)
250
+
.await
251
+
.map_err(|_| StatusCode::BAD_REQUEST)?,
252
+
IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>(
253
+
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
254
+
FROM actor
255
+
LEFT JOIN account ON actor.did = account.did
256
+
where actor.handle = ? LIMIT 1",
257
+
)
258
+
.bind(identifier)
259
+
.fetch_optional(&state.account_pool)
260
+
.await
261
+
.map_err(|_| StatusCode::BAD_REQUEST)?,
262
+
IdentifierType::DID => sqlx::query_as::<_, (String, String, String, String)>(
263
+
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
264
+
FROM actor
265
+
LEFT JOIN account ON actor.did = account.did
266
+
where account.did = ? LIMIT 1",
267
+
)
268
+
.bind(identifier)
269
+
.fetch_optional(&state.account_pool)
270
+
.await
271
+
.map_err(|_| StatusCode::BAD_REQUEST)?,
272
+
};
273
+
274
+
if let Some((did, password_scrypt, email, handle)) = account_row {
275
+
// Check two-factor requirement for this DID in the gatekeeper DB
276
+
let required_opt = sqlx::query_as::<_, (u8,)>(
277
+
"SELECT required FROM two_factor_accounts WHERE did = ? LIMIT 1",
278
+
)
279
+
.bind(&did)
280
+
.fetch_optional(&state.pds_gatekeeper_pool)
281
+
.await
282
+
.map_err(|_| StatusCode::BAD_REQUEST)?;
283
+
284
+
let two_factor_required = match required_opt {
285
+
Some(row) => row.0 != 0,
286
+
None => false,
287
+
};
288
+
289
+
if two_factor_required {
290
+
// Verify password before proceeding to 2FA email step
291
+
let verified = verify_password(password, &password_scrypt).await?;
292
+
if !verified {
293
+
return Ok(AuthResult::WrongIdentityOrPassword);
294
+
}
295
+
//Two factor is required and a taken was provided
296
+
if let Some(two_factor_code) = two_factor_code {
297
+
//Seems it sends over a empty on login without it set? As in no input is shown on the ui for first login try
298
+
if two_factor_code != "" {
299
+
return match assert_valid_token(
300
+
&state.account_pool,
301
+
did.clone(),
302
+
two_factor_code,
303
+
)
304
+
.await
305
+
{
306
+
Ok(_) => {
307
+
let _ = delete_all_email_tokens(&state.account_pool, did.clone()).await;
308
+
Ok(AuthResult::ProxyThrough)
309
+
}
310
+
Err(err) => Ok(AuthResult::TokenCheckFailed(err)),
311
+
};
312
+
}
313
+
}
314
+
315
+
return match create_two_factor_token(&state.account_pool, did).await {
316
+
Ok(code) => {
317
+
let mut email_data = Map::new();
318
+
email_data.insert("token".to_string(), Value::from(code.clone()));
319
+
email_data.insert("handle".to_string(), Value::from(handle.clone()));
320
+
//TODO bad unwrap
321
+
let email_body = state
322
+
.template_engine
323
+
.render("two_factor_code.hbs", email_data)
324
+
.unwrap();
325
+
326
+
let email = Message::builder()
327
+
//TODO prob get the proper type in the state
328
+
.from(state.mailer_from.parse().unwrap())
329
+
.to(email.parse().unwrap())
330
+
.subject("Sign in to Bluesky")
331
+
.multipart(
332
+
MultiPart::alternative() // This is composed of two parts.
333
+
.singlepart(
334
+
SinglePart::builder()
335
+
.header(header::ContentType::TEXT_PLAIN)
336
+
.body(format!("We received a sign-in request for the account @{}. Use the code: {} to sign in. If this wasn't you, we recommend taking steps to protect your account by changing your password at https://bsky.app/settings.", handle, code)), // Every message should have a plain text fallback.
337
+
)
338
+
.singlepart(
339
+
SinglePart::builder()
340
+
.header(header::ContentType::TEXT_HTML)
341
+
.body(email_body),
342
+
),
343
+
)
344
+
//TODO bad
345
+
.unwrap();
346
+
match state.mailer.send(email).await {
347
+
Ok(_) => Ok(AuthResult::TwoFactorRequired),
348
+
Err(err) => {
349
+
log::error!("Error sending the 2FA email: {}", err);
350
+
Err(StatusCode::BAD_REQUEST)
351
+
}
352
+
}
353
+
}
354
+
Err(err) => {
355
+
log::error!("error on creating a 2fa token: {}", err);
356
+
Err(StatusCode::BAD_REQUEST)
357
+
}
358
+
};
359
+
}
360
+
}
361
+
362
+
// No local 2FA requirement (or account not found)
363
+
Ok(AuthResult::ProxyThrough)
364
+
}
365
+
366
+
pub async fn create_two_factor_token(
367
+
account_db: &SqlitePool,
368
+
did: String,
369
+
) -> anyhow::Result<String> {
370
+
let purpose = "2fa_code";
371
+
372
+
loop {
373
+
let token = get_random_token();
374
+
let right_now = Utc::now();
375
+
let query = "INSERT INTO email_token (purpose, did, token, requestedAt)
376
+
VALUES (?, ?, ?, ?)
377
+
ON CONFLICT(purpose, did) DO UPDATE SET
378
+
token=excluded.token,
379
+
requestedAt=excluded.requestedAt";
380
+
381
+
let res = sqlx::query(query)
382
+
.bind(purpose)
383
+
.bind(&did)
384
+
.bind(&token)
385
+
.bind(right_now)
386
+
.execute(account_db)
387
+
.await;
388
+
389
+
return match res {
390
+
Ok(_) => Ok(token),
391
+
Err(e) => {
392
+
log::error!("Error creating a two factor token: {}", e);
393
+
Err(anyhow::anyhow!(e))
394
+
}
395
+
};
396
+
}
397
+
}
398
+
399
+
pub async fn delete_all_email_tokens(account_db: &SqlitePool, did: String) -> anyhow::Result<()> {
400
+
sqlx::query("DELETE FROM email_token WHERE did = ?")
401
+
.bind(did)
402
+
.execute(account_db)
403
+
.await?;
404
+
Ok(())
405
+
}
406
+
407
+
pub async fn assert_valid_token(
408
+
account_db: &SqlitePool,
409
+
did: String,
410
+
token: String,
411
+
) -> Result<(), TokenCheckError> {
412
+
let token_upper = token.to_ascii_uppercase();
413
+
let purpose = "2fa_code";
414
+
415
+
let row: Option<(String,)> = sqlx::query_as(
416
+
"SELECT requestedAt FROM email_token WHERE purpose = ? AND did = ? AND token = ? LIMIT 1",
417
+
)
418
+
.bind(purpose)
419
+
.bind(did)
420
+
.bind(token_upper)
421
+
.fetch_optional(account_db)
422
+
.await
423
+
.map_err(|err| {
424
+
log::error!("Error getting the 2fa token: {}", err);
425
+
TokenCheckError::InvalidToken
426
+
})?;
427
+
428
+
match row {
429
+
None => Err(TokenCheckError::InvalidToken),
430
+
Some(row) => {
431
+
// Token lives for 15 minutes
432
+
let expiration_ms = 15 * 60_000;
433
+
434
+
// Parse requestedAt; assume RFC3339-like string (as created_by PDS or by our code)
435
+
let requested_at_utc = match chrono::DateTime::parse_from_rfc3339(&row.0) {
436
+
Ok(dt) => dt.with_timezone(&Utc),
437
+
Err(_) => {
438
+
return Err(TokenCheckError::InvalidToken);
439
+
}
440
+
};
441
+
442
+
let now = Utc::now();
443
+
let age_ms = (now - requested_at_utc).num_milliseconds();
444
+
let expired = age_ms > expiration_ms;
445
+
if expired {
446
+
return Err(TokenCheckError::ExpiredToken);
447
+
}
448
+
449
+
Ok(())
450
+
}
451
+
}
452
+
}