this repo has no description
1use axum::{
2 Form, Json,
3 extract::{Query, State},
4 http::{HeaderMap, header::SET_COOKIE},
5 response::{IntoResponse, Redirect, Response, Html},
6};
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9use subtle::ConstantTimeEq;
10use urlencoding::encode as url_encode;
11
12use crate::state::{AppState, RateLimitKind};
13use crate::oauth::{Code, DeviceAccount, DeviceData, DeviceId, OAuthError, SessionId, db, templates};
14use crate::notifications::{NotificationChannel, channel_display_name, enqueue_2fa_code};
15
16const DEVICE_COOKIE_NAME: &str = "oauth_device_id";
17
18fn extract_device_cookie(headers: &HeaderMap) -> Option<String> {
19 headers
20 .get("cookie")
21 .and_then(|v| v.to_str().ok())
22 .and_then(|cookie_str| {
23 for cookie in cookie_str.split(';') {
24 let cookie = cookie.trim();
25 if let Some(value) = cookie.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) {
26 return Some(value.to_string());
27 }
28 }
29 None
30 })
31}
32
33fn extract_client_ip(headers: &HeaderMap) -> String {
34 if let Some(forwarded) = headers.get("x-forwarded-for") {
35 if let Ok(value) = forwarded.to_str() {
36 if let Some(first_ip) = value.split(',').next() {
37 return first_ip.trim().to_string();
38 }
39 }
40 }
41
42 if let Some(real_ip) = headers.get("x-real-ip") {
43 if let Ok(value) = real_ip.to_str() {
44 return value.trim().to_string();
45 }
46 }
47
48 "0.0.0.0".to_string()
49}
50
51fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
52 headers
53 .get("user-agent")
54 .and_then(|v| v.to_str().ok())
55 .map(|s| s.to_string())
56}
57
58fn make_device_cookie(device_id: &str) -> String {
59 format!(
60 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000",
61 DEVICE_COOKIE_NAME,
62 device_id
63 )
64}
65
66#[derive(Debug, Deserialize)]
67pub struct AuthorizeQuery {
68 pub request_uri: Option<String>,
69 pub client_id: Option<String>,
70 pub new_account: Option<bool>,
71}
72
73#[derive(Debug, Serialize)]
74pub struct AuthorizeResponse {
75 pub client_id: String,
76 pub client_name: Option<String>,
77 pub scope: Option<String>,
78 pub redirect_uri: String,
79 pub state: Option<String>,
80 pub login_hint: Option<String>,
81}
82
83#[derive(Debug, Deserialize)]
84pub struct AuthorizeSubmit {
85 pub request_uri: String,
86 pub username: String,
87 pub password: String,
88 #[serde(default)]
89 pub remember_device: bool,
90}
91
92#[derive(Debug, Deserialize)]
93pub struct AuthorizeSelectSubmit {
94 pub request_uri: String,
95 pub did: String,
96}
97
98fn wants_json(headers: &HeaderMap) -> bool {
99 headers
100 .get("accept")
101 .and_then(|v| v.to_str().ok())
102 .map(|accept| accept.contains("application/json"))
103 .unwrap_or(false)
104}
105
106pub async fn authorize_get(
107 State(state): State<AppState>,
108 headers: HeaderMap,
109 Query(query): Query<AuthorizeQuery>,
110) -> Response {
111 let request_uri = match query.request_uri {
112 Some(uri) => uri,
113 None => {
114 if wants_json(&headers) {
115 return (
116 axum::http::StatusCode::BAD_REQUEST,
117 Json(serde_json::json!({
118 "error": "invalid_request",
119 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization."
120 })),
121 ).into_response();
122 }
123 return (
124 axum::http::StatusCode::BAD_REQUEST,
125 Html(templates::error_page(
126 "invalid_request",
127 Some("Missing request_uri parameter. Use PAR to initiate authorization."),
128 )),
129 ).into_response();
130 }
131 };
132
133 let request_data = match db::get_authorization_request(&state.db, &request_uri).await {
134 Ok(Some(data)) => data,
135 Ok(None) => {
136 if wants_json(&headers) {
137 return (
138 axum::http::StatusCode::BAD_REQUEST,
139 Json(serde_json::json!({
140 "error": "invalid_request",
141 "error_description": "Invalid or expired request_uri. Please start a new authorization request."
142 })),
143 ).into_response();
144 }
145 return (
146 axum::http::StatusCode::BAD_REQUEST,
147 Html(templates::error_page(
148 "invalid_request",
149 Some("Invalid or expired request_uri. Please start a new authorization request."),
150 )),
151 ).into_response();
152 }
153 Err(e) => {
154 if wants_json(&headers) {
155 return (
156 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
157 Json(serde_json::json!({
158 "error": "server_error",
159 "error_description": format!("Database error: {:?}", e)
160 })),
161 ).into_response();
162 }
163 return (
164 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
165 Html(templates::error_page(
166 "server_error",
167 Some(&format!("Database error: {:?}", e)),
168 )),
169 ).into_response();
170 }
171 };
172
173 if request_data.expires_at < Utc::now() {
174 let _ = db::delete_authorization_request(&state.db, &request_uri).await;
175 if wants_json(&headers) {
176 return (
177 axum::http::StatusCode::BAD_REQUEST,
178 Json(serde_json::json!({
179 "error": "invalid_request",
180 "error_description": "Authorization request has expired. Please start a new request."
181 })),
182 ).into_response();
183 }
184 return (
185 axum::http::StatusCode::BAD_REQUEST,
186 Html(templates::error_page(
187 "invalid_request",
188 Some("Authorization request has expired. Please start a new request."),
189 )),
190 ).into_response();
191 }
192
193 if wants_json(&headers) {
194 return Json(AuthorizeResponse {
195 client_id: request_data.parameters.client_id.clone(),
196 client_name: None,
197 scope: request_data.parameters.scope.clone(),
198 redirect_uri: request_data.parameters.redirect_uri.clone(),
199 state: request_data.parameters.state.clone(),
200 login_hint: request_data.parameters.login_hint.clone(),
201 }).into_response();
202 }
203
204 let force_new_account = query.new_account.unwrap_or(false);
205
206 if !force_new_account {
207 if let Some(device_id) = extract_device_cookie(&headers) {
208 if let Ok(accounts) = db::get_device_accounts(&state.db, &device_id).await {
209 if !accounts.is_empty() {
210 let device_accounts: Vec<DeviceAccount> = accounts
211 .into_iter()
212 .map(|row| DeviceAccount {
213 did: row.did,
214 handle: row.handle,
215 email: row.email,
216 last_used_at: row.last_used_at,
217 })
218 .collect();
219
220 return Html(templates::account_selector_page(
221 &request_data.parameters.client_id,
222 None,
223 &request_uri,
224 &device_accounts,
225 )).into_response();
226 }
227 }
228 }
229 }
230
231 Html(templates::login_page(
232 &request_data.parameters.client_id,
233 None,
234 request_data.parameters.scope.as_deref(),
235 &request_uri,
236 None,
237 request_data.parameters.login_hint.as_deref(),
238 )).into_response()
239}
240
241pub async fn authorize_get_json(
242 State(state): State<AppState>,
243 Query(query): Query<AuthorizeQuery>,
244) -> Result<Json<AuthorizeResponse>, OAuthError> {
245 let request_uri = query.request_uri.ok_or_else(|| {
246 OAuthError::InvalidRequest("request_uri is required".to_string())
247 })?;
248
249 let request_data = db::get_authorization_request(&state.db, &request_uri)
250 .await?
251 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
252
253 if request_data.expires_at < Utc::now() {
254 db::delete_authorization_request(&state.db, &request_uri).await?;
255 return Err(OAuthError::InvalidRequest("request_uri has expired".to_string()));
256 }
257
258 Ok(Json(AuthorizeResponse {
259 client_id: request_data.parameters.client_id.clone(),
260 client_name: None,
261 scope: request_data.parameters.scope.clone(),
262 redirect_uri: request_data.parameters.redirect_uri.clone(),
263 state: request_data.parameters.state.clone(),
264 login_hint: request_data.parameters.login_hint.clone(),
265 }))
266}
267
268pub async fn authorize_post(
269 State(state): State<AppState>,
270 headers: HeaderMap,
271 Form(form): Form<AuthorizeSubmit>,
272) -> Response {
273 let json_response = wants_json(&headers);
274
275 let client_ip = extract_client_ip(&headers);
276 if !state.check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip).await {
277 tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded");
278 if json_response {
279 return (
280 axum::http::StatusCode::TOO_MANY_REQUESTS,
281 Json(serde_json::json!({
282 "error": "RateLimitExceeded",
283 "error_description": "Too many login attempts. Please try again later."
284 })),
285 ).into_response();
286 }
287 return (
288 axum::http::StatusCode::TOO_MANY_REQUESTS,
289 Html(templates::error_page(
290 "RateLimitExceeded",
291 Some("Too many login attempts. Please try again later."),
292 )),
293 ).into_response();
294 }
295
296 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
297 Ok(Some(data)) => data,
298 Ok(None) => {
299 if json_response {
300 return (
301 axum::http::StatusCode::BAD_REQUEST,
302 Json(serde_json::json!({
303 "error": "invalid_request",
304 "error_description": "Invalid or expired request_uri."
305 })),
306 ).into_response();
307 }
308 return Html(templates::error_page(
309 "invalid_request",
310 Some("Invalid or expired request_uri. Please start a new authorization request."),
311 )).into_response();
312 }
313 Err(e) => {
314 if json_response {
315 return (
316 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
317 Json(serde_json::json!({
318 "error": "server_error",
319 "error_description": format!("Database error: {:?}", e)
320 })),
321 ).into_response();
322 }
323 return Html(templates::error_page(
324 "server_error",
325 Some(&format!("Database error: {:?}", e)),
326 )).into_response();
327 }
328 };
329
330 if request_data.expires_at < Utc::now() {
331 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
332 if json_response {
333 return (
334 axum::http::StatusCode::BAD_REQUEST,
335 Json(serde_json::json!({
336 "error": "invalid_request",
337 "error_description": "Authorization request has expired."
338 })),
339 ).into_response();
340 }
341 return Html(templates::error_page(
342 "invalid_request",
343 Some("Authorization request has expired. Please start a new request."),
344 )).into_response();
345 }
346
347 let show_login_error = |error_msg: &str, json: bool| -> Response {
348 if json {
349 return (
350 axum::http::StatusCode::FORBIDDEN,
351 Json(serde_json::json!({
352 "error": "access_denied",
353 "error_description": error_msg
354 })),
355 ).into_response();
356 }
357 Html(templates::login_page(
358 &request_data.parameters.client_id,
359 None,
360 request_data.parameters.scope.as_deref(),
361 &form.request_uri,
362 Some(error_msg),
363 Some(&form.username),
364 )).into_response()
365 };
366
367 let user = match sqlx::query!(
368 r#"
369 SELECT id, did, email, password_hash, two_factor_enabled,
370 preferred_notification_channel as "preferred_notification_channel: NotificationChannel",
371 deactivated_at, takedown_ref
372 FROM users
373 WHERE handle = $1 OR email = $1
374 "#,
375 form.username
376 )
377 .fetch_optional(&state.db)
378 .await
379 {
380 Ok(Some(u)) => u,
381 Ok(None) => {
382 let _ = bcrypt::verify(&form.password, "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK");
383 return show_login_error("Invalid handle/email or password.", json_response);
384 }
385 Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
386 };
387
388 if user.deactivated_at.is_some() {
389 return show_login_error("This account has been deactivated.", json_response);
390 }
391
392 if user.takedown_ref.is_some() {
393 return show_login_error("This account has been taken down.", json_response);
394 }
395
396 let password_valid = match bcrypt::verify(&form.password, &user.password_hash) {
397 Ok(valid) => valid,
398 Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
399 };
400
401 if !password_valid {
402 return show_login_error("Invalid handle/email or password.", json_response);
403 }
404
405 if user.two_factor_enabled {
406 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
407
408 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await {
409 Ok(challenge) => {
410 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
411 if let Err(e) = enqueue_2fa_code(
412 &state.db,
413 user.id,
414 &challenge.code,
415 &hostname,
416 ).await {
417 tracing::warn!(
418 did = %user.did,
419 error = %e,
420 "Failed to enqueue 2FA notification"
421 );
422 }
423
424 let channel_name = channel_display_name(user.preferred_notification_channel);
425 let redirect_url = format!(
426 "/oauth/authorize/2fa?request_uri={}&channel={}",
427 url_encode(&form.request_uri),
428 url_encode(channel_name)
429 );
430 return Redirect::temporary(&redirect_url).into_response();
431 }
432 Err(_) => {
433 return show_login_error("An error occurred. Please try again.", json_response);
434 }
435 }
436 }
437
438 let code = Code::generate();
439 let mut device_id: Option<String> = extract_device_cookie(&headers);
440 let mut new_cookie: Option<String> = None;
441
442 if form.remember_device {
443 let final_device_id = if let Some(existing_id) = &device_id {
444 existing_id.clone()
445 } else {
446 let new_id = DeviceId::generate();
447 let device_data = DeviceData {
448 session_id: SessionId::generate().0,
449 user_agent: extract_user_agent(&headers),
450 ip_address: extract_client_ip(&headers),
451 last_seen_at: Utc::now(),
452 };
453
454 if db::create_device(&state.db, &new_id.0, &device_data).await.is_ok() {
455 new_cookie = Some(make_device_cookie(&new_id.0));
456 device_id = Some(new_id.0.clone());
457 }
458 new_id.0
459 };
460
461 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await;
462 }
463
464 if let Err(_) = db::update_authorization_request(
465 &state.db,
466 &form.request_uri,
467 &user.did,
468 device_id.as_deref(),
469 &code.0,
470 )
471 .await
472 {
473 return show_login_error("An error occurred. Please try again.", json_response);
474 }
475
476 let redirect_url = build_success_redirect(
477 &request_data.parameters.redirect_uri,
478 &code.0,
479 request_data.parameters.state.as_deref(),
480 );
481
482 let redirect = Redirect::temporary(&redirect_url);
483
484 if let Some(cookie) = new_cookie {
485 ([(SET_COOKIE, cookie)], redirect).into_response()
486 } else {
487 redirect.into_response()
488 }
489}
490
491pub async fn authorize_select(
492 State(state): State<AppState>,
493 headers: HeaderMap,
494 Form(form): Form<AuthorizeSelectSubmit>,
495) -> Response {
496 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
497 Ok(Some(data)) => data,
498 Ok(None) => {
499 return Html(templates::error_page(
500 "invalid_request",
501 Some("Invalid or expired request_uri. Please start a new authorization request."),
502 )).into_response();
503 }
504 Err(_) => {
505 return Html(templates::error_page(
506 "server_error",
507 Some("An error occurred. Please try again."),
508 )).into_response();
509 }
510 };
511
512 if request_data.expires_at < Utc::now() {
513 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
514 return Html(templates::error_page(
515 "invalid_request",
516 Some("Authorization request has expired. Please start a new request."),
517 )).into_response();
518 }
519
520 let device_id = match extract_device_cookie(&headers) {
521 Some(id) => id,
522 None => {
523 return Html(templates::error_page(
524 "invalid_request",
525 Some("No device session found. Please sign in."),
526 )).into_response();
527 }
528 };
529
530 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await {
531 Ok(valid) => valid,
532 Err(_) => {
533 return Html(templates::error_page(
534 "server_error",
535 Some("An error occurred. Please try again."),
536 )).into_response();
537 }
538 };
539
540 if !account_valid {
541 return Html(templates::error_page(
542 "access_denied",
543 Some("This account is not available on this device. Please sign in."),
544 )).into_response();
545 }
546
547 let user = match sqlx::query!(
548 r#"
549 SELECT id, two_factor_enabled,
550 preferred_notification_channel as "preferred_notification_channel: NotificationChannel"
551 FROM users
552 WHERE did = $1
553 "#,
554 form.did
555 )
556 .fetch_optional(&state.db)
557 .await
558 {
559 Ok(Some(u)) => u,
560 Ok(None) => {
561 return Html(templates::error_page(
562 "access_denied",
563 Some("Account not found. Please sign in."),
564 )).into_response();
565 }
566 Err(_) => {
567 return Html(templates::error_page(
568 "server_error",
569 Some("An error occurred. Please try again."),
570 )).into_response();
571 }
572 };
573
574 if user.two_factor_enabled {
575 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
576
577 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await {
578 Ok(challenge) => {
579 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
580 if let Err(e) = enqueue_2fa_code(
581 &state.db,
582 user.id,
583 &challenge.code,
584 &hostname,
585 ).await {
586 tracing::warn!(
587 did = %form.did,
588 error = %e,
589 "Failed to enqueue 2FA notification"
590 );
591 }
592
593 let channel_name = channel_display_name(user.preferred_notification_channel);
594 let redirect_url = format!(
595 "/oauth/authorize/2fa?request_uri={}&channel={}",
596 url_encode(&form.request_uri),
597 url_encode(channel_name)
598 );
599 return Redirect::temporary(&redirect_url).into_response();
600 }
601 Err(_) => {
602 return Html(templates::error_page(
603 "server_error",
604 Some("An error occurred. Please try again."),
605 )).into_response();
606 }
607 }
608 }
609
610 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await;
611
612 let code = Code::generate();
613
614 if let Err(_) = db::update_authorization_request(
615 &state.db,
616 &form.request_uri,
617 &form.did,
618 Some(&device_id),
619 &code.0,
620 )
621 .await
622 {
623 return Html(templates::error_page(
624 "server_error",
625 Some("An error occurred. Please try again."),
626 )).into_response();
627 }
628
629 let redirect_url = build_success_redirect(
630 &request_data.parameters.redirect_uri,
631 &code.0,
632 request_data.parameters.state.as_deref(),
633 );
634
635 Redirect::temporary(&redirect_url).into_response()
636}
637
638fn build_success_redirect(redirect_uri: &str, code: &str, state: Option<&str>) -> String {
639 let mut redirect_url = redirect_uri.to_string();
640
641 let separator = if redirect_url.contains('?') { '&' } else { '?' };
642 redirect_url.push(separator);
643 redirect_url.push_str(&format!("code={}", url_encode(code)));
644
645 if let Some(req_state) = state {
646 redirect_url.push_str(&format!("&state={}", url_encode(req_state)));
647 }
648
649 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
650 redirect_url.push_str(&format!("&iss={}", url_encode(&format!("https://{}", pds_hostname))));
651
652 redirect_url
653}
654
655#[derive(Debug, Serialize)]
656pub struct AuthorizeDenyResponse {
657 pub error: String,
658 pub error_description: String,
659}
660
661pub async fn authorize_deny(
662 State(state): State<AppState>,
663 Form(form): Form<AuthorizeDenyForm>,
664) -> Result<Response, OAuthError> {
665 let request_data = db::get_authorization_request(&state.db, &form.request_uri)
666 .await?
667 .ok_or_else(|| OAuthError::InvalidRequest("Invalid request_uri".to_string()))?;
668
669 db::delete_authorization_request(&state.db, &form.request_uri).await?;
670
671 let redirect_uri = &request_data.parameters.redirect_uri;
672 let mut redirect_url = redirect_uri.to_string();
673
674 let separator = if redirect_url.contains('?') { '&' } else { '?' };
675 redirect_url.push(separator);
676 redirect_url.push_str("error=access_denied");
677 redirect_url.push_str("&error_description=User%20denied%20the%20request");
678
679 if let Some(state) = &request_data.parameters.state {
680 redirect_url.push_str(&format!("&state={}", url_encode(state)));
681 }
682
683 Ok(Redirect::temporary(&redirect_url).into_response())
684}
685
686#[derive(Debug, Deserialize)]
687pub struct AuthorizeDenyForm {
688 pub request_uri: String,
689}
690
691#[derive(Debug, Deserialize)]
692pub struct Authorize2faQuery {
693 pub request_uri: String,
694 pub channel: Option<String>,
695}
696
697#[derive(Debug, Deserialize)]
698pub struct Authorize2faSubmit {
699 pub request_uri: String,
700 pub code: String,
701}
702
703const MAX_2FA_ATTEMPTS: i32 = 5;
704
705pub async fn authorize_2fa_get(
706 State(state): State<AppState>,
707 Query(query): Query<Authorize2faQuery>,
708) -> Response {
709 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await {
710 Ok(Some(c)) => c,
711 Ok(None) => {
712 return Html(templates::error_page(
713 "invalid_request",
714 Some("No 2FA challenge found. Please start over."),
715 )).into_response();
716 }
717 Err(_) => {
718 return Html(templates::error_page(
719 "server_error",
720 Some("An error occurred. Please try again."),
721 )).into_response();
722 }
723 };
724
725 if challenge.expires_at < Utc::now() {
726 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
727 return Html(templates::error_page(
728 "invalid_request",
729 Some("2FA code has expired. Please start over."),
730 )).into_response();
731 }
732
733 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
734 Ok(Some(d)) => d,
735 Ok(None) => {
736 return Html(templates::error_page(
737 "invalid_request",
738 Some("Authorization request not found. Please start over."),
739 )).into_response();
740 }
741 Err(_) => {
742 return Html(templates::error_page(
743 "server_error",
744 Some("An error occurred. Please try again."),
745 )).into_response();
746 }
747 };
748
749 let channel = query.channel.as_deref().unwrap_or("email");
750
751 Html(templates::two_factor_page(
752 &query.request_uri,
753 channel,
754 None,
755 )).into_response()
756}
757
758pub async fn authorize_2fa_post(
759 State(state): State<AppState>,
760 headers: HeaderMap,
761 Form(form): Form<Authorize2faSubmit>,
762) -> Response {
763 let client_ip = extract_client_ip(&headers);
764 if !state.check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip).await {
765 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded");
766 return (
767 axum::http::StatusCode::TOO_MANY_REQUESTS,
768 Html(templates::error_page(
769 "RateLimitExceeded",
770 Some("Too many attempts. Please try again later."),
771 )),
772 ).into_response();
773 }
774
775 let challenge = match db::get_2fa_challenge(&state.db, &form.request_uri).await {
776 Ok(Some(c)) => c,
777 Ok(None) => {
778 return Html(templates::error_page(
779 "invalid_request",
780 Some("No 2FA challenge found. Please start over."),
781 )).into_response();
782 }
783 Err(_) => {
784 return Html(templates::error_page(
785 "server_error",
786 Some("An error occurred. Please try again."),
787 )).into_response();
788 }
789 };
790
791 if challenge.expires_at < Utc::now() {
792 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
793 return Html(templates::error_page(
794 "invalid_request",
795 Some("2FA code has expired. Please start over."),
796 )).into_response();
797 }
798
799 if challenge.attempts >= MAX_2FA_ATTEMPTS {
800 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
801 return Html(templates::error_page(
802 "access_denied",
803 Some("Too many failed attempts. Please start over."),
804 )).into_response();
805 }
806
807 let code_valid: bool = form.code.trim().as_bytes().ct_eq(challenge.code.as_bytes()).into();
808
809 if !code_valid {
810 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await;
811
812 let channel = match sqlx::query_scalar!(
813 r#"SELECT preferred_notification_channel as "channel: NotificationChannel" FROM users WHERE did = $1"#,
814 challenge.did
815 )
816 .fetch_optional(&state.db)
817 .await
818 {
819 Ok(Some(ch)) => channel_display_name(ch).to_string(),
820 Ok(None) | Err(_) => "email".to_string(),
821 };
822
823 let _request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
824 Ok(Some(d)) => d,
825 Ok(None) => {
826 return Html(templates::error_page(
827 "invalid_request",
828 Some("Authorization request not found. Please start over."),
829 )).into_response();
830 }
831 Err(_) => {
832 return Html(templates::error_page(
833 "server_error",
834 Some("An error occurred. Please try again."),
835 )).into_response();
836 }
837 };
838
839 return Html(templates::two_factor_page(
840 &form.request_uri,
841 &channel,
842 Some("Invalid verification code. Please try again."),
843 )).into_response();
844 }
845
846 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
847
848 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
849 Ok(Some(d)) => d,
850 Ok(None) => {
851 return Html(templates::error_page(
852 "invalid_request",
853 Some("Authorization request not found."),
854 )).into_response();
855 }
856 Err(_) => {
857 return Html(templates::error_page(
858 "server_error",
859 Some("An error occurred."),
860 )).into_response();
861 }
862 };
863
864 let code = Code::generate();
865 let device_id = extract_device_cookie(&headers);
866
867 if let Err(_) = db::update_authorization_request(
868 &state.db,
869 &form.request_uri,
870 &challenge.did,
871 device_id.as_deref(),
872 &code.0,
873 )
874 .await
875 {
876 return Html(templates::error_page(
877 "server_error",
878 Some("An error occurred. Please try again."),
879 )).into_response();
880 }
881
882 let redirect_url = build_success_redirect(
883 &request_data.parameters.redirect_uri,
884 &code.0,
885 request_data.parameters.state.as_deref(),
886 );
887
888 Redirect::temporary(&redirect_url).into_response()
889}