-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 anyhow::anyhow;
1
2
use crate::AppState;
2
3
use crate::xrpc::helpers::TokenCheckError::InvalidToken;
3
4
use axum::body::{Body, to_bytes};
···
103
104
//Just going a head and doing uppercase here.
104
105
let slice_one = &full_code[0..5].to_ascii_uppercase();
105
106
let slice_two = &full_code[5..10].to_ascii_uppercase();
106
-
format!("{}-{}", slice_one, slice_two)
107
+
format!("{slice_one}-{slice_two}")
107
108
}
108
109
109
110
pub enum TokenCheckError {
···
151
152
let sha = hasher.finalize();
152
153
let salt = hex::encode(&sha[..16]);
153
154
let hash_hex = scrypt_hex(password, &salt)?;
154
-
Ok(format!("{}:{}", salt, hash_hex))
155
+
Ok(format!("{salt}:{hash_hex}"))
155
156
}
156
157
157
-
async fn verify_password(password: &str, password_scrypt: &str) -> Result<bool, StatusCode> {
158
+
async fn verify_password(password: &str, password_scrypt: &str) -> anyhow::Result<bool> {
158
159
// Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes)
159
160
let mut parts = password_scrypt.splitn(2, ':');
160
161
let salt = match parts.next() {
···
195
196
)
196
197
.bind(identifier)
197
198
.fetch_optional(&state.account_pool)
198
-
.await
199
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
199
+
.await?,
200
200
IdentifierType::Handle => sqlx::query_as::<_, (String, String, String, String)>(
201
201
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
202
202
FROM actor
···
205
205
)
206
206
.bind(identifier)
207
207
.fetch_optional(&state.account_pool)
208
-
.await
209
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
208
+
.await?,
210
209
IdentifierType::Did => sqlx::query_as::<_, (String, String, String, String)>(
211
210
"SELECT account.did, account.passwordScrypt, account.email, actor.handle
212
211
FROM actor
···
215
214
)
216
215
.bind(identifier)
217
216
.fetch_optional(&state.account_pool)
218
-
.await
219
-
.map_err(|_| StatusCode::BAD_REQUEST)?,
217
+
.await?,
220
218
};
221
219
222
220
if let Some((did, password_scrypt, email, handle)) = account_row {
···
226
224
)
227
225
.bind(did.clone())
228
226
.fetch_optional(&state.pds_gatekeeper_pool)
229
-
.await
230
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
227
+
.await?;
231
228
232
229
let two_factor_required = match required_opt {
233
230
Some(row) => row.0 != 0,
···
249
246
}
250
247
}
251
248
Err(err) => {
252
-
log::error!("Error checking the app password: {}", err);
253
-
Err(StatusCode::BAD_REQUEST)
249
+
log::error!("Error checking the app password: {err}");
250
+
Err(err)
254
251
}
255
252
};
256
253
}
···
266
263
.await
267
264
{
268
265
Ok(_) => {
269
-
let _ = delete_all_email_tokens(&state.account_pool, did.clone()).await;
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
270
Ok(AuthResult::ProxyThrough)
271
271
}
272
272
Err(err) => Ok(AuthResult::TokenCheckFailed(err)),
···
275
275
}
276
276
277
277
return match create_two_factor_token(&state.account_pool, did).await {
278
-
//TODO replace unwraps with the mythical ?
279
278
Ok(code) => {
280
279
let mut email_data = Map::new();
281
280
email_data.insert("token".to_string(), Value::from(code.clone()));
282
281
email_data.insert("handle".to_string(), Value::from(handle.clone()));
283
-
//TODO bad unwrap
284
282
let email_body = state
285
283
.template_engine
286
-
.render("two_factor_code.hbs", email_data)
287
-
.unwrap();
284
+
.render("two_factor_code.hbs", email_data)?;
288
285
289
286
let email = Message::builder()
290
287
//TODO prob get the proper type in the state
291
-
.from(state.mailer_from.parse().unwrap())
292
-
.to(email.parse().unwrap())
288
+
.from(state.mailer_from.parse()?)
289
+
.to(email.parse()?)
293
290
.subject("Sign in to Bluesky")
294
291
.multipart(
295
292
MultiPart::alternative() // This is composed of two parts.
296
293
.singlepart(
297
294
SinglePart::builder()
298
295
.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.
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.
300
297
)
301
298
.singlepart(
302
299
SinglePart::builder()
303
300
.header(header::ContentType::TEXT_HTML)
304
301
.body(email_body),
305
302
),
306
-
)
307
-
//TODO bad
308
-
.unwrap();
303
+
)?;
309
304
match state.mailer.send(email).await {
310
305
Ok(_) => Ok(AuthResult::TwoFactorRequired),
311
306
Err(err) => {
312
-
log::error!("Error sending the 2FA email: {}", err);
313
-
Err(StatusCode::BAD_REQUEST)
307
+
log::error!("Error sending the 2FA email: {err}");
308
+
Err(anyhow!(err))
314
309
}
315
310
}
316
311
}
317
312
Err(err) => {
318
-
log::error!("error on creating a 2fa token: {}", err);
319
-
Err(StatusCode::BAD_REQUEST)
313
+
log::error!("error on creating a 2fa token: {err}");
314
+
Err(anyhow!(err))
320
315
}
321
316
};
322
317
}
···
351
346
352
347
match res {
353
348
Ok(_) => Ok(token),
354
-
Err(e) => {
355
-
log::error!("Error creating a two factor token: {}", e);
356
-
Err(anyhow::anyhow!(e))
349
+
Err(err) => {
350
+
log::error!("Error creating a two factor token: {err}");
351
+
Err(anyhow::anyhow!(err))
357
352
}
358
353
}
359
354
}
···
383
378
.fetch_optional(account_db)
384
379
.await
385
380
.map_err(|err| {
386
-
log::error!("Error getting the 2fa token: {}", err);
381
+
log::error!("Error getting the 2fa token: {err}");
387
382
InvalidToken
388
383
})?;
389
384
+8
-4
src/middleware.rs
+8
-4
src/middleware.rs
···
1
-
use crate::xrpc::helpers::json_error_response;
1
+
use crate::helpers::json_error_response;
2
2
use axum::extract::Request;
3
3
use axum::http::{HeaderMap, StatusCode};
4
4
use axum::middleware::Next;
···
23
23
match token {
24
24
Ok(token) => {
25
25
match token {
26
-
None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "").expect("Error creating an error response"),
26
+
None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "")
27
+
.expect("Error creating an error response"),
27
28
Some(token) => {
28
29
let token = UntrustedToken::new(&token);
29
30
if token.is_err() {
···
38
39
.expect("Error creating an error response");
39
40
}
40
41
41
-
let key = Hs256Key::new(env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"));
42
+
let key = Hs256Key::new(
43
+
env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"),
44
+
);
42
45
let token: Result<Token<TokenClaims>, ValidationError> =
43
46
Hs256.validator(&key).validate(&parsed_token);
44
47
if token.is_err() {
···
55
58
}
56
59
Err(err) => {
57
60
log::error!("Error extracting token: {err}");
58
-
json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "").expect("Error creating an error response")
61
+
json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "")
62
+
.expect("Error creating an error response")
59
63
}
60
64
}
61
65
}
+10
-2
src/main.rs
+10
-2
src/main.rs
···
133
133
AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build();
134
134
//Email templates setup
135
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>();
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
+
}
138
146
139
147
let pds_base_url =
140
148
env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
+5
-6
README.md
+5
-6
README.md
···
12
12
13
13
## 2FA
14
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
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
21
17
22
18
## Captcha on Create Account
23
19
···
25
21
26
22
# Setup
27
23
24
+
We are getting close! Testing now
25
+
28
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.
29
27
But I want to run it locally on my own PDS first to test run it a bit.
30
28
···
37
35
path /xrpc/com.atproto.server.getSession
38
36
path /xrpc/com.atproto.server.updateEmail
39
37
path /xrpc/com.atproto.server.createSession
38
+
path /@atproto/oauth-provider/~api/sign-in
40
39
}
41
40
42
41
handle @gatekeeper {
-1
src/helpers.rs
-1
src/helpers.rs
···
11
11
use lettre::message::{MultiPart, SinglePart, header};
12
12
use lettre::{AsyncTransport, Message};
13
13
use rand::Rng;
14
-
use rand::distr::{Alphabetic, Alphanumeric, SampleString};
15
14
use serde::de::DeserializeOwned;
16
15
use serde_json::{Map, Value};
17
16
use sha2::{Digest, Sha256};