-3
migrations_bells_and_whistles/.keep
-3
migrations_bells_and_whistles/.keep
+1
Cargo.lock
+1
Cargo.lock
+1
Cargo.toml
+1
Cargo.toml
+27
-32
src/xrpc/helpers.rs
+27
-32
src/xrpc/helpers.rs
···
1
use crate::AppState;
2
use crate::xrpc::helpers::TokenCheckError::InvalidToken;
3
use axum::body::{Body, to_bytes};
···
103
//Just going a head and doing uppercase here.
104
let slice_one = &full_code[0..5].to_ascii_uppercase();
105
let slice_two = &full_code[5..10].to_ascii_uppercase();
106
-
format!("{}-{}", slice_one, slice_two)
107
}
108
109
pub enum TokenCheckError {
···
151
let sha = hasher.finalize();
152
let salt = hex::encode(&sha[..16]);
153
let hash_hex = scrypt_hex(password, &salt)?;
154
-
Ok(format!("{}:{}", salt, hash_hex))
155
}
156
157
-
async fn verify_password(password: &str, password_scrypt: &str) -> Result<bool, StatusCode> {
158
// Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes)
159
let mut parts = password_scrypt.splitn(2, ':');
160
let salt = match parts.next() {
···
195
)
196
.bind(identifier)
197
.fetch_optional(&state.account_pool)
198
-
.await
199
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
200
IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>(
201
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
202
FROM actor
···
205
)
206
.bind(identifier)
207
.fetch_optional(&state.account_pool)
208
-
.await
209
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
210
IdentifierType::Did => sqlx::query_as::<_, (String, String, String, String)>(
211
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
212
FROM actor
···
215
)
216
.bind(identifier)
217
.fetch_optional(&state.account_pool)
218
-
.await
219
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
220
};
221
222
if let Some((did, password_scrypt, email, handle)) = account_row {
···
226
)
227
.bind(did.clone())
228
.fetch_optional(&state.pds_gatekeeper_pool)
229
-
.await
230
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
231
232
let two_factor_required = match required_opt {
233
Some(row) => row.0 != 0,
···
249
}
250
}
251
Err(err) => {
252
-
log::error!("Error checking the app password: {}", err);
253
-
Err(StatusCode::BAD_REQUEST)
254
}
255
};
256
}
···
266
.await
267
{
268
Ok(_) => {
269
-
let _ = delete_all_email_tokens(&state.account_pool, did.clone()).await;
270
Ok(AuthResult::ProxyThrough)
271
}
272
Err(err) => Ok(AuthResult::TokenCheckFailed(err)),
···
275
}
276
277
return match create_two_factor_token(&state.account_pool, did).await {
278
-
//TODO replace unwraps with the mythical ?
279
Ok(code) => {
280
let mut email_data = Map::new();
281
email_data.insert("token".to_string(), Value::from(code.clone()));
282
email_data.insert("handle".to_string(), Value::from(handle.clone()));
283
-
//TODO bad unwrap
284
let email_body = state
285
.template_engine
286
-
.render("two_factor_code.hbs", email_data)
287
-
.unwrap();
288
289
let email = Message::builder()
290
//TODO prob get the proper type in the state
291
-
.from(state.mailer_from.parse().unwrap())
292
-
.to(email.parse().unwrap())
293
.subject("Sign in to Bluesky")
294
.multipart(
295
MultiPart::alternative() // This is composed of two parts.
296
.singlepart(
297
SinglePart::builder()
298
.header(header::ContentType::TEXT_PLAIN)
299
-
.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.
300
)
301
.singlepart(
302
SinglePart::builder()
303
.header(header::ContentType::TEXT_HTML)
304
.body(email_body),
305
),
306
-
)
307
-
//TODO bad
308
-
.unwrap();
309
match state.mailer.send(email).await {
310
Ok(_) => Ok(AuthResult::TwoFactorRequired),
311
Err(err) => {
312
-
log::error!("Error sending the 2FA email: {}", err);
313
-
Err(StatusCode::BAD_REQUEST)
314
}
315
}
316
}
317
Err(err) => {
318
-
log::error!("error on creating a 2fa token: {}", err);
319
-
Err(StatusCode::BAD_REQUEST)
320
}
321
};
322
}
···
351
352
match res {
353
Ok(_) => Ok(token),
354
-
Err(e) => {
355
-
log::error!("Error creating a two factor token: {}", e);
356
-
Err(anyhow::anyhow!(e))
357
}
358
}
359
}
···
383
.fetch_optional(account_db)
384
.await
385
.map_err(|err| {
386
-
log::error!("Error getting the 2fa token: {}", err);
387
InvalidToken
388
})?;
389
···
1
+
use anyhow::anyhow;
2
use crate::AppState;
3
use crate::xrpc::helpers::TokenCheckError::InvalidToken;
4
use axum::body::{Body, to_bytes};
···
104
//Just going a head and doing uppercase here.
105
let slice_one = &full_code[0..5].to_ascii_uppercase();
106
let slice_two = &full_code[5..10].to_ascii_uppercase();
107
+
format!("{slice_one}-{slice_two}")
108
}
109
110
pub enum TokenCheckError {
···
152
let sha = hasher.finalize();
153
let salt = hex::encode(&sha[..16]);
154
let hash_hex = scrypt_hex(password, &salt)?;
155
+
Ok(format!("{salt}:{hash_hex}"))
156
}
157
158
+
async fn verify_password(password: &str, password_scrypt: &str) -> anyhow::Result<bool> {
159
// Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes)
160
let mut parts = password_scrypt.splitn(2, ':');
161
let salt = match parts.next() {
···
196
)
197
.bind(identifier)
198
.fetch_optional(&state.account_pool)
199
+
.await?,
200
IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>(
201
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
202
FROM actor
···
205
)
206
.bind(identifier)
207
.fetch_optional(&state.account_pool)
208
+
.await?,
209
IdentifierType::Did => sqlx::query_as::<_, (String, String, String, String)>(
210
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
211
FROM actor
···
214
)
215
.bind(identifier)
216
.fetch_optional(&state.account_pool)
217
+
.await?,
218
};
219
220
if let Some((did, password_scrypt, email, handle)) = account_row {
···
224
)
225
.bind(did.clone())
226
.fetch_optional(&state.pds_gatekeeper_pool)
227
+
.await?;
228
229
let two_factor_required = match required_opt {
230
Some(row) => row.0 != 0,
···
246
}
247
}
248
Err(err) => {
249
+
log::error!("Error checking the app password: {err}");
250
+
Err(err)
251
}
252
};
253
}
···
263
.await
264
{
265
Ok(_) => {
266
+
let result_of_cleanup = delete_all_email_tokens(&state.account_pool, did.clone()).await;
267
+
if result_of_cleanup.is_err(){
268
+
log::error!("There was an error deleting the email tokens after login: {:?}", result_of_cleanup.err())
269
+
}
270
Ok(AuthResult::ProxyThrough)
271
}
272
Err(err) => Ok(AuthResult::TokenCheckFailed(err)),
···
275
}
276
277
return match create_two_factor_token(&state.account_pool, did).await {
278
Ok(code) => {
279
let mut email_data = Map::new();
280
email_data.insert("token".to_string(), Value::from(code.clone()));
281
email_data.insert("handle".to_string(), Value::from(handle.clone()));
282
let email_body = state
283
.template_engine
284
+
.render("two_factor_code.hbs", email_data)?;
285
286
let email = Message::builder()
287
//TODO prob get the proper type in the state
288
+
.from(state.mailer_from.parse()?)
289
+
.to(email.parse()?)
290
.subject("Sign in to Bluesky")
291
.multipart(
292
MultiPart::alternative() // This is composed of two parts.
293
.singlepart(
294
SinglePart::builder()
295
.header(header::ContentType::TEXT_PLAIN)
296
+
.body(format!("We received a sign-in request for the account @{handle}. Use the code: {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.")), // Every message should have a plain text fallback.
297
)
298
.singlepart(
299
SinglePart::builder()
300
.header(header::ContentType::TEXT_HTML)
301
.body(email_body),
302
),
303
+
)?;
304
match state.mailer.send(email).await {
305
Ok(_) => Ok(AuthResult::TwoFactorRequired),
306
Err(err) => {
307
+
log::error!("Error sending the 2FA email: {err}");
308
+
Err(anyhow!(err))
309
}
310
}
311
}
312
Err(err) => {
313
+
log::error!("error on creating a 2fa token: {err}");
314
+
Err(anyhow!(err))
315
}
316
};
317
}
···
346
347
match res {
348
Ok(_) => Ok(token),
349
+
Err(err) => {
350
+
log::error!("Error creating a two factor token: {err}");
351
+
Err(anyhow::anyhow!(err))
352
}
353
}
354
}
···
378
.fetch_optional(account_db)
379
.await
380
.map_err(|err| {
381
+
log::error!("Error getting the 2fa token: {err}");
382
InvalidToken
383
})?;
384
+8
-4
src/middleware.rs
+8
-4
src/middleware.rs
···
1
-
use crate::xrpc::helpers::json_error_response;
2
use axum::extract::Request;
3
use axum::http::{HeaderMap, StatusCode};
4
use axum::middleware::Next;
···
23
match token {
24
Ok(token) => {
25
match token {
26
-
None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "").expect("Error creating an error response"),
27
Some(token) => {
28
let token = UntrustedToken::new(&token);
29
if token.is_err() {
···
38
.expect("Error creating an error response");
39
}
40
41
-
let key = Hs256Key::new(env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"));
42
let token: Result<Token<TokenClaims>, ValidationError> =
43
Hs256.validator(&key).validate(&parsed_token);
44
if token.is_err() {
···
55
}
56
Err(err) => {
57
log::error!("Error extracting token: {err}");
58
-
json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "").expect("Error creating an error response")
59
}
60
}
61
}
···
1
+
use crate::helpers::json_error_response;
2
use axum::extract::Request;
3
use axum::http::{HeaderMap, StatusCode};
4
use axum::middleware::Next;
···
23
match token {
24
Ok(token) => {
25
match token {
26
+
None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "")
27
+
.expect("Error creating an error response"),
28
Some(token) => {
29
let token = UntrustedToken::new(&token);
30
if token.is_err() {
···
39
.expect("Error creating an error response");
40
}
41
42
+
let key = Hs256Key::new(
43
+
env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"),
44
+
);
45
let token: Result<Token<TokenClaims>, ValidationError> =
46
Hs256.validator(&key).validate(&parsed_token);
47
if token.is_err() {
···
58
}
59
Err(err) => {
60
log::error!("Error extracting token: {err}");
61
+
json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "")
62
+
.expect("Error creating an error response")
63
}
64
}
65
}
-1
src/xrpc/mod.rs
-1
src/xrpc/mod.rs
+10
-2
src/main.rs
+10
-2
src/main.rs
···
133
AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build();
134
//Email templates setup
135
let mut hbs = Handlebars::new();
136
-
//TODO add an override to manually load in the hbs templates
137
-
let _ = hbs.register_embed_templates::<EmailTemplates>();
138
139
let pds_base_url =
140
env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
···
133
AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build();
134
//Email templates setup
135
let mut hbs = Handlebars::new();
136
+
137
+
let users_email_directory = env::var("GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY");
138
+
if let Ok(users_email_directory) = users_email_directory {
139
+
hbs.register_template_file(
140
+
"two_factor_code.hbs",
141
+
format!("{users_email_directory}/two_factor_code.hbs"),
142
+
)?;
143
+
} else {
144
+
let _ = hbs.register_embed_templates::<EmailTemplates>();
145
+
}
146
147
let pds_base_url =
148
env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
+5
-6
README.md
+5
-6
README.md
···
12
13
## 2FA
14
15
-
- [x] Ability to turn on/off 2FA
16
-
- [x] getSession overwrite to set the `emailAuthFactor` flag if the user has 2FA turned on
17
-
- [x] send an email using the `PDS_EMAIL_SMTP_URL` with a handlebar email template like Bluesky's 2FA sign in email.
18
-
- [ ] generate a 2FA code
19
-
- [ ] createSession gatekeeping (It does stop logins, just eh, doesn't actually send a real code or check it yet)
20
-
- [ ] oauth endpoint gatekeeping
21
22
## Captcha on Create Account
23
···
25
26
# Setup
27
28
Nothing here yet! If you are brave enough to try before full release, let me know and I'll help you set it up.
29
But I want to run it locally on my own PDS first to test run it a bit.
30
···
37
path /xrpc/com.atproto.server.getSession
38
path /xrpc/com.atproto.server.updateEmail
39
path /xrpc/com.atproto.server.createSession
40
}
41
42
handle @gatekeeper {
···
12
13
## 2FA
14
15
+
- Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins
16
+
- Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA
17
18
## Captcha on Create Account
19
···
21
22
# Setup
23
24
+
We are getting close! Testing now
25
+
26
Nothing here yet! If you are brave enough to try before full release, let me know and I'll help you set it up.
27
But I want to run it locally on my own PDS first to test run it a bit.
28
···
35
path /xrpc/com.atproto.server.getSession
36
path /xrpc/com.atproto.server.updateEmail
37
path /xrpc/com.atproto.server.createSession
38
+
path /@atproto/oauth-provider/~api/sign-in
39
}
40
41
handle @gatekeeper {
-1
src/helpers.rs
-1
src/helpers.rs