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