this repo has no description
1use crate::comms::{CommsChannel, channel_display_name, enqueue_2fa_code};
2use crate::oauth::{
3 Code, DeviceData, DeviceId, OAuthError, SessionId, client::ClientMetadataCache, db,
4};
5use crate::state::{AppState, RateLimitKind};
6use axum::{
7 Json,
8 extract::{Query, State},
9 http::{
10 HeaderMap, StatusCode,
11 header::{LOCATION, SET_COOKIE},
12 },
13 response::{IntoResponse, 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 redirect_to_frontend_error(error: &str, description: &str) -> Response {
27 redirect_see_other(&format!(
28 "/#/oauth/error?error={}&error_description={}",
29 url_encode(error),
30 url_encode(description)
31 ))
32}
33
34fn extract_device_cookie(headers: &HeaderMap) -> Option<String> {
35 headers
36 .get("cookie")
37 .and_then(|v| v.to_str().ok())
38 .and_then(|cookie_str| {
39 for cookie in cookie_str.split(';') {
40 let cookie = cookie.trim();
41 if let Some(value) = cookie.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) {
42 return Some(value.to_string());
43 }
44 }
45 None
46 })
47}
48
49fn extract_client_ip(headers: &HeaderMap) -> String {
50 if let Some(forwarded) = headers.get("x-forwarded-for")
51 && let Ok(value) = forwarded.to_str()
52 && let Some(first_ip) = value.split(',').next()
53 {
54 return first_ip.trim().to_string();
55 }
56 if let Some(real_ip) = headers.get("x-real-ip")
57 && let Ok(value) = real_ip.to_str()
58 {
59 return value.trim().to_string();
60 }
61 "0.0.0.0".to_string()
62}
63
64fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
65 headers
66 .get("user-agent")
67 .and_then(|v| v.to_str().ok())
68 .map(|s| s.to_string())
69}
70
71fn make_device_cookie(device_id: &str) -> String {
72 format!(
73 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000",
74 DEVICE_COOKIE_NAME, device_id
75 )
76}
77
78#[derive(Debug, Deserialize)]
79pub struct AuthorizeQuery {
80 pub request_uri: Option<String>,
81 pub client_id: Option<String>,
82 pub new_account: Option<bool>,
83}
84
85#[derive(Debug, Serialize)]
86pub struct AuthorizeResponse {
87 pub client_id: String,
88 pub client_name: Option<String>,
89 pub scope: Option<String>,
90 pub redirect_uri: String,
91 pub state: Option<String>,
92 pub login_hint: Option<String>,
93}
94
95#[derive(Debug, Deserialize)]
96pub struct AuthorizeSubmit {
97 pub request_uri: String,
98 pub username: String,
99 pub password: String,
100 #[serde(default)]
101 pub remember_device: bool,
102}
103
104#[derive(Debug, Deserialize)]
105pub struct AuthorizeSelectSubmit {
106 pub request_uri: String,
107 pub did: String,
108}
109
110fn wants_json(headers: &HeaderMap) -> bool {
111 headers
112 .get("accept")
113 .and_then(|v| v.to_str().ok())
114 .map(|accept| accept.contains("application/json"))
115 .unwrap_or(false)
116}
117
118pub async fn authorize_get(
119 State(state): State<AppState>,
120 headers: HeaderMap,
121 Query(query): Query<AuthorizeQuery>,
122) -> Response {
123 let request_uri = match query.request_uri {
124 Some(uri) => uri,
125 None => {
126 if wants_json(&headers) {
127 return (
128 StatusCode::BAD_REQUEST,
129 Json(serde_json::json!({
130 "error": "invalid_request",
131 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization."
132 })),
133 ).into_response();
134 }
135 return redirect_to_frontend_error(
136 "invalid_request",
137 "Missing request_uri parameter. Use PAR to initiate authorization.",
138 );
139 }
140 };
141 let request_data = match db::get_authorization_request(&state.db, &request_uri).await {
142 Ok(Some(data)) => data,
143 Ok(None) => {
144 if wants_json(&headers) {
145 return (
146 StatusCode::BAD_REQUEST,
147 Json(serde_json::json!({
148 "error": "invalid_request",
149 "error_description": "Invalid or expired request_uri. Please start a new authorization request."
150 })),
151 ).into_response();
152 }
153 return redirect_to_frontend_error(
154 "invalid_request",
155 "Invalid or expired request_uri. Please start a new authorization request.",
156 );
157 }
158 Err(e) => {
159 if wants_json(&headers) {
160 return (
161 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 redirect_to_frontend_error("server_error", "A database error occurred.");
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 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 redirect_to_frontend_error(
184 "invalid_request",
185 "Authorization request has expired. Please start a new request.",
186 );
187 }
188 let client_cache = ClientMetadataCache::new(3600);
189 let client_name = client_cache
190 .get(&request_data.parameters.client_id)
191 .await
192 .ok()
193 .and_then(|m| m.client_name);
194 if wants_json(&headers) {
195 return Json(AuthorizeResponse {
196 client_id: request_data.parameters.client_id.clone(),
197 client_name: client_name.clone(),
198 scope: request_data.parameters.scope.clone(),
199 redirect_uri: request_data.parameters.redirect_uri.clone(),
200 state: request_data.parameters.state.clone(),
201 login_hint: request_data.parameters.login_hint.clone(),
202 })
203 .into_response();
204 }
205 let force_new_account = query.new_account.unwrap_or(false);
206 if !force_new_account
207 && let Some(device_id) = extract_device_cookie(&headers)
208 && let Ok(accounts) = db::get_device_accounts(&state.db, &device_id).await
209 && !accounts.is_empty()
210 {
211 return redirect_see_other(&format!(
212 "/#/oauth/accounts?request_uri={}",
213 url_encode(&request_uri)
214 ));
215 }
216 redirect_see_other(&format!(
217 "/#/oauth/login?request_uri={}",
218 url_encode(&request_uri)
219 ))
220}
221
222pub async fn authorize_get_json(
223 State(state): State<AppState>,
224 Query(query): Query<AuthorizeQuery>,
225) -> Result<Json<AuthorizeResponse>, OAuthError> {
226 let request_uri = query
227 .request_uri
228 .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?;
229 let request_data = db::get_authorization_request(&state.db, &request_uri)
230 .await?
231 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
232 if request_data.expires_at < Utc::now() {
233 db::delete_authorization_request(&state.db, &request_uri).await?;
234 return Err(OAuthError::InvalidRequest(
235 "request_uri has expired".to_string(),
236 ));
237 }
238 Ok(Json(AuthorizeResponse {
239 client_id: request_data.parameters.client_id.clone(),
240 client_name: None,
241 scope: request_data.parameters.scope.clone(),
242 redirect_uri: request_data.parameters.redirect_uri.clone(),
243 state: request_data.parameters.state.clone(),
244 login_hint: request_data.parameters.login_hint.clone(),
245 }))
246}
247
248#[derive(Debug, Serialize)]
249pub struct AccountInfo {
250 pub did: String,
251 pub handle: String,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub email: Option<String>,
254}
255
256#[derive(Debug, Serialize)]
257pub struct AccountsResponse {
258 pub accounts: Vec<AccountInfo>,
259 pub request_uri: String,
260}
261
262fn mask_email(email: &str) -> String {
263 if let Some(at_pos) = email.find('@') {
264 let local = &email[..at_pos];
265 let domain = &email[at_pos..];
266 if local.len() <= 2 {
267 format!("{}***{}", local.chars().next().unwrap_or('*'), domain)
268 } else {
269 let first = local.chars().next().unwrap_or('*');
270 let last = local.chars().last().unwrap_or('*');
271 format!("{}***{}{}", first, last, domain)
272 }
273 } else {
274 "***".to_string()
275 }
276}
277
278pub async fn authorize_accounts(
279 State(state): State<AppState>,
280 headers: HeaderMap,
281 Query(query): Query<AuthorizeQuery>,
282) -> Response {
283 let request_uri = match query.request_uri {
284 Some(uri) => uri,
285 None => {
286 return (
287 StatusCode::BAD_REQUEST,
288 Json(serde_json::json!({
289 "error": "invalid_request",
290 "error_description": "Missing request_uri parameter"
291 })),
292 )
293 .into_response();
294 }
295 };
296 let device_id = match extract_device_cookie(&headers) {
297 Some(id) => id,
298 None => {
299 return Json(AccountsResponse {
300 accounts: vec![],
301 request_uri,
302 })
303 .into_response();
304 }
305 };
306 let accounts = match db::get_device_accounts(&state.db, &device_id).await {
307 Ok(accts) => accts,
308 Err(_) => {
309 return Json(AccountsResponse {
310 accounts: vec![],
311 request_uri,
312 })
313 .into_response();
314 }
315 };
316 let account_infos: Vec<AccountInfo> = accounts
317 .into_iter()
318 .map(|row| AccountInfo {
319 did: row.did,
320 handle: row.handle,
321 email: row.email.map(|e| mask_email(&e)),
322 })
323 .collect();
324 Json(AccountsResponse {
325 accounts: account_infos,
326 request_uri,
327 })
328 .into_response()
329}
330
331pub async fn authorize_post(
332 State(state): State<AppState>,
333 headers: HeaderMap,
334 Json(form): Json<AuthorizeSubmit>,
335) -> Response {
336 let json_response = wants_json(&headers);
337 let client_ip = extract_client_ip(&headers);
338 if !state
339 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
340 .await
341 {
342 tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded");
343 if json_response {
344 return (
345 axum::http::StatusCode::TOO_MANY_REQUESTS,
346 Json(serde_json::json!({
347 "error": "RateLimitExceeded",
348 "error_description": "Too many login attempts. Please try again later."
349 })),
350 )
351 .into_response();
352 }
353 return redirect_to_frontend_error(
354 "RateLimitExceeded",
355 "Too many login attempts. Please try again later.",
356 );
357 }
358 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
359 Ok(Some(data)) => data,
360 Ok(None) => {
361 if json_response {
362 return (
363 axum::http::StatusCode::BAD_REQUEST,
364 Json(serde_json::json!({
365 "error": "invalid_request",
366 "error_description": "Invalid or expired request_uri."
367 })),
368 )
369 .into_response();
370 }
371 return redirect_to_frontend_error(
372 "invalid_request",
373 "Invalid or expired request_uri. Please start a new authorization request.",
374 );
375 }
376 Err(e) => {
377 if json_response {
378 return (
379 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
380 Json(serde_json::json!({
381 "error": "server_error",
382 "error_description": format!("Database error: {:?}", e)
383 })),
384 )
385 .into_response();
386 }
387 return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e));
388 }
389 };
390 if request_data.expires_at < Utc::now() {
391 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
392 if json_response {
393 return (
394 axum::http::StatusCode::BAD_REQUEST,
395 Json(serde_json::json!({
396 "error": "invalid_request",
397 "error_description": "Authorization request has expired."
398 })),
399 )
400 .into_response();
401 }
402 return redirect_to_frontend_error(
403 "invalid_request",
404 "Authorization request has expired. Please start a new request.",
405 );
406 }
407 let show_login_error = |error_msg: &str, json: bool| -> Response {
408 if json {
409 return (
410 axum::http::StatusCode::FORBIDDEN,
411 Json(serde_json::json!({
412 "error": "access_denied",
413 "error_description": error_msg
414 })),
415 )
416 .into_response();
417 }
418 redirect_see_other(&format!(
419 "/#/oauth/login?request_uri={}&error={}",
420 url_encode(&form.request_uri),
421 url_encode(error_msg)
422 ))
423 };
424 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
425 let normalized_username = form.username.trim();
426 let normalized_username = normalized_username
427 .strip_prefix('@')
428 .unwrap_or(normalized_username);
429 let normalized_username = if normalized_username.contains('@') {
430 normalized_username.to_string()
431 } else if !normalized_username.contains('.') {
432 format!("{}.{}", normalized_username, pds_hostname)
433 } else {
434 normalized_username.to_string()
435 };
436 tracing::debug!(
437 original_username = %form.username,
438 normalized_username = %normalized_username,
439 pds_hostname = %pds_hostname,
440 "Normalized username for lookup"
441 );
442 let user = match sqlx::query!(
443 r#"
444 SELECT id, did, email, password_hash, password_required, two_factor_enabled,
445 preferred_comms_channel as "preferred_comms_channel: CommsChannel",
446 deactivated_at, takedown_ref,
447 email_verified, discord_verified, telegram_verified, signal_verified
448 FROM users
449 WHERE handle = $1 OR email = $1
450 "#,
451 normalized_username
452 )
453 .fetch_optional(&state.db)
454 .await
455 {
456 Ok(Some(u)) => u,
457 Ok(None) => {
458 let _ = bcrypt::verify(
459 &form.password,
460 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK",
461 );
462 return show_login_error("Invalid handle/email or password.", json_response);
463 }
464 Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
465 };
466 if user.deactivated_at.is_some() {
467 return show_login_error("This account has been deactivated.", json_response);
468 }
469 if user.takedown_ref.is_some() {
470 return show_login_error("This account has been taken down.", json_response);
471 }
472 let is_verified = user.email_verified
473 || user.discord_verified
474 || user.telegram_verified
475 || user.signal_verified;
476 if !is_verified {
477 return show_login_error(
478 "Please verify your account before logging in.",
479 json_response,
480 );
481 }
482
483 if !user.password_required {
484 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
485 .await
486 .is_err()
487 {
488 return show_login_error("An error occurred. Please try again.", json_response);
489 }
490 let redirect_url = format!(
491 "/#/oauth/passkey?request_uri={}",
492 url_encode(&form.request_uri)
493 );
494 if json_response {
495 return (
496 StatusCode::OK,
497 Json(serde_json::json!({
498 "next": "passkey",
499 "redirect": redirect_url
500 })),
501 )
502 .into_response();
503 }
504 return redirect_see_other(&redirect_url);
505 }
506
507 let password_valid = match &user.password_hash {
508 Some(hash) => match bcrypt::verify(&form.password, hash) {
509 Ok(valid) => valid,
510 Err(_) => {
511 return show_login_error("An error occurred. Please try again.", json_response);
512 }
513 },
514 None => false,
515 };
516 if !password_valid {
517 return show_login_error("Invalid handle/email or password.", json_response);
518 }
519 let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await;
520 if has_totp {
521 let device_cookie = extract_device_cookie(&headers);
522 let device_is_trusted = if let Some(ref dev_id) = device_cookie {
523 crate::api::server::is_device_trusted(&state.db, dev_id, &user.did).await
524 } else {
525 false
526 };
527
528 if device_is_trusted {
529 if let Some(ref dev_id) = device_cookie {
530 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await;
531 }
532 } else {
533 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
534 .await
535 .is_err()
536 {
537 return show_login_error("An error occurred. Please try again.", json_response);
538 }
539 if json_response {
540 return Json(serde_json::json!({
541 "needs_totp": true
542 }))
543 .into_response();
544 }
545 return redirect_see_other(&format!(
546 "/#/oauth/totp?request_uri={}",
547 url_encode(&form.request_uri)
548 ));
549 }
550 }
551 if user.two_factor_enabled {
552 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
553 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await {
554 Ok(challenge) => {
555 let hostname =
556 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
557 if let Err(e) =
558 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
559 {
560 tracing::warn!(
561 did = %user.did,
562 error = %e,
563 "Failed to enqueue 2FA notification"
564 );
565 }
566 let channel_name = channel_display_name(user.preferred_comms_channel);
567 if json_response {
568 return Json(serde_json::json!({
569 "needs_2fa": true,
570 "channel": channel_name
571 }))
572 .into_response();
573 }
574 return redirect_see_other(&format!(
575 "/#/oauth/2fa?request_uri={}&channel={}",
576 url_encode(&form.request_uri),
577 url_encode(channel_name)
578 ));
579 }
580 Err(_) => {
581 return show_login_error("An error occurred. Please try again.", json_response);
582 }
583 }
584 }
585 let mut device_id: Option<String> = extract_device_cookie(&headers);
586 let mut new_cookie: Option<String> = None;
587 if form.remember_device {
588 let final_device_id = if let Some(existing_id) = &device_id {
589 existing_id.clone()
590 } else {
591 let new_id = DeviceId::generate();
592 let device_data = DeviceData {
593 session_id: SessionId::generate().0,
594 user_agent: extract_user_agent(&headers),
595 ip_address: extract_client_ip(&headers),
596 last_seen_at: Utc::now(),
597 };
598 if db::create_device(&state.db, &new_id.0, &device_data)
599 .await
600 .is_ok()
601 {
602 new_cookie = Some(make_device_cookie(&new_id.0));
603 device_id = Some(new_id.0.clone());
604 }
605 new_id.0
606 };
607 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await;
608 }
609 if db::set_authorization_did(
610 &state.db,
611 &form.request_uri,
612 &user.did,
613 device_id.as_deref(),
614 )
615 .await
616 .is_err()
617 {
618 return show_login_error("An error occurred. Please try again.", json_response);
619 }
620 let requested_scope_str = request_data
621 .parameters
622 .scope
623 .as_deref()
624 .unwrap_or("atproto");
625 let requested_scopes: Vec<String> = requested_scope_str
626 .split_whitespace()
627 .map(|s| s.to_string())
628 .collect();
629 let needs_consent = db::should_show_consent(
630 &state.db,
631 &user.did,
632 &request_data.parameters.client_id,
633 &requested_scopes,
634 )
635 .await
636 .unwrap_or(true);
637 if needs_consent {
638 let consent_url = format!(
639 "/#/oauth/consent?request_uri={}",
640 url_encode(&form.request_uri)
641 );
642 if json_response {
643 if let Some(cookie) = new_cookie {
644 return (
645 StatusCode::OK,
646 [(SET_COOKIE, cookie)],
647 Json(serde_json::json!({"redirect_uri": consent_url})),
648 )
649 .into_response();
650 }
651 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
652 }
653 if let Some(cookie) = new_cookie {
654 return (
655 StatusCode::SEE_OTHER,
656 [(SET_COOKIE, cookie), (LOCATION, consent_url)],
657 )
658 .into_response();
659 }
660 return redirect_see_other(&consent_url);
661 }
662 let code = Code::generate();
663 if db::update_authorization_request(
664 &state.db,
665 &form.request_uri,
666 &user.did,
667 device_id.as_deref(),
668 &code.0,
669 )
670 .await
671 .is_err()
672 {
673 return show_login_error("An error occurred. Please try again.", json_response);
674 }
675 let redirect_url = build_success_redirect(
676 &request_data.parameters.redirect_uri,
677 &code.0,
678 request_data.parameters.state.as_deref(),
679 request_data.parameters.response_mode.as_deref(),
680 );
681 if json_response {
682 if let Some(cookie) = new_cookie {
683 (
684 StatusCode::OK,
685 [(SET_COOKIE, cookie)],
686 Json(serde_json::json!({"redirect_uri": redirect_url})),
687 )
688 .into_response()
689 } else {
690 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response()
691 }
692 } else if let Some(cookie) = new_cookie {
693 (
694 StatusCode::SEE_OTHER,
695 [(SET_COOKIE, cookie), (LOCATION, redirect_url)],
696 )
697 .into_response()
698 } else {
699 redirect_see_other(&redirect_url)
700 }
701}
702
703pub async fn authorize_select(
704 State(state): State<AppState>,
705 headers: HeaderMap,
706 Json(form): Json<AuthorizeSelectSubmit>,
707) -> Response {
708 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
709 (
710 status,
711 Json(serde_json::json!({
712 "error": error,
713 "error_description": description
714 })),
715 )
716 .into_response()
717 };
718 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
719 Ok(Some(data)) => data,
720 Ok(None) => {
721 return json_error(
722 StatusCode::BAD_REQUEST,
723 "invalid_request",
724 "Invalid or expired request_uri. Please start a new authorization request.",
725 );
726 }
727 Err(_) => {
728 return json_error(
729 StatusCode::INTERNAL_SERVER_ERROR,
730 "server_error",
731 "An error occurred. Please try again.",
732 );
733 }
734 };
735 if request_data.expires_at < Utc::now() {
736 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
737 return json_error(
738 StatusCode::BAD_REQUEST,
739 "invalid_request",
740 "Authorization request has expired. Please start a new request.",
741 );
742 }
743 let device_id = match extract_device_cookie(&headers) {
744 Some(id) => id,
745 None => {
746 return json_error(
747 StatusCode::BAD_REQUEST,
748 "invalid_request",
749 "No device session found. Please sign in.",
750 );
751 }
752 };
753 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await {
754 Ok(valid) => valid,
755 Err(_) => {
756 return json_error(
757 StatusCode::INTERNAL_SERVER_ERROR,
758 "server_error",
759 "An error occurred. Please try again.",
760 );
761 }
762 };
763 if !account_valid {
764 return json_error(
765 StatusCode::FORBIDDEN,
766 "access_denied",
767 "This account is not available on this device. Please sign in.",
768 );
769 }
770 let user = match sqlx::query!(
771 r#"
772 SELECT id, two_factor_enabled,
773 preferred_comms_channel as "preferred_comms_channel: CommsChannel",
774 email_verified, discord_verified, telegram_verified, signal_verified
775 FROM users
776 WHERE did = $1
777 "#,
778 form.did
779 )
780 .fetch_optional(&state.db)
781 .await
782 {
783 Ok(Some(u)) => u,
784 Ok(None) => {
785 return json_error(
786 StatusCode::FORBIDDEN,
787 "access_denied",
788 "Account not found. Please sign in.",
789 );
790 }
791 Err(_) => {
792 return json_error(
793 StatusCode::INTERNAL_SERVER_ERROR,
794 "server_error",
795 "An error occurred. Please try again.",
796 );
797 }
798 };
799 let is_verified = user.email_verified
800 || user.discord_verified
801 || user.telegram_verified
802 || user.signal_verified;
803 if !is_verified {
804 return json_error(
805 StatusCode::FORBIDDEN,
806 "access_denied",
807 "Please verify your account before logging in.",
808 );
809 }
810 let has_totp = crate::api::server::has_totp_enabled(&state, &form.did).await;
811 if has_totp {
812 if db::set_authorization_did(&state.db, &form.request_uri, &form.did, Some(&device_id))
813 .await
814 .is_err()
815 {
816 return json_error(
817 StatusCode::INTERNAL_SERVER_ERROR,
818 "server_error",
819 "An error occurred. Please try again.",
820 );
821 }
822 return Json(serde_json::json!({
823 "needs_totp": true
824 }))
825 .into_response();
826 }
827 if user.two_factor_enabled {
828 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
829 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await {
830 Ok(challenge) => {
831 let hostname =
832 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
833 if let Err(e) =
834 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
835 {
836 tracing::warn!(
837 did = %form.did,
838 error = %e,
839 "Failed to enqueue 2FA notification"
840 );
841 }
842 let channel_name = channel_display_name(user.preferred_comms_channel);
843 return Json(serde_json::json!({
844 "needs_2fa": true,
845 "channel": channel_name
846 }))
847 .into_response();
848 }
849 Err(_) => {
850 return json_error(
851 StatusCode::INTERNAL_SERVER_ERROR,
852 "server_error",
853 "An error occurred. Please try again.",
854 );
855 }
856 }
857 }
858 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await;
859 let code = Code::generate();
860 if db::update_authorization_request(
861 &state.db,
862 &form.request_uri,
863 &form.did,
864 Some(&device_id),
865 &code.0,
866 )
867 .await
868 .is_err()
869 {
870 return json_error(
871 StatusCode::INTERNAL_SERVER_ERROR,
872 "server_error",
873 "An error occurred. Please try again.",
874 );
875 }
876 let redirect_url = build_success_redirect(
877 &request_data.parameters.redirect_uri,
878 &code.0,
879 request_data.parameters.state.as_deref(),
880 request_data.parameters.response_mode.as_deref(),
881 );
882 Json(serde_json::json!({
883 "redirect_uri": redirect_url
884 }))
885 .into_response()
886}
887
888fn build_success_redirect(
889 redirect_uri: &str,
890 code: &str,
891 state: Option<&str>,
892 response_mode: Option<&str>,
893) -> String {
894 let mut redirect_url = redirect_uri.to_string();
895 let use_fragment = response_mode == Some("fragment");
896 let separator = if use_fragment {
897 '#'
898 } else if redirect_url.contains('?') {
899 '&'
900 } else {
901 '?'
902 };
903 redirect_url.push(separator);
904 redirect_url.push_str(&format!("code={}", url_encode(code)));
905 if let Some(req_state) = state {
906 redirect_url.push_str(&format!("&state={}", url_encode(req_state)));
907 }
908 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
909 redirect_url.push_str(&format!(
910 "&iss={}",
911 url_encode(&format!("https://{}", pds_hostname))
912 ));
913 redirect_url
914}
915
916#[derive(Debug, Serialize)]
917pub struct AuthorizeDenyResponse {
918 pub error: String,
919 pub error_description: String,
920}
921
922pub async fn authorize_deny(
923 State(state): State<AppState>,
924 Json(form): Json<AuthorizeDenyForm>,
925) -> Response {
926 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
927 Ok(Some(data)) => data,
928 Ok(None) => {
929 return (
930 StatusCode::BAD_REQUEST,
931 Json(serde_json::json!({
932 "error": "invalid_request",
933 "error_description": "Invalid request_uri"
934 })),
935 )
936 .into_response();
937 }
938 Err(_) => {
939 return (
940 StatusCode::INTERNAL_SERVER_ERROR,
941 Json(serde_json::json!({
942 "error": "server_error",
943 "error_description": "An error occurred"
944 })),
945 )
946 .into_response();
947 }
948 };
949 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
950 let redirect_uri = &request_data.parameters.redirect_uri;
951 let mut redirect_url = redirect_uri.to_string();
952 let separator = if redirect_url.contains('?') { '&' } else { '?' };
953 redirect_url.push(separator);
954 redirect_url.push_str("error=access_denied");
955 redirect_url.push_str("&error_description=User%20denied%20the%20request");
956 if let Some(state) = &request_data.parameters.state {
957 redirect_url.push_str(&format!("&state={}", url_encode(state)));
958 }
959 Json(serde_json::json!({
960 "redirect_uri": redirect_url
961 }))
962 .into_response()
963}
964
965#[derive(Debug, Deserialize)]
966pub struct AuthorizeDenyForm {
967 pub request_uri: String,
968}
969
970#[derive(Debug, Deserialize)]
971pub struct Authorize2faQuery {
972 pub request_uri: String,
973 pub channel: Option<String>,
974}
975
976#[derive(Debug, Deserialize)]
977pub struct Authorize2faSubmit {
978 pub request_uri: String,
979 pub code: String,
980 #[serde(default)]
981 pub trust_device: bool,
982}
983
984const MAX_2FA_ATTEMPTS: i32 = 5;
985
986pub async fn authorize_2fa_get(
987 State(state): State<AppState>,
988 Query(query): Query<Authorize2faQuery>,
989) -> Response {
990 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await {
991 Ok(Some(c)) => c,
992 Ok(None) => {
993 return redirect_to_frontend_error(
994 "invalid_request",
995 "No 2FA challenge found. Please start over.",
996 );
997 }
998 Err(_) => {
999 return redirect_to_frontend_error(
1000 "server_error",
1001 "An error occurred. Please try again.",
1002 );
1003 }
1004 };
1005 if challenge.expires_at < Utc::now() {
1006 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1007 return redirect_to_frontend_error(
1008 "invalid_request",
1009 "2FA code has expired. Please start over.",
1010 );
1011 }
1012 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
1013 Ok(Some(d)) => d,
1014 Ok(None) => {
1015 return redirect_to_frontend_error(
1016 "invalid_request",
1017 "Authorization request not found. Please start over.",
1018 );
1019 }
1020 Err(_) => {
1021 return redirect_to_frontend_error(
1022 "server_error",
1023 "An error occurred. Please try again.",
1024 );
1025 }
1026 };
1027 let channel = query.channel.as_deref().unwrap_or("email");
1028 redirect_see_other(&format!(
1029 "/#/oauth/2fa?request_uri={}&channel={}",
1030 url_encode(&query.request_uri),
1031 url_encode(channel)
1032 ))
1033}
1034
1035#[derive(Debug, Serialize)]
1036pub struct ScopeInfo {
1037 pub scope: String,
1038 pub category: String,
1039 pub required: bool,
1040 pub description: String,
1041 pub display_name: String,
1042 pub granted: Option<bool>,
1043}
1044
1045#[derive(Debug, Serialize)]
1046pub struct ConsentResponse {
1047 pub request_uri: String,
1048 pub client_id: String,
1049 pub client_name: Option<String>,
1050 pub client_uri: Option<String>,
1051 pub logo_uri: Option<String>,
1052 pub scopes: Vec<ScopeInfo>,
1053 pub show_consent: bool,
1054 pub did: String,
1055}
1056
1057#[derive(Debug, Deserialize)]
1058pub struct ConsentQuery {
1059 pub request_uri: String,
1060}
1061
1062#[derive(Debug, Deserialize)]
1063pub struct ConsentSubmit {
1064 pub request_uri: String,
1065 pub approved_scopes: Vec<String>,
1066 pub remember: bool,
1067}
1068
1069pub async fn consent_get(
1070 State(state): State<AppState>,
1071 Query(query): Query<ConsentQuery>,
1072) -> Response {
1073 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
1074 Ok(Some(data)) => data,
1075 Ok(None) => {
1076 return (
1077 StatusCode::BAD_REQUEST,
1078 Json(serde_json::json!({
1079 "error": "invalid_request",
1080 "error_description": "Invalid or expired request_uri"
1081 })),
1082 )
1083 .into_response();
1084 }
1085 Err(e) => {
1086 return (
1087 StatusCode::INTERNAL_SERVER_ERROR,
1088 Json(serde_json::json!({
1089 "error": "server_error",
1090 "error_description": format!("Database error: {:?}", e)
1091 })),
1092 )
1093 .into_response();
1094 }
1095 };
1096 if request_data.expires_at < Utc::now() {
1097 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await;
1098 return (
1099 StatusCode::BAD_REQUEST,
1100 Json(serde_json::json!({
1101 "error": "invalid_request",
1102 "error_description": "Authorization request has expired"
1103 })),
1104 )
1105 .into_response();
1106 }
1107 let did = match &request_data.did {
1108 Some(d) => d.clone(),
1109 None => {
1110 return (
1111 StatusCode::FORBIDDEN,
1112 Json(serde_json::json!({
1113 "error": "access_denied",
1114 "error_description": "Not authenticated"
1115 })),
1116 )
1117 .into_response();
1118 }
1119 };
1120 let client_cache = ClientMetadataCache::new(3600);
1121 let client_metadata = client_cache
1122 .get(&request_data.parameters.client_id)
1123 .await
1124 .ok();
1125 let requested_scope_str = request_data
1126 .parameters
1127 .scope
1128 .as_deref()
1129 .unwrap_or("atproto");
1130 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect();
1131 let preferences =
1132 db::get_scope_preferences(&state.db, &did, &request_data.parameters.client_id)
1133 .await
1134 .unwrap_or_default();
1135 let pref_map: std::collections::HashMap<_, _> = preferences
1136 .iter()
1137 .map(|p| (p.scope.as_str(), p.granted))
1138 .collect();
1139 let requested_scope_strings: Vec<String> =
1140 requested_scopes.iter().map(|s| s.to_string()).collect();
1141 let show_consent = db::should_show_consent(
1142 &state.db,
1143 &did,
1144 &request_data.parameters.client_id,
1145 &requested_scope_strings,
1146 )
1147 .await
1148 .unwrap_or(true);
1149 let mut scopes = Vec::new();
1150 for scope in &requested_scopes {
1151 let (category, required, description, display_name) =
1152 if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) {
1153 (
1154 def.category.display_name().to_string(),
1155 def.required,
1156 def.description.to_string(),
1157 def.display_name.to_string(),
1158 )
1159 } else if scope.starts_with("ref:") {
1160 (
1161 "Reference".to_string(),
1162 false,
1163 "Referenced scope".to_string(),
1164 scope.to_string(),
1165 )
1166 } else {
1167 (
1168 "Other".to_string(),
1169 false,
1170 format!("Access to {}", scope),
1171 scope.to_string(),
1172 )
1173 };
1174 let granted = pref_map.get(*scope).copied();
1175 scopes.push(ScopeInfo {
1176 scope: scope.to_string(),
1177 category,
1178 required,
1179 description,
1180 display_name,
1181 granted,
1182 });
1183 }
1184 Json(ConsentResponse {
1185 request_uri: query.request_uri.clone(),
1186 client_id: request_data.parameters.client_id.clone(),
1187 client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()),
1188 client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()),
1189 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()),
1190 scopes,
1191 show_consent,
1192 did,
1193 })
1194 .into_response()
1195}
1196
1197pub async fn consent_post(
1198 State(state): State<AppState>,
1199 Json(form): Json<ConsentSubmit>,
1200) -> Response {
1201 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1202 Ok(Some(data)) => data,
1203 Ok(None) => {
1204 return (
1205 StatusCode::BAD_REQUEST,
1206 Json(serde_json::json!({
1207 "error": "invalid_request",
1208 "error_description": "Invalid or expired request_uri"
1209 })),
1210 )
1211 .into_response();
1212 }
1213 Err(e) => {
1214 return (
1215 StatusCode::INTERNAL_SERVER_ERROR,
1216 Json(serde_json::json!({
1217 "error": "server_error",
1218 "error_description": format!("Database error: {:?}", e)
1219 })),
1220 )
1221 .into_response();
1222 }
1223 };
1224 if request_data.expires_at < Utc::now() {
1225 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1226 return (
1227 StatusCode::BAD_REQUEST,
1228 Json(serde_json::json!({
1229 "error": "invalid_request",
1230 "error_description": "Authorization request has expired"
1231 })),
1232 )
1233 .into_response();
1234 }
1235 let did = match &request_data.did {
1236 Some(d) => d.clone(),
1237 None => {
1238 return (
1239 StatusCode::FORBIDDEN,
1240 Json(serde_json::json!({
1241 "error": "access_denied",
1242 "error_description": "Not authenticated"
1243 })),
1244 )
1245 .into_response();
1246 }
1247 };
1248 let requested_scope_str = request_data
1249 .parameters
1250 .scope
1251 .as_deref()
1252 .unwrap_or("atproto");
1253 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect();
1254 let has_granular_scopes = requested_scopes.iter().any(|s| {
1255 s.starts_with("repo:")
1256 || s.starts_with("blob:")
1257 || s.starts_with("rpc:")
1258 || s.starts_with("account:")
1259 || s.starts_with("identity:")
1260 });
1261 let user_denied_some_granular = has_granular_scopes
1262 && requested_scopes
1263 .iter()
1264 .filter(|s| {
1265 s.starts_with("repo:")
1266 || s.starts_with("blob:")
1267 || s.starts_with("rpc:")
1268 || s.starts_with("account:")
1269 || s.starts_with("identity:")
1270 })
1271 .any(|s| !form.approved_scopes.contains(&s.to_string()));
1272 let atproto_was_requested = requested_scopes.contains(&"atproto");
1273 if atproto_was_requested
1274 && !has_granular_scopes
1275 && !form.approved_scopes.contains(&"atproto".to_string())
1276 {
1277 return (
1278 StatusCode::BAD_REQUEST,
1279 Json(serde_json::json!({
1280 "error": "invalid_request",
1281 "error_description": "The atproto scope was requested and must be approved"
1282 })),
1283 )
1284 .into_response();
1285 }
1286 let final_approved: Vec<String> = if user_denied_some_granular {
1287 form.approved_scopes
1288 .iter()
1289 .filter(|s| *s != "atproto")
1290 .cloned()
1291 .collect()
1292 } else {
1293 form.approved_scopes.clone()
1294 };
1295 if final_approved.is_empty() {
1296 return (
1297 StatusCode::BAD_REQUEST,
1298 Json(serde_json::json!({
1299 "error": "invalid_request",
1300 "error_description": "At least one scope must be approved"
1301 })),
1302 )
1303 .into_response();
1304 }
1305 let approved_scope_str = final_approved.join(" ");
1306 let has_valid_scope = final_approved.iter().all(|s| {
1307 s == "atproto"
1308 || s == "transition:generic"
1309 || s == "transition:chat.bsky"
1310 || s == "transition:email"
1311 || s.starts_with("repo:")
1312 || s.starts_with("blob:")
1313 || s.starts_with("rpc:")
1314 || s.starts_with("account:")
1315 || s.starts_with("include:")
1316 });
1317 if !has_valid_scope {
1318 return (
1319 StatusCode::BAD_REQUEST,
1320 Json(serde_json::json!({
1321 "error": "invalid_request",
1322 "error_description": "Invalid scope format"
1323 })),
1324 )
1325 .into_response();
1326 }
1327 if form.remember {
1328 let preferences: Vec<db::ScopePreference> = requested_scopes
1329 .iter()
1330 .map(|s| db::ScopePreference {
1331 scope: s.to_string(),
1332 granted: form.approved_scopes.contains(&s.to_string()),
1333 })
1334 .collect();
1335 let _ = db::upsert_scope_preferences(
1336 &state.db,
1337 &did,
1338 &request_data.parameters.client_id,
1339 &preferences,
1340 )
1341 .await;
1342 }
1343 if let Err(e) =
1344 db::update_request_scope(&state.db, &form.request_uri, &approved_scope_str).await
1345 {
1346 tracing::warn!("Failed to update request scope: {:?}", e);
1347 }
1348 let code = Code::generate();
1349 if db::update_authorization_request(
1350 &state.db,
1351 &form.request_uri,
1352 &did,
1353 request_data.device_id.as_deref(),
1354 &code.0,
1355 )
1356 .await
1357 .is_err()
1358 {
1359 return (
1360 StatusCode::INTERNAL_SERVER_ERROR,
1361 Json(serde_json::json!({
1362 "error": "server_error",
1363 "error_description": "Failed to complete authorization"
1364 })),
1365 )
1366 .into_response();
1367 }
1368 let redirect_url = build_success_redirect(
1369 &request_data.parameters.redirect_uri,
1370 &code.0,
1371 request_data.parameters.state.as_deref(),
1372 request_data.parameters.response_mode.as_deref(),
1373 );
1374 Json(serde_json::json!({
1375 "redirect_uri": redirect_url
1376 }))
1377 .into_response()
1378}
1379
1380pub async fn authorize_2fa_post(
1381 State(state): State<AppState>,
1382 headers: HeaderMap,
1383 Json(form): Json<Authorize2faSubmit>,
1384) -> Response {
1385 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
1386 (
1387 status,
1388 Json(serde_json::json!({
1389 "error": error,
1390 "error_description": description
1391 })),
1392 )
1393 .into_response()
1394 };
1395 let client_ip = extract_client_ip(&headers);
1396 if !state
1397 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1398 .await
1399 {
1400 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded");
1401 return json_error(
1402 StatusCode::TOO_MANY_REQUESTS,
1403 "RateLimitExceeded",
1404 "Too many attempts. Please try again later.",
1405 );
1406 }
1407 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1408 Ok(Some(d)) => d,
1409 Ok(None) => {
1410 return json_error(
1411 StatusCode::BAD_REQUEST,
1412 "invalid_request",
1413 "Authorization request not found.",
1414 );
1415 }
1416 Err(_) => {
1417 return json_error(
1418 StatusCode::INTERNAL_SERVER_ERROR,
1419 "server_error",
1420 "An error occurred.",
1421 );
1422 }
1423 };
1424 if request_data.expires_at < Utc::now() {
1425 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1426 return json_error(
1427 StatusCode::BAD_REQUEST,
1428 "invalid_request",
1429 "Authorization request has expired.",
1430 );
1431 }
1432 let challenge = db::get_2fa_challenge(&state.db, &form.request_uri)
1433 .await
1434 .ok()
1435 .flatten();
1436 if let Some(challenge) = challenge {
1437 if challenge.expires_at < Utc::now() {
1438 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1439 return json_error(
1440 StatusCode::BAD_REQUEST,
1441 "invalid_request",
1442 "2FA code has expired. Please start over.",
1443 );
1444 }
1445 if challenge.attempts >= MAX_2FA_ATTEMPTS {
1446 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1447 return json_error(
1448 StatusCode::FORBIDDEN,
1449 "access_denied",
1450 "Too many failed attempts. Please start over.",
1451 );
1452 }
1453 let code_valid: bool = form
1454 .code
1455 .trim()
1456 .as_bytes()
1457 .ct_eq(challenge.code.as_bytes())
1458 .into();
1459 if !code_valid {
1460 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await;
1461 return json_error(
1462 StatusCode::FORBIDDEN,
1463 "invalid_code",
1464 "Invalid verification code. Please try again.",
1465 );
1466 }
1467 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1468 let code = Code::generate();
1469 let device_id = extract_device_cookie(&headers);
1470 if db::update_authorization_request(
1471 &state.db,
1472 &form.request_uri,
1473 &challenge.did,
1474 device_id.as_deref(),
1475 &code.0,
1476 )
1477 .await
1478 .is_err()
1479 {
1480 return json_error(
1481 StatusCode::INTERNAL_SERVER_ERROR,
1482 "server_error",
1483 "An error occurred. Please try again.",
1484 );
1485 }
1486 let redirect_url = build_success_redirect(
1487 &request_data.parameters.redirect_uri,
1488 &code.0,
1489 request_data.parameters.state.as_deref(),
1490 request_data.parameters.response_mode.as_deref(),
1491 );
1492 return Json(serde_json::json!({
1493 "redirect_uri": redirect_url
1494 }))
1495 .into_response();
1496 }
1497 let did = match &request_data.did {
1498 Some(d) => d.clone(),
1499 None => {
1500 return json_error(
1501 StatusCode::BAD_REQUEST,
1502 "invalid_request",
1503 "No 2FA challenge found. Please start over.",
1504 );
1505 }
1506 };
1507 if !crate::api::server::has_totp_enabled(&state, &did).await {
1508 return json_error(
1509 StatusCode::BAD_REQUEST,
1510 "invalid_request",
1511 "No 2FA challenge found. Please start over.",
1512 );
1513 }
1514 let totp_valid =
1515 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
1516 if !totp_valid {
1517 return json_error(
1518 StatusCode::FORBIDDEN,
1519 "invalid_code",
1520 "Invalid verification code. Please try again.",
1521 );
1522 }
1523 let device_id = extract_device_cookie(&headers);
1524 if form.trust_device
1525 && let Some(ref dev_id) = device_id
1526 {
1527 let _ = crate::api::server::trust_device(&state.db, dev_id).await;
1528 }
1529 let requested_scope_str = request_data
1530 .parameters
1531 .scope
1532 .as_deref()
1533 .unwrap_or("atproto");
1534 let requested_scopes: Vec<String> = requested_scope_str
1535 .split_whitespace()
1536 .map(|s| s.to_string())
1537 .collect();
1538 let needs_consent = db::should_show_consent(
1539 &state.db,
1540 &did,
1541 &request_data.parameters.client_id,
1542 &requested_scopes,
1543 )
1544 .await
1545 .unwrap_or(true);
1546 if needs_consent {
1547 let consent_url = format!(
1548 "/#/oauth/consent?request_uri={}",
1549 url_encode(&form.request_uri)
1550 );
1551 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
1552 }
1553 let code = Code::generate();
1554 if db::update_authorization_request(
1555 &state.db,
1556 &form.request_uri,
1557 &did,
1558 device_id.as_deref(),
1559 &code.0,
1560 )
1561 .await
1562 .is_err()
1563 {
1564 return json_error(
1565 StatusCode::INTERNAL_SERVER_ERROR,
1566 "server_error",
1567 "An error occurred. Please try again.",
1568 );
1569 }
1570 let redirect_url = build_success_redirect(
1571 &request_data.parameters.redirect_uri,
1572 &code.0,
1573 request_data.parameters.state.as_deref(),
1574 request_data.parameters.response_mode.as_deref(),
1575 );
1576 Json(serde_json::json!({
1577 "redirect_uri": redirect_url
1578 }))
1579 .into_response()
1580}
1581
1582#[derive(Debug, Deserialize)]
1583#[serde(rename_all = "camelCase")]
1584pub struct CheckPasskeysQuery {
1585 pub identifier: String,
1586}
1587
1588#[derive(Debug, Serialize)]
1589#[serde(rename_all = "camelCase")]
1590pub struct CheckPasskeysResponse {
1591 pub has_passkeys: bool,
1592}
1593
1594pub async fn check_user_has_passkeys(
1595 State(state): State<AppState>,
1596 Query(query): Query<CheckPasskeysQuery>,
1597) -> Response {
1598 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1599 let normalized_identifier = query.identifier.trim();
1600 let normalized_identifier = normalized_identifier
1601 .strip_prefix('@')
1602 .unwrap_or(normalized_identifier);
1603 let normalized_identifier = if let Some(bare_handle) =
1604 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname))
1605 {
1606 bare_handle.to_string()
1607 } else {
1608 normalized_identifier.to_string()
1609 };
1610
1611 let user = sqlx::query!(
1612 "SELECT did FROM users WHERE handle = $1 OR email = $1",
1613 normalized_identifier
1614 )
1615 .fetch_optional(&state.db)
1616 .await;
1617
1618 let has_passkeys = match user {
1619 Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await,
1620 _ => false,
1621 };
1622
1623 Json(CheckPasskeysResponse { has_passkeys }).into_response()
1624}
1625
1626#[derive(Debug, Serialize)]
1627#[serde(rename_all = "camelCase")]
1628pub struct SecurityStatusResponse {
1629 pub has_passkeys: bool,
1630 pub has_totp: bool,
1631}
1632
1633pub async fn check_user_security_status(
1634 State(state): State<AppState>,
1635 Query(query): Query<CheckPasskeysQuery>,
1636) -> Response {
1637 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1638 let identifier = query.identifier.trim();
1639 let identifier = identifier.strip_prefix('@').unwrap_or(identifier);
1640 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") {
1641 identifier.to_string()
1642 } else if !identifier.contains('.') {
1643 format!("{}.{}", identifier.to_lowercase(), pds_hostname)
1644 } else {
1645 identifier.to_lowercase()
1646 };
1647
1648 let user = sqlx::query!(
1649 "SELECT did FROM users WHERE handle = $1 OR email = $1",
1650 normalized_identifier
1651 )
1652 .fetch_optional(&state.db)
1653 .await;
1654
1655 let (has_passkeys, has_totp) = match user {
1656 Ok(Some(u)) => {
1657 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await;
1658 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await;
1659 (passkeys, totp)
1660 }
1661 _ => (false, false),
1662 };
1663
1664 Json(SecurityStatusResponse {
1665 has_passkeys,
1666 has_totp,
1667 })
1668 .into_response()
1669}
1670
1671#[derive(Debug, Deserialize)]
1672pub struct PasskeyStartInput {
1673 pub request_uri: String,
1674 pub identifier: String,
1675}
1676
1677#[derive(Debug, Serialize)]
1678#[serde(rename_all = "camelCase")]
1679pub struct PasskeyStartResponse {
1680 pub options: serde_json::Value,
1681}
1682
1683pub async fn passkey_start(
1684 State(state): State<AppState>,
1685 headers: HeaderMap,
1686 Json(form): Json<PasskeyStartInput>,
1687) -> Response {
1688 let client_ip = extract_client_ip(&headers);
1689
1690 if !state
1691 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1692 .await
1693 {
1694 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded");
1695 return (
1696 StatusCode::TOO_MANY_REQUESTS,
1697 Json(serde_json::json!({
1698 "error": "RateLimitExceeded",
1699 "error_description": "Too many login attempts. Please try again later."
1700 })),
1701 )
1702 .into_response();
1703 }
1704
1705 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1706 Ok(Some(data)) => data,
1707 Ok(None) => {
1708 return (
1709 StatusCode::BAD_REQUEST,
1710 Json(serde_json::json!({
1711 "error": "invalid_request",
1712 "error_description": "Invalid or expired request_uri."
1713 })),
1714 )
1715 .into_response();
1716 }
1717 Err(_) => {
1718 return (
1719 StatusCode::INTERNAL_SERVER_ERROR,
1720 Json(serde_json::json!({
1721 "error": "server_error",
1722 "error_description": "An error occurred."
1723 })),
1724 )
1725 .into_response();
1726 }
1727 };
1728
1729 if request_data.expires_at < Utc::now() {
1730 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1731 return (
1732 StatusCode::BAD_REQUEST,
1733 Json(serde_json::json!({
1734 "error": "invalid_request",
1735 "error_description": "Authorization request has expired."
1736 })),
1737 )
1738 .into_response();
1739 }
1740
1741 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1742 let normalized_username = form.identifier.trim();
1743 let normalized_username = normalized_username
1744 .strip_prefix('@')
1745 .unwrap_or(normalized_username);
1746 let normalized_username = if normalized_username.contains('@') {
1747 normalized_username.to_string()
1748 } else if !normalized_username.contains('.') {
1749 format!("{}.{}", normalized_username, pds_hostname)
1750 } else {
1751 normalized_username.to_string()
1752 };
1753
1754 let user = match sqlx::query!(
1755 r#"
1756 SELECT did, deactivated_at, takedown_ref,
1757 email_verified, discord_verified, telegram_verified, signal_verified
1758 FROM users
1759 WHERE handle = $1 OR email = $1
1760 "#,
1761 normalized_username
1762 )
1763 .fetch_optional(&state.db)
1764 .await
1765 {
1766 Ok(Some(u)) => u,
1767 Ok(None) => {
1768 return (
1769 StatusCode::FORBIDDEN,
1770 Json(serde_json::json!({
1771 "error": "access_denied",
1772 "error_description": "User not found or has no passkeys."
1773 })),
1774 )
1775 .into_response();
1776 }
1777 Err(_) => {
1778 return (
1779 StatusCode::INTERNAL_SERVER_ERROR,
1780 Json(serde_json::json!({
1781 "error": "server_error",
1782 "error_description": "An error occurred."
1783 })),
1784 )
1785 .into_response();
1786 }
1787 };
1788
1789 if user.deactivated_at.is_some() {
1790 return (
1791 StatusCode::FORBIDDEN,
1792 Json(serde_json::json!({
1793 "error": "access_denied",
1794 "error_description": "This account has been deactivated."
1795 })),
1796 )
1797 .into_response();
1798 }
1799
1800 if user.takedown_ref.is_some() {
1801 return (
1802 StatusCode::FORBIDDEN,
1803 Json(serde_json::json!({
1804 "error": "access_denied",
1805 "error_description": "This account has been taken down."
1806 })),
1807 )
1808 .into_response();
1809 }
1810
1811 let is_verified = user.email_verified
1812 || user.discord_verified
1813 || user.telegram_verified
1814 || user.signal_verified;
1815
1816 if !is_verified {
1817 return (
1818 StatusCode::FORBIDDEN,
1819 Json(serde_json::json!({
1820 "error": "access_denied",
1821 "error_description": "Please verify your account before logging in."
1822 })),
1823 )
1824 .into_response();
1825 }
1826
1827 let stored_passkeys =
1828 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await {
1829 Ok(pks) => pks,
1830 Err(e) => {
1831 tracing::error!(error = %e, "Failed to get passkeys");
1832 return (
1833 StatusCode::INTERNAL_SERVER_ERROR,
1834 Json(serde_json::json!({
1835 "error": "server_error",
1836 "error_description": "An error occurred."
1837 })),
1838 )
1839 .into_response();
1840 }
1841 };
1842
1843 if stored_passkeys.is_empty() {
1844 return (
1845 StatusCode::FORBIDDEN,
1846 Json(serde_json::json!({
1847 "error": "access_denied",
1848 "error_description": "User not found or has no passkeys."
1849 })),
1850 )
1851 .into_response();
1852 }
1853
1854 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
1855 .iter()
1856 .filter_map(|sp| sp.to_security_key().ok())
1857 .collect();
1858
1859 if passkeys.is_empty() {
1860 return (
1861 StatusCode::INTERNAL_SERVER_ERROR,
1862 Json(serde_json::json!({
1863 "error": "server_error",
1864 "error_description": "Failed to load passkeys."
1865 })),
1866 )
1867 .into_response();
1868 }
1869
1870 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
1871 Ok(w) => w,
1872 Err(e) => {
1873 tracing::error!(error = %e, "Failed to create WebAuthn config");
1874 return (
1875 StatusCode::INTERNAL_SERVER_ERROR,
1876 Json(serde_json::json!({
1877 "error": "server_error",
1878 "error_description": "WebAuthn configuration failed."
1879 })),
1880 )
1881 .into_response();
1882 }
1883 };
1884
1885 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
1886 Ok(result) => result,
1887 Err(e) => {
1888 tracing::error!(error = %e, "Failed to start passkey authentication");
1889 return (
1890 StatusCode::INTERNAL_SERVER_ERROR,
1891 Json(serde_json::json!({
1892 "error": "server_error",
1893 "error_description": "Failed to start authentication."
1894 })),
1895 )
1896 .into_response();
1897 }
1898 };
1899
1900 if let Err(e) =
1901 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await
1902 {
1903 tracing::error!(error = %e, "Failed to save authentication state");
1904 return (
1905 StatusCode::INTERNAL_SERVER_ERROR,
1906 Json(serde_json::json!({
1907 "error": "server_error",
1908 "error_description": "An error occurred."
1909 })),
1910 )
1911 .into_response();
1912 }
1913
1914 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
1915 .await
1916 .is_err()
1917 {
1918 return (
1919 StatusCode::INTERNAL_SERVER_ERROR,
1920 Json(serde_json::json!({
1921 "error": "server_error",
1922 "error_description": "An error occurred."
1923 })),
1924 )
1925 .into_response();
1926 }
1927
1928 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
1929
1930 Json(PasskeyStartResponse { options }).into_response()
1931}
1932
1933#[derive(Debug, Deserialize)]
1934pub struct PasskeyFinishInput {
1935 pub request_uri: String,
1936 pub credential: serde_json::Value,
1937}
1938
1939pub async fn passkey_finish(
1940 State(state): State<AppState>,
1941 headers: HeaderMap,
1942 Json(form): Json<PasskeyFinishInput>,
1943) -> Response {
1944 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1945 Ok(Some(data)) => data,
1946 Ok(None) => {
1947 return (
1948 StatusCode::BAD_REQUEST,
1949 Json(serde_json::json!({
1950 "error": "invalid_request",
1951 "error_description": "Invalid or expired request_uri."
1952 })),
1953 )
1954 .into_response();
1955 }
1956 Err(_) => {
1957 return (
1958 StatusCode::INTERNAL_SERVER_ERROR,
1959 Json(serde_json::json!({
1960 "error": "server_error",
1961 "error_description": "An error occurred."
1962 })),
1963 )
1964 .into_response();
1965 }
1966 };
1967
1968 if request_data.expires_at < Utc::now() {
1969 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1970 return (
1971 StatusCode::BAD_REQUEST,
1972 Json(serde_json::json!({
1973 "error": "invalid_request",
1974 "error_description": "Authorization request has expired."
1975 })),
1976 )
1977 .into_response();
1978 }
1979
1980 let did = match request_data.did {
1981 Some(d) => d,
1982 None => {
1983 return (
1984 StatusCode::BAD_REQUEST,
1985 Json(serde_json::json!({
1986 "error": "invalid_request",
1987 "error_description": "No passkey authentication in progress."
1988 })),
1989 )
1990 .into_response();
1991 }
1992 };
1993
1994 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await {
1995 Ok(Some(s)) => s,
1996 Ok(None) => {
1997 return (
1998 StatusCode::BAD_REQUEST,
1999 Json(serde_json::json!({
2000 "error": "invalid_request",
2001 "error_description": "No passkey authentication in progress or challenge expired."
2002 })),
2003 )
2004 .into_response();
2005 }
2006 Err(e) => {
2007 tracing::error!(error = %e, "Failed to load authentication state");
2008 return (
2009 StatusCode::INTERNAL_SERVER_ERROR,
2010 Json(serde_json::json!({
2011 "error": "server_error",
2012 "error_description": "An error occurred."
2013 })),
2014 )
2015 .into_response();
2016 }
2017 };
2018
2019 let credential: webauthn_rs::prelude::PublicKeyCredential =
2020 match serde_json::from_value(form.credential) {
2021 Ok(c) => c,
2022 Err(e) => {
2023 tracing::warn!(error = %e, "Failed to parse credential");
2024 return (
2025 StatusCode::BAD_REQUEST,
2026 Json(serde_json::json!({
2027 "error": "invalid_request",
2028 "error_description": "Failed to parse credential response."
2029 })),
2030 )
2031 .into_response();
2032 }
2033 };
2034
2035 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2036 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2037 Ok(w) => w,
2038 Err(e) => {
2039 tracing::error!(error = %e, "Failed to create WebAuthn config");
2040 return (
2041 StatusCode::INTERNAL_SERVER_ERROR,
2042 Json(serde_json::json!({
2043 "error": "server_error",
2044 "error_description": "WebAuthn configuration failed."
2045 })),
2046 )
2047 .into_response();
2048 }
2049 };
2050
2051 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
2052 Ok(r) => r,
2053 Err(e) => {
2054 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication");
2055 return (
2056 StatusCode::FORBIDDEN,
2057 Json(serde_json::json!({
2058 "error": "access_denied",
2059 "error_description": "Passkey verification failed."
2060 })),
2061 )
2062 .into_response();
2063 }
2064 };
2065
2066 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await {
2067 tracing::warn!(error = %e, "Failed to delete authentication state");
2068 }
2069
2070 if auth_result.needs_update()
2071 && let Err(e) = crate::auth::webauthn::update_passkey_counter(
2072 &state.db,
2073 auth_result.cred_id(),
2074 auth_result.counter(),
2075 )
2076 .await
2077 {
2078 tracing::warn!(error = %e, "Failed to update passkey counter");
2079 }
2080
2081 tracing::info!(did = %did, "Passkey authentication successful");
2082
2083 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await;
2084 if has_totp {
2085 return Json(serde_json::json!({
2086 "needs_totp": true
2087 }))
2088 .into_response();
2089 }
2090
2091 let user = sqlx::query!(
2092 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1",
2093 did
2094 )
2095 .fetch_optional(&state.db)
2096 .await;
2097
2098 if let Ok(Some(user)) = user
2099 && user.two_factor_enabled
2100 {
2101 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
2102 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await {
2103 Ok(challenge) => {
2104 let hostname =
2105 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2106 if let Err(e) =
2107 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
2108 {
2109 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
2110 }
2111 let channel_name = channel_display_name(user.preferred_comms_channel);
2112 return Json(serde_json::json!({
2113 "needs_2fa": true,
2114 "channel": channel_name
2115 }))
2116 .into_response();
2117 }
2118 Err(_) => {
2119 return (
2120 StatusCode::INTERNAL_SERVER_ERROR,
2121 Json(serde_json::json!({
2122 "error": "server_error",
2123 "error_description": "An error occurred."
2124 })),
2125 )
2126 .into_response();
2127 }
2128 }
2129 }
2130
2131 let device_id = extract_device_cookie(&headers);
2132 let requested_scope_str = request_data
2133 .parameters
2134 .scope
2135 .as_deref()
2136 .unwrap_or("atproto");
2137 let requested_scopes: Vec<String> = requested_scope_str
2138 .split_whitespace()
2139 .map(|s| s.to_string())
2140 .collect();
2141
2142 let needs_consent = db::should_show_consent(
2143 &state.db,
2144 &did,
2145 &request_data.parameters.client_id,
2146 &requested_scopes,
2147 )
2148 .await
2149 .unwrap_or(true);
2150
2151 if needs_consent {
2152 let consent_url = format!(
2153 "/#/oauth/consent?request_uri={}",
2154 url_encode(&form.request_uri)
2155 );
2156 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
2157 }
2158
2159 let code = Code::generate();
2160 if db::update_authorization_request(
2161 &state.db,
2162 &form.request_uri,
2163 &did,
2164 device_id.as_deref(),
2165 &code.0,
2166 )
2167 .await
2168 .is_err()
2169 {
2170 return (
2171 StatusCode::INTERNAL_SERVER_ERROR,
2172 Json(serde_json::json!({
2173 "error": "server_error",
2174 "error_description": "An error occurred."
2175 })),
2176 )
2177 .into_response();
2178 }
2179
2180 let redirect_url = build_success_redirect(
2181 &request_data.parameters.redirect_uri,
2182 &code.0,
2183 request_data.parameters.state.as_deref(),
2184 request_data.parameters.response_mode.as_deref(),
2185 );
2186
2187 Json(serde_json::json!({
2188 "redirect_uri": redirect_url
2189 }))
2190 .into_response()
2191}
2192
2193#[derive(Debug, Deserialize)]
2194pub struct AuthorizePasskeyQuery {
2195 pub request_uri: String,
2196}
2197
2198#[derive(Debug, Serialize)]
2199#[serde(rename_all = "camelCase")]
2200pub struct PasskeyAuthResponse {
2201 pub options: serde_json::Value,
2202 pub request_uri: String,
2203}
2204
2205pub async fn authorize_passkey_start(
2206 State(state): State<AppState>,
2207 Query(query): Query<AuthorizePasskeyQuery>,
2208) -> Response {
2209 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2210
2211 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
2212 Ok(Some(d)) => d,
2213 Ok(None) => {
2214 return (
2215 StatusCode::BAD_REQUEST,
2216 Json(serde_json::json!({
2217 "error": "invalid_request",
2218 "error_description": "Authorization request not found."
2219 })),
2220 )
2221 .into_response();
2222 }
2223 Err(_) => {
2224 return (
2225 StatusCode::INTERNAL_SERVER_ERROR,
2226 Json(serde_json::json!({
2227 "error": "server_error",
2228 "error_description": "An error occurred."
2229 })),
2230 )
2231 .into_response();
2232 }
2233 };
2234
2235 if request_data.expires_at < Utc::now() {
2236 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await;
2237 return (
2238 StatusCode::BAD_REQUEST,
2239 Json(serde_json::json!({
2240 "error": "invalid_request",
2241 "error_description": "Authorization request has expired."
2242 })),
2243 )
2244 .into_response();
2245 }
2246
2247 let did = match &request_data.did {
2248 Some(d) => d.clone(),
2249 None => {
2250 return (
2251 StatusCode::BAD_REQUEST,
2252 Json(serde_json::json!({
2253 "error": "invalid_request",
2254 "error_description": "User not authenticated yet."
2255 })),
2256 )
2257 .into_response();
2258 }
2259 };
2260
2261 let stored_passkeys = match crate::auth::webauthn::get_passkeys_for_user(&state.db, &did).await
2262 {
2263 Ok(pks) => pks,
2264 Err(e) => {
2265 tracing::error!("Failed to get passkeys: {:?}", e);
2266 return (
2267 StatusCode::INTERNAL_SERVER_ERROR,
2268 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2269 )
2270 .into_response();
2271 }
2272 };
2273
2274 if stored_passkeys.is_empty() {
2275 return (
2276 StatusCode::BAD_REQUEST,
2277 Json(serde_json::json!({
2278 "error": "invalid_request",
2279 "error_description": "No passkeys registered for this account."
2280 })),
2281 )
2282 .into_response();
2283 }
2284
2285 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
2286 .iter()
2287 .filter_map(|sp| sp.to_security_key().ok())
2288 .collect();
2289
2290 if passkeys.is_empty() {
2291 return (
2292 StatusCode::INTERNAL_SERVER_ERROR,
2293 Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})),
2294 )
2295 .into_response();
2296 }
2297
2298 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2299 Ok(w) => w,
2300 Err(e) => {
2301 tracing::error!("Failed to create WebAuthn config: {:?}", e);
2302 return (
2303 StatusCode::INTERNAL_SERVER_ERROR,
2304 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2305 )
2306 .into_response();
2307 }
2308 };
2309
2310 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
2311 Ok(result) => result,
2312 Err(e) => {
2313 tracing::error!("Failed to start passkey authentication: {:?}", e);
2314 return (
2315 StatusCode::INTERNAL_SERVER_ERROR,
2316 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2317 )
2318 .into_response();
2319 }
2320 };
2321
2322 if let Err(e) =
2323 crate::auth::webauthn::save_authentication_state(&state.db, &did, &auth_state).await
2324 {
2325 tracing::error!("Failed to save authentication state: {:?}", e);
2326 return (
2327 StatusCode::INTERNAL_SERVER_ERROR,
2328 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2329 )
2330 .into_response();
2331 }
2332
2333 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
2334 Json(PasskeyAuthResponse {
2335 options,
2336 request_uri: query.request_uri,
2337 })
2338 .into_response()
2339}
2340
2341#[derive(Debug, Deserialize)]
2342#[serde(rename_all = "camelCase")]
2343pub struct AuthorizePasskeySubmit {
2344 pub request_uri: String,
2345 pub credential: serde_json::Value,
2346}
2347
2348pub async fn authorize_passkey_finish(
2349 State(state): State<AppState>,
2350 headers: HeaderMap,
2351 Json(form): Json<AuthorizePasskeySubmit>,
2352) -> Response {
2353 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2354
2355 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
2356 Ok(Some(d)) => d,
2357 Ok(None) => {
2358 return (
2359 StatusCode::BAD_REQUEST,
2360 Json(serde_json::json!({
2361 "error": "invalid_request",
2362 "error_description": "Authorization request not found."
2363 })),
2364 )
2365 .into_response();
2366 }
2367 Err(_) => {
2368 return (
2369 StatusCode::INTERNAL_SERVER_ERROR,
2370 Json(serde_json::json!({
2371 "error": "server_error",
2372 "error_description": "An error occurred."
2373 })),
2374 )
2375 .into_response();
2376 }
2377 };
2378
2379 if request_data.expires_at < Utc::now() {
2380 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
2381 return (
2382 StatusCode::BAD_REQUEST,
2383 Json(serde_json::json!({
2384 "error": "invalid_request",
2385 "error_description": "Authorization request has expired."
2386 })),
2387 )
2388 .into_response();
2389 }
2390
2391 let did = match &request_data.did {
2392 Some(d) => d.clone(),
2393 None => {
2394 return (
2395 StatusCode::BAD_REQUEST,
2396 Json(serde_json::json!({
2397 "error": "invalid_request",
2398 "error_description": "User not authenticated yet."
2399 })),
2400 )
2401 .into_response();
2402 }
2403 };
2404
2405 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await {
2406 Ok(Some(s)) => s,
2407 Ok(None) => {
2408 return (
2409 StatusCode::BAD_REQUEST,
2410 Json(serde_json::json!({
2411 "error": "invalid_request",
2412 "error_description": "No passkey challenge found. Please start over."
2413 })),
2414 )
2415 .into_response();
2416 }
2417 Err(e) => {
2418 tracing::error!("Failed to load authentication state: {:?}", e);
2419 return (
2420 StatusCode::INTERNAL_SERVER_ERROR,
2421 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2422 )
2423 .into_response();
2424 }
2425 };
2426
2427 let credential: webauthn_rs::prelude::PublicKeyCredential =
2428 match serde_json::from_value(form.credential.clone()) {
2429 Ok(c) => c,
2430 Err(e) => {
2431 tracing::error!("Failed to parse credential: {:?}", e);
2432 return (
2433 StatusCode::BAD_REQUEST,
2434 Json(serde_json::json!({
2435 "error": "invalid_request",
2436 "error_description": "Invalid credential format."
2437 })),
2438 )
2439 .into_response();
2440 }
2441 };
2442
2443 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2444 Ok(w) => w,
2445 Err(e) => {
2446 tracing::error!("Failed to create WebAuthn config: {:?}", e);
2447 return (
2448 StatusCode::INTERNAL_SERVER_ERROR,
2449 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2450 )
2451 .into_response();
2452 }
2453 };
2454
2455 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
2456 Ok(r) => r,
2457 Err(e) => {
2458 tracing::warn!("Passkey authentication failed: {:?}", e);
2459 return (
2460 StatusCode::FORBIDDEN,
2461 Json(serde_json::json!({
2462 "error": "access_denied",
2463 "error_description": "Passkey authentication failed."
2464 })),
2465 )
2466 .into_response();
2467 }
2468 };
2469
2470 let _ = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await;
2471
2472 if let Err(e) = crate::auth::webauthn::update_passkey_counter(
2473 &state.db,
2474 credential.id.as_ref(),
2475 auth_result.counter(),
2476 )
2477 .await
2478 {
2479 tracing::warn!("Failed to update passkey counter: {:?}", e);
2480 }
2481
2482 let has_totp = crate::api::server::has_totp_enabled_db(&state.db, &did).await;
2483 if has_totp {
2484 let device_cookie = extract_device_cookie(&headers);
2485 let device_is_trusted = if let Some(ref dev_id) = device_cookie {
2486 crate::api::server::is_device_trusted(&state.db, dev_id, &did).await
2487 } else {
2488 false
2489 };
2490
2491 if device_is_trusted {
2492 if let Some(ref dev_id) = device_cookie {
2493 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await;
2494 }
2495 } else {
2496 let user = match sqlx::query!(
2497 r#"SELECT id, preferred_comms_channel as "preferred_comms_channel: CommsChannel" FROM users WHERE did = $1"#,
2498 did
2499 )
2500 .fetch_optional(&state.db)
2501 .await
2502 {
2503 Ok(Some(u)) => u,
2504 _ => {
2505 return (
2506 StatusCode::INTERNAL_SERVER_ERROR,
2507 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2508 )
2509 .into_response();
2510 }
2511 };
2512
2513 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
2514 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await {
2515 Ok(challenge) => {
2516 if let Err(e) =
2517 enqueue_2fa_code(&state.db, user.id, &challenge.code, &pds_hostname).await
2518 {
2519 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
2520 }
2521 let channel_name = channel_display_name(user.preferred_comms_channel);
2522 let redirect_url = format!(
2523 "/#/oauth/2fa?request_uri={}&channel={}",
2524 url_encode(&form.request_uri),
2525 url_encode(channel_name)
2526 );
2527 return (
2528 StatusCode::OK,
2529 Json(serde_json::json!({
2530 "next": "2fa",
2531 "redirect": redirect_url
2532 })),
2533 )
2534 .into_response();
2535 }
2536 Err(_) => {
2537 return (
2538 StatusCode::INTERNAL_SERVER_ERROR,
2539 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2540 )
2541 .into_response();
2542 }
2543 }
2544 }
2545 }
2546
2547 let redirect_url = format!(
2548 "/#/oauth/consent?request_uri={}",
2549 url_encode(&form.request_uri)
2550 );
2551 (
2552 StatusCode::OK,
2553 Json(serde_json::json!({
2554 "next": "consent",
2555 "redirect": redirect_url
2556 })),
2557 )
2558 .into_response()
2559}