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