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