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