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 let Some(bare_handle) =
430 normalized_username.strip_suffix(&format!(".{}", pds_hostname))
431 {
432 bare_handle.to_string()
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, 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 let password_valid = match bcrypt::verify(&form.password, &user.password_hash) {
483 Ok(valid) => valid,
484 Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
485 };
486 if !password_valid {
487 return show_login_error("Invalid handle/email or password.", json_response);
488 }
489 let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await;
490 if has_totp {
491 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
492 .await
493 .is_err()
494 {
495 return show_login_error("An error occurred. Please try again.", json_response);
496 }
497 if json_response {
498 return Json(serde_json::json!({
499 "needs_totp": true
500 }))
501 .into_response();
502 }
503 return redirect_see_other(&format!(
504 "/#/oauth/totp?request_uri={}",
505 url_encode(&form.request_uri)
506 ));
507 }
508 if user.two_factor_enabled {
509 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
510 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await {
511 Ok(challenge) => {
512 let hostname =
513 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
514 if let Err(e) =
515 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
516 {
517 tracing::warn!(
518 did = %user.did,
519 error = %e,
520 "Failed to enqueue 2FA notification"
521 );
522 }
523 let channel_name = channel_display_name(user.preferred_comms_channel);
524 if json_response {
525 return Json(serde_json::json!({
526 "needs_2fa": true,
527 "channel": channel_name
528 }))
529 .into_response();
530 }
531 return redirect_see_other(&format!(
532 "/#/oauth/2fa?request_uri={}&channel={}",
533 url_encode(&form.request_uri),
534 url_encode(channel_name)
535 ));
536 }
537 Err(_) => {
538 return show_login_error("An error occurred. Please try again.", json_response);
539 }
540 }
541 }
542 let mut device_id: Option<String> = extract_device_cookie(&headers);
543 let mut new_cookie: Option<String> = None;
544 if form.remember_device {
545 let final_device_id = if let Some(existing_id) = &device_id {
546 existing_id.clone()
547 } else {
548 let new_id = DeviceId::generate();
549 let device_data = DeviceData {
550 session_id: SessionId::generate().0,
551 user_agent: extract_user_agent(&headers),
552 ip_address: extract_client_ip(&headers),
553 last_seen_at: Utc::now(),
554 };
555 if db::create_device(&state.db, &new_id.0, &device_data)
556 .await
557 .is_ok()
558 {
559 new_cookie = Some(make_device_cookie(&new_id.0));
560 device_id = Some(new_id.0.clone());
561 }
562 new_id.0
563 };
564 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await;
565 }
566 if db::set_authorization_did(
567 &state.db,
568 &form.request_uri,
569 &user.did,
570 device_id.as_deref(),
571 )
572 .await
573 .is_err()
574 {
575 return show_login_error("An error occurred. Please try again.", json_response);
576 }
577 let requested_scope_str = request_data
578 .parameters
579 .scope
580 .as_deref()
581 .unwrap_or("atproto");
582 let requested_scopes: Vec<String> = requested_scope_str
583 .split_whitespace()
584 .map(|s| s.to_string())
585 .collect();
586 let needs_consent = db::should_show_consent(
587 &state.db,
588 &user.did,
589 &request_data.parameters.client_id,
590 &requested_scopes,
591 )
592 .await
593 .unwrap_or(true);
594 if needs_consent {
595 let consent_url = format!(
596 "/#/oauth/consent?request_uri={}",
597 url_encode(&form.request_uri)
598 );
599 if json_response {
600 if let Some(cookie) = new_cookie {
601 return (
602 StatusCode::OK,
603 [(SET_COOKIE, cookie)],
604 Json(serde_json::json!({"redirect_uri": consent_url})),
605 )
606 .into_response();
607 }
608 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
609 }
610 if let Some(cookie) = new_cookie {
611 return (
612 StatusCode::SEE_OTHER,
613 [(SET_COOKIE, cookie), (LOCATION, consent_url)],
614 )
615 .into_response();
616 }
617 return redirect_see_other(&consent_url);
618 }
619 let code = Code::generate();
620 if db::update_authorization_request(
621 &state.db,
622 &form.request_uri,
623 &user.did,
624 device_id.as_deref(),
625 &code.0,
626 )
627 .await
628 .is_err()
629 {
630 return show_login_error("An error occurred. Please try again.", json_response);
631 }
632 let redirect_url = build_success_redirect(
633 &request_data.parameters.redirect_uri,
634 &code.0,
635 request_data.parameters.state.as_deref(),
636 request_data.parameters.response_mode.as_deref(),
637 );
638 if json_response {
639 if let Some(cookie) = new_cookie {
640 (
641 StatusCode::OK,
642 [(SET_COOKIE, cookie)],
643 Json(serde_json::json!({"redirect_uri": redirect_url})),
644 )
645 .into_response()
646 } else {
647 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response()
648 }
649 } else if let Some(cookie) = new_cookie {
650 (
651 StatusCode::SEE_OTHER,
652 [(SET_COOKIE, cookie), (LOCATION, redirect_url)],
653 )
654 .into_response()
655 } else {
656 redirect_see_other(&redirect_url)
657 }
658}
659
660pub async fn authorize_select(
661 State(state): State<AppState>,
662 headers: HeaderMap,
663 Json(form): Json<AuthorizeSelectSubmit>,
664) -> Response {
665 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
666 (
667 status,
668 Json(serde_json::json!({
669 "error": error,
670 "error_description": description
671 })),
672 )
673 .into_response()
674 };
675 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
676 Ok(Some(data)) => data,
677 Ok(None) => {
678 return json_error(
679 StatusCode::BAD_REQUEST,
680 "invalid_request",
681 "Invalid or expired request_uri. Please start a new authorization request.",
682 );
683 }
684 Err(_) => {
685 return json_error(
686 StatusCode::INTERNAL_SERVER_ERROR,
687 "server_error",
688 "An error occurred. Please try again.",
689 );
690 }
691 };
692 if request_data.expires_at < Utc::now() {
693 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
694 return json_error(
695 StatusCode::BAD_REQUEST,
696 "invalid_request",
697 "Authorization request has expired. Please start a new request.",
698 );
699 }
700 let device_id = match extract_device_cookie(&headers) {
701 Some(id) => id,
702 None => {
703 return json_error(
704 StatusCode::BAD_REQUEST,
705 "invalid_request",
706 "No device session found. Please sign in.",
707 );
708 }
709 };
710 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await {
711 Ok(valid) => valid,
712 Err(_) => {
713 return json_error(
714 StatusCode::INTERNAL_SERVER_ERROR,
715 "server_error",
716 "An error occurred. Please try again.",
717 );
718 }
719 };
720 if !account_valid {
721 return json_error(
722 StatusCode::FORBIDDEN,
723 "access_denied",
724 "This account is not available on this device. Please sign in.",
725 );
726 }
727 let user = match sqlx::query!(
728 r#"
729 SELECT id, two_factor_enabled,
730 preferred_comms_channel as "preferred_comms_channel: CommsChannel",
731 email_verified, discord_verified, telegram_verified, signal_verified
732 FROM users
733 WHERE did = $1
734 "#,
735 form.did
736 )
737 .fetch_optional(&state.db)
738 .await
739 {
740 Ok(Some(u)) => u,
741 Ok(None) => {
742 return json_error(
743 StatusCode::FORBIDDEN,
744 "access_denied",
745 "Account not found. Please sign in.",
746 );
747 }
748 Err(_) => {
749 return json_error(
750 StatusCode::INTERNAL_SERVER_ERROR,
751 "server_error",
752 "An error occurred. Please try again.",
753 );
754 }
755 };
756 let is_verified = user.email_verified
757 || user.discord_verified
758 || user.telegram_verified
759 || user.signal_verified;
760 if !is_verified {
761 return json_error(
762 StatusCode::FORBIDDEN,
763 "access_denied",
764 "Please verify your account before logging in.",
765 );
766 }
767 let has_totp = crate::api::server::has_totp_enabled(&state, &form.did).await;
768 if has_totp {
769 if db::set_authorization_did(&state.db, &form.request_uri, &form.did, Some(&device_id))
770 .await
771 .is_err()
772 {
773 return json_error(
774 StatusCode::INTERNAL_SERVER_ERROR,
775 "server_error",
776 "An error occurred. Please try again.",
777 );
778 }
779 return Json(serde_json::json!({
780 "needs_totp": true
781 }))
782 .into_response();
783 }
784 if user.two_factor_enabled {
785 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
786 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await {
787 Ok(challenge) => {
788 let hostname =
789 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
790 if let Err(e) =
791 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
792 {
793 tracing::warn!(
794 did = %form.did,
795 error = %e,
796 "Failed to enqueue 2FA notification"
797 );
798 }
799 let channel_name = channel_display_name(user.preferred_comms_channel);
800 return Json(serde_json::json!({
801 "needs_2fa": true,
802 "channel": channel_name
803 }))
804 .into_response();
805 }
806 Err(_) => {
807 return json_error(
808 StatusCode::INTERNAL_SERVER_ERROR,
809 "server_error",
810 "An error occurred. Please try again.",
811 );
812 }
813 }
814 }
815 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await;
816 let code = Code::generate();
817 if db::update_authorization_request(
818 &state.db,
819 &form.request_uri,
820 &form.did,
821 Some(&device_id),
822 &code.0,
823 )
824 .await
825 .is_err()
826 {
827 return json_error(
828 StatusCode::INTERNAL_SERVER_ERROR,
829 "server_error",
830 "An error occurred. Please try again.",
831 );
832 }
833 let redirect_url = build_success_redirect(
834 &request_data.parameters.redirect_uri,
835 &code.0,
836 request_data.parameters.state.as_deref(),
837 request_data.parameters.response_mode.as_deref(),
838 );
839 Json(serde_json::json!({
840 "redirect_uri": redirect_url
841 }))
842 .into_response()
843}
844
845fn build_success_redirect(
846 redirect_uri: &str,
847 code: &str,
848 state: Option<&str>,
849 response_mode: Option<&str>,
850) -> String {
851 let mut redirect_url = redirect_uri.to_string();
852 let use_fragment = response_mode == Some("fragment");
853 let separator = if use_fragment {
854 '#'
855 } else if redirect_url.contains('?') {
856 '&'
857 } else {
858 '?'
859 };
860 redirect_url.push(separator);
861 redirect_url.push_str(&format!("code={}", url_encode(code)));
862 if let Some(req_state) = state {
863 redirect_url.push_str(&format!("&state={}", url_encode(req_state)));
864 }
865 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
866 redirect_url.push_str(&format!(
867 "&iss={}",
868 url_encode(&format!("https://{}", pds_hostname))
869 ));
870 redirect_url
871}
872
873#[derive(Debug, Serialize)]
874pub struct AuthorizeDenyResponse {
875 pub error: String,
876 pub error_description: String,
877}
878
879pub async fn authorize_deny(
880 State(state): State<AppState>,
881 Json(form): Json<AuthorizeDenyForm>,
882) -> Response {
883 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
884 Ok(Some(data)) => data,
885 Ok(None) => {
886 return (
887 StatusCode::BAD_REQUEST,
888 Json(serde_json::json!({
889 "error": "invalid_request",
890 "error_description": "Invalid request_uri"
891 })),
892 )
893 .into_response();
894 }
895 Err(_) => {
896 return (
897 StatusCode::INTERNAL_SERVER_ERROR,
898 Json(serde_json::json!({
899 "error": "server_error",
900 "error_description": "An error occurred"
901 })),
902 )
903 .into_response();
904 }
905 };
906 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
907 let redirect_uri = &request_data.parameters.redirect_uri;
908 let mut redirect_url = redirect_uri.to_string();
909 let separator = if redirect_url.contains('?') { '&' } else { '?' };
910 redirect_url.push(separator);
911 redirect_url.push_str("error=access_denied");
912 redirect_url.push_str("&error_description=User%20denied%20the%20request");
913 if let Some(state) = &request_data.parameters.state {
914 redirect_url.push_str(&format!("&state={}", url_encode(state)));
915 }
916 Json(serde_json::json!({
917 "redirect_uri": redirect_url
918 }))
919 .into_response()
920}
921
922#[derive(Debug, Deserialize)]
923pub struct AuthorizeDenyForm {
924 pub request_uri: String,
925}
926
927#[derive(Debug, Deserialize)]
928pub struct Authorize2faQuery {
929 pub request_uri: String,
930 pub channel: Option<String>,
931}
932
933#[derive(Debug, Deserialize)]
934pub struct Authorize2faSubmit {
935 pub request_uri: String,
936 pub code: String,
937}
938
939const MAX_2FA_ATTEMPTS: i32 = 5;
940
941pub async fn authorize_2fa_get(
942 State(state): State<AppState>,
943 Query(query): Query<Authorize2faQuery>,
944) -> Response {
945 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await {
946 Ok(Some(c)) => c,
947 Ok(None) => {
948 return redirect_to_frontend_error(
949 "invalid_request",
950 "No 2FA challenge found. Please start over.",
951 );
952 }
953 Err(_) => {
954 return redirect_to_frontend_error(
955 "server_error",
956 "An error occurred. Please try again.",
957 );
958 }
959 };
960 if challenge.expires_at < Utc::now() {
961 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
962 return redirect_to_frontend_error(
963 "invalid_request",
964 "2FA code has expired. Please start over.",
965 );
966 }
967 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
968 Ok(Some(d)) => d,
969 Ok(None) => {
970 return redirect_to_frontend_error(
971 "invalid_request",
972 "Authorization request not found. Please start over.",
973 );
974 }
975 Err(_) => {
976 return redirect_to_frontend_error(
977 "server_error",
978 "An error occurred. Please try again.",
979 );
980 }
981 };
982 let channel = query.channel.as_deref().unwrap_or("email");
983 redirect_see_other(&format!(
984 "/#/oauth/2fa?request_uri={}&channel={}",
985 url_encode(&query.request_uri),
986 url_encode(channel)
987 ))
988}
989
990#[derive(Debug, Serialize)]
991pub struct ScopeInfo {
992 pub scope: String,
993 pub category: String,
994 pub required: bool,
995 pub description: String,
996 pub display_name: String,
997 pub granted: Option<bool>,
998}
999
1000#[derive(Debug, Serialize)]
1001pub struct ConsentResponse {
1002 pub request_uri: String,
1003 pub client_id: String,
1004 pub client_name: Option<String>,
1005 pub client_uri: Option<String>,
1006 pub logo_uri: Option<String>,
1007 pub scopes: Vec<ScopeInfo>,
1008 pub show_consent: bool,
1009 pub did: String,
1010}
1011
1012#[derive(Debug, Deserialize)]
1013pub struct ConsentQuery {
1014 pub request_uri: String,
1015}
1016
1017#[derive(Debug, Deserialize)]
1018pub struct ConsentSubmit {
1019 pub request_uri: String,
1020 pub approved_scopes: Vec<String>,
1021 pub remember: bool,
1022}
1023
1024pub async fn consent_get(
1025 State(state): State<AppState>,
1026 Query(query): Query<ConsentQuery>,
1027) -> Response {
1028 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
1029 Ok(Some(data)) => data,
1030 Ok(None) => {
1031 return (
1032 StatusCode::BAD_REQUEST,
1033 Json(serde_json::json!({
1034 "error": "invalid_request",
1035 "error_description": "Invalid or expired request_uri"
1036 })),
1037 )
1038 .into_response();
1039 }
1040 Err(e) => {
1041 return (
1042 StatusCode::INTERNAL_SERVER_ERROR,
1043 Json(serde_json::json!({
1044 "error": "server_error",
1045 "error_description": format!("Database error: {:?}", e)
1046 })),
1047 )
1048 .into_response();
1049 }
1050 };
1051 if request_data.expires_at < Utc::now() {
1052 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await;
1053 return (
1054 StatusCode::BAD_REQUEST,
1055 Json(serde_json::json!({
1056 "error": "invalid_request",
1057 "error_description": "Authorization request has expired"
1058 })),
1059 )
1060 .into_response();
1061 }
1062 let did = match &request_data.did {
1063 Some(d) => d.clone(),
1064 None => {
1065 return (
1066 StatusCode::FORBIDDEN,
1067 Json(serde_json::json!({
1068 "error": "access_denied",
1069 "error_description": "Not authenticated"
1070 })),
1071 )
1072 .into_response();
1073 }
1074 };
1075 let client_cache = ClientMetadataCache::new(3600);
1076 let client_metadata = client_cache
1077 .get(&request_data.parameters.client_id)
1078 .await
1079 .ok();
1080 let requested_scope_str = request_data
1081 .parameters
1082 .scope
1083 .as_deref()
1084 .unwrap_or("atproto");
1085 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect();
1086 let preferences =
1087 db::get_scope_preferences(&state.db, &did, &request_data.parameters.client_id)
1088 .await
1089 .unwrap_or_default();
1090 let pref_map: std::collections::HashMap<_, _> = preferences
1091 .iter()
1092 .map(|p| (p.scope.as_str(), p.granted))
1093 .collect();
1094 let requested_scope_strings: Vec<String> =
1095 requested_scopes.iter().map(|s| s.to_string()).collect();
1096 let show_consent = db::should_show_consent(
1097 &state.db,
1098 &did,
1099 &request_data.parameters.client_id,
1100 &requested_scope_strings,
1101 )
1102 .await
1103 .unwrap_or(true);
1104 let mut scopes = Vec::new();
1105 for scope in &requested_scopes {
1106 let (category, required, description, display_name) =
1107 if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) {
1108 (
1109 def.category.display_name().to_string(),
1110 def.required,
1111 def.description.to_string(),
1112 def.display_name.to_string(),
1113 )
1114 } else if scope.starts_with("ref:") {
1115 (
1116 "Reference".to_string(),
1117 false,
1118 "Referenced scope".to_string(),
1119 scope.to_string(),
1120 )
1121 } else {
1122 (
1123 "Other".to_string(),
1124 false,
1125 format!("Access to {}", scope),
1126 scope.to_string(),
1127 )
1128 };
1129 let granted = pref_map.get(*scope).copied();
1130 scopes.push(ScopeInfo {
1131 scope: scope.to_string(),
1132 category,
1133 required,
1134 description,
1135 display_name,
1136 granted,
1137 });
1138 }
1139 Json(ConsentResponse {
1140 request_uri: query.request_uri.clone(),
1141 client_id: request_data.parameters.client_id.clone(),
1142 client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()),
1143 client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()),
1144 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()),
1145 scopes,
1146 show_consent,
1147 did,
1148 })
1149 .into_response()
1150}
1151
1152pub async fn consent_post(
1153 State(state): State<AppState>,
1154 Json(form): Json<ConsentSubmit>,
1155) -> Response {
1156 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1157 Ok(Some(data)) => data,
1158 Ok(None) => {
1159 return (
1160 StatusCode::BAD_REQUEST,
1161 Json(serde_json::json!({
1162 "error": "invalid_request",
1163 "error_description": "Invalid or expired request_uri"
1164 })),
1165 )
1166 .into_response();
1167 }
1168 Err(e) => {
1169 return (
1170 StatusCode::INTERNAL_SERVER_ERROR,
1171 Json(serde_json::json!({
1172 "error": "server_error",
1173 "error_description": format!("Database error: {:?}", e)
1174 })),
1175 )
1176 .into_response();
1177 }
1178 };
1179 if request_data.expires_at < Utc::now() {
1180 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1181 return (
1182 StatusCode::BAD_REQUEST,
1183 Json(serde_json::json!({
1184 "error": "invalid_request",
1185 "error_description": "Authorization request has expired"
1186 })),
1187 )
1188 .into_response();
1189 }
1190 let did = match &request_data.did {
1191 Some(d) => d.clone(),
1192 None => {
1193 return (
1194 StatusCode::FORBIDDEN,
1195 Json(serde_json::json!({
1196 "error": "access_denied",
1197 "error_description": "Not authenticated"
1198 })),
1199 )
1200 .into_response();
1201 }
1202 };
1203 let requested_scope_str = request_data
1204 .parameters
1205 .scope
1206 .as_deref()
1207 .unwrap_or("atproto");
1208 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect();
1209 let has_granular_scopes = requested_scopes.iter().any(|s| {
1210 s.starts_with("repo:")
1211 || s.starts_with("blob:")
1212 || s.starts_with("rpc:")
1213 || s.starts_with("account:")
1214 || s.starts_with("identity:")
1215 });
1216 let user_denied_some_granular = has_granular_scopes
1217 && requested_scopes
1218 .iter()
1219 .filter(|s| {
1220 s.starts_with("repo:")
1221 || s.starts_with("blob:")
1222 || s.starts_with("rpc:")
1223 || s.starts_with("account:")
1224 || s.starts_with("identity:")
1225 })
1226 .any(|s| !form.approved_scopes.contains(&s.to_string()));
1227 let atproto_was_requested = requested_scopes.contains(&"atproto");
1228 if atproto_was_requested
1229 && !has_granular_scopes
1230 && !form.approved_scopes.contains(&"atproto".to_string())
1231 {
1232 return (
1233 StatusCode::BAD_REQUEST,
1234 Json(serde_json::json!({
1235 "error": "invalid_request",
1236 "error_description": "The atproto scope was requested and must be approved"
1237 })),
1238 )
1239 .into_response();
1240 }
1241 let final_approved: Vec<String> = if user_denied_some_granular {
1242 form.approved_scopes
1243 .iter()
1244 .filter(|s| *s != "atproto")
1245 .cloned()
1246 .collect()
1247 } else {
1248 form.approved_scopes.clone()
1249 };
1250 if final_approved.is_empty() {
1251 return (
1252 StatusCode::BAD_REQUEST,
1253 Json(serde_json::json!({
1254 "error": "invalid_request",
1255 "error_description": "At least one scope must be approved"
1256 })),
1257 )
1258 .into_response();
1259 }
1260 let approved_scope_str = final_approved.join(" ");
1261 let has_valid_scope = final_approved.iter().all(|s| {
1262 s == "atproto"
1263 || s == "transition:generic"
1264 || s == "transition:chat.bsky"
1265 || s == "transition:email"
1266 || s.starts_with("repo:")
1267 || s.starts_with("blob:")
1268 || s.starts_with("rpc:")
1269 || s.starts_with("account:")
1270 || s.starts_with("include:")
1271 });
1272 if !has_valid_scope {
1273 return (
1274 StatusCode::BAD_REQUEST,
1275 Json(serde_json::json!({
1276 "error": "invalid_request",
1277 "error_description": "Invalid scope format"
1278 })),
1279 )
1280 .into_response();
1281 }
1282 if form.remember {
1283 let preferences: Vec<db::ScopePreference> = requested_scopes
1284 .iter()
1285 .map(|s| db::ScopePreference {
1286 scope: s.to_string(),
1287 granted: form.approved_scopes.contains(&s.to_string()),
1288 })
1289 .collect();
1290 let _ = db::upsert_scope_preferences(
1291 &state.db,
1292 &did,
1293 &request_data.parameters.client_id,
1294 &preferences,
1295 )
1296 .await;
1297 }
1298 if let Err(e) =
1299 db::update_request_scope(&state.db, &form.request_uri, &approved_scope_str).await
1300 {
1301 tracing::warn!("Failed to update request scope: {:?}", e);
1302 }
1303 let code = Code::generate();
1304 if db::update_authorization_request(
1305 &state.db,
1306 &form.request_uri,
1307 &did,
1308 request_data.device_id.as_deref(),
1309 &code.0,
1310 )
1311 .await
1312 .is_err()
1313 {
1314 return (
1315 StatusCode::INTERNAL_SERVER_ERROR,
1316 Json(serde_json::json!({
1317 "error": "server_error",
1318 "error_description": "Failed to complete authorization"
1319 })),
1320 )
1321 .into_response();
1322 }
1323 let redirect_url = build_success_redirect(
1324 &request_data.parameters.redirect_uri,
1325 &code.0,
1326 request_data.parameters.state.as_deref(),
1327 request_data.parameters.response_mode.as_deref(),
1328 );
1329 Json(serde_json::json!({
1330 "redirect_uri": redirect_url
1331 }))
1332 .into_response()
1333}
1334
1335pub async fn authorize_2fa_post(
1336 State(state): State<AppState>,
1337 headers: HeaderMap,
1338 Json(form): Json<Authorize2faSubmit>,
1339) -> Response {
1340 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
1341 (
1342 status,
1343 Json(serde_json::json!({
1344 "error": error,
1345 "error_description": description
1346 })),
1347 )
1348 .into_response()
1349 };
1350 let client_ip = extract_client_ip(&headers);
1351 if !state
1352 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1353 .await
1354 {
1355 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded");
1356 return json_error(
1357 StatusCode::TOO_MANY_REQUESTS,
1358 "RateLimitExceeded",
1359 "Too many attempts. Please try again later.",
1360 );
1361 }
1362 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1363 Ok(Some(d)) => d,
1364 Ok(None) => {
1365 return json_error(
1366 StatusCode::BAD_REQUEST,
1367 "invalid_request",
1368 "Authorization request not found.",
1369 );
1370 }
1371 Err(_) => {
1372 return json_error(
1373 StatusCode::INTERNAL_SERVER_ERROR,
1374 "server_error",
1375 "An error occurred.",
1376 );
1377 }
1378 };
1379 if request_data.expires_at < Utc::now() {
1380 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1381 return json_error(
1382 StatusCode::BAD_REQUEST,
1383 "invalid_request",
1384 "Authorization request has expired.",
1385 );
1386 }
1387 let challenge = db::get_2fa_challenge(&state.db, &form.request_uri)
1388 .await
1389 .ok()
1390 .flatten();
1391 if let Some(challenge) = challenge {
1392 if challenge.expires_at < Utc::now() {
1393 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1394 return json_error(
1395 StatusCode::BAD_REQUEST,
1396 "invalid_request",
1397 "2FA code has expired. Please start over.",
1398 );
1399 }
1400 if challenge.attempts >= MAX_2FA_ATTEMPTS {
1401 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1402 return json_error(
1403 StatusCode::FORBIDDEN,
1404 "access_denied",
1405 "Too many failed attempts. Please start over.",
1406 );
1407 }
1408 let code_valid: bool = form
1409 .code
1410 .trim()
1411 .as_bytes()
1412 .ct_eq(challenge.code.as_bytes())
1413 .into();
1414 if !code_valid {
1415 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await;
1416 return json_error(
1417 StatusCode::FORBIDDEN,
1418 "invalid_code",
1419 "Invalid verification code. Please try again.",
1420 );
1421 }
1422 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1423 let code = Code::generate();
1424 let device_id = extract_device_cookie(&headers);
1425 if db::update_authorization_request(
1426 &state.db,
1427 &form.request_uri,
1428 &challenge.did,
1429 device_id.as_deref(),
1430 &code.0,
1431 )
1432 .await
1433 .is_err()
1434 {
1435 return json_error(
1436 StatusCode::INTERNAL_SERVER_ERROR,
1437 "server_error",
1438 "An error occurred. Please try again.",
1439 );
1440 }
1441 let redirect_url = build_success_redirect(
1442 &request_data.parameters.redirect_uri,
1443 &code.0,
1444 request_data.parameters.state.as_deref(),
1445 request_data.parameters.response_mode.as_deref(),
1446 );
1447 return Json(serde_json::json!({
1448 "redirect_uri": redirect_url
1449 }))
1450 .into_response();
1451 }
1452 let did = match &request_data.did {
1453 Some(d) => d.clone(),
1454 None => {
1455 return json_error(
1456 StatusCode::BAD_REQUEST,
1457 "invalid_request",
1458 "No 2FA challenge found. Please start over.",
1459 );
1460 }
1461 };
1462 if !crate::api::server::has_totp_enabled(&state, &did).await {
1463 return json_error(
1464 StatusCode::BAD_REQUEST,
1465 "invalid_request",
1466 "No 2FA challenge found. Please start over.",
1467 );
1468 }
1469 let totp_valid =
1470 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
1471 if !totp_valid {
1472 return json_error(
1473 StatusCode::FORBIDDEN,
1474 "invalid_code",
1475 "Invalid verification code. Please try again.",
1476 );
1477 }
1478 let requested_scope_str = request_data
1479 .parameters
1480 .scope
1481 .as_deref()
1482 .unwrap_or("atproto");
1483 let requested_scopes: Vec<String> = requested_scope_str
1484 .split_whitespace()
1485 .map(|s| s.to_string())
1486 .collect();
1487 let needs_consent = db::should_show_consent(
1488 &state.db,
1489 &did,
1490 &request_data.parameters.client_id,
1491 &requested_scopes,
1492 )
1493 .await
1494 .unwrap_or(true);
1495 if needs_consent {
1496 let consent_url = format!(
1497 "/#/oauth/consent?request_uri={}",
1498 url_encode(&form.request_uri)
1499 );
1500 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
1501 }
1502 let code = Code::generate();
1503 let device_id = extract_device_cookie(&headers);
1504 if db::update_authorization_request(
1505 &state.db,
1506 &form.request_uri,
1507 &did,
1508 device_id.as_deref(),
1509 &code.0,
1510 )
1511 .await
1512 .is_err()
1513 {
1514 return json_error(
1515 StatusCode::INTERNAL_SERVER_ERROR,
1516 "server_error",
1517 "An error occurred. Please try again.",
1518 );
1519 }
1520 let redirect_url = build_success_redirect(
1521 &request_data.parameters.redirect_uri,
1522 &code.0,
1523 request_data.parameters.state.as_deref(),
1524 request_data.parameters.response_mode.as_deref(),
1525 );
1526 Json(serde_json::json!({
1527 "redirect_uri": redirect_url
1528 }))
1529 .into_response()
1530}
1531
1532#[derive(Debug, Deserialize)]
1533#[serde(rename_all = "camelCase")]
1534pub struct CheckPasskeysQuery {
1535 pub identifier: String,
1536}
1537
1538#[derive(Debug, Serialize)]
1539#[serde(rename_all = "camelCase")]
1540pub struct CheckPasskeysResponse {
1541 pub has_passkeys: bool,
1542}
1543
1544pub async fn check_user_has_passkeys(
1545 State(state): State<AppState>,
1546 Query(query): Query<CheckPasskeysQuery>,
1547) -> Response {
1548 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1549 let normalized_identifier = query.identifier.trim();
1550 let normalized_identifier = normalized_identifier
1551 .strip_prefix('@')
1552 .unwrap_or(normalized_identifier);
1553 let normalized_identifier = if let Some(bare_handle) =
1554 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname))
1555 {
1556 bare_handle.to_string()
1557 } else {
1558 normalized_identifier.to_string()
1559 };
1560
1561 let user = sqlx::query!(
1562 "SELECT did FROM users WHERE handle = $1 OR email = $1",
1563 normalized_identifier
1564 )
1565 .fetch_optional(&state.db)
1566 .await;
1567
1568 let has_passkeys = match user {
1569 Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await,
1570 _ => false,
1571 };
1572
1573 Json(CheckPasskeysResponse { has_passkeys }).into_response()
1574}
1575
1576#[derive(Debug, Serialize)]
1577#[serde(rename_all = "camelCase")]
1578pub struct SecurityStatusResponse {
1579 pub has_passkeys: bool,
1580 pub has_totp: bool,
1581}
1582
1583pub async fn check_user_security_status(
1584 State(state): State<AppState>,
1585 Query(query): Query<CheckPasskeysQuery>,
1586) -> Response {
1587 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1588 let normalized_identifier = query.identifier.trim();
1589 let normalized_identifier = normalized_identifier
1590 .strip_prefix('@')
1591 .unwrap_or(normalized_identifier);
1592 let normalized_identifier = if let Some(bare_handle) =
1593 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname))
1594 {
1595 bare_handle.to_string()
1596 } else {
1597 normalized_identifier.to_string()
1598 };
1599
1600 let user = sqlx::query!(
1601 "SELECT did FROM users WHERE handle = $1 OR email = $1",
1602 normalized_identifier
1603 )
1604 .fetch_optional(&state.db)
1605 .await;
1606
1607 let (has_passkeys, has_totp) = match user {
1608 Ok(Some(u)) => {
1609 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await;
1610 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await;
1611 (passkeys, totp)
1612 }
1613 _ => (false, false),
1614 };
1615
1616 Json(SecurityStatusResponse {
1617 has_passkeys,
1618 has_totp,
1619 })
1620 .into_response()
1621}
1622
1623#[derive(Debug, Deserialize)]
1624pub struct PasskeyStartInput {
1625 pub request_uri: String,
1626 pub identifier: String,
1627}
1628
1629#[derive(Debug, Serialize)]
1630#[serde(rename_all = "camelCase")]
1631pub struct PasskeyStartResponse {
1632 pub options: serde_json::Value,
1633}
1634
1635pub async fn passkey_start(
1636 State(state): State<AppState>,
1637 headers: HeaderMap,
1638 Json(form): Json<PasskeyStartInput>,
1639) -> Response {
1640 let client_ip = extract_client_ip(&headers);
1641
1642 if !state
1643 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1644 .await
1645 {
1646 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded");
1647 return (
1648 StatusCode::TOO_MANY_REQUESTS,
1649 Json(serde_json::json!({
1650 "error": "RateLimitExceeded",
1651 "error_description": "Too many login attempts. Please try again later."
1652 })),
1653 )
1654 .into_response();
1655 }
1656
1657 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1658 Ok(Some(data)) => data,
1659 Ok(None) => {
1660 return (
1661 StatusCode::BAD_REQUEST,
1662 Json(serde_json::json!({
1663 "error": "invalid_request",
1664 "error_description": "Invalid or expired request_uri."
1665 })),
1666 )
1667 .into_response();
1668 }
1669 Err(_) => {
1670 return (
1671 StatusCode::INTERNAL_SERVER_ERROR,
1672 Json(serde_json::json!({
1673 "error": "server_error",
1674 "error_description": "An error occurred."
1675 })),
1676 )
1677 .into_response();
1678 }
1679 };
1680
1681 if request_data.expires_at < Utc::now() {
1682 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1683 return (
1684 StatusCode::BAD_REQUEST,
1685 Json(serde_json::json!({
1686 "error": "invalid_request",
1687 "error_description": "Authorization request has expired."
1688 })),
1689 )
1690 .into_response();
1691 }
1692
1693 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1694 let normalized_username = form.identifier.trim();
1695 let normalized_username = normalized_username
1696 .strip_prefix('@')
1697 .unwrap_or(normalized_username);
1698 let normalized_username = if let Some(bare_handle) =
1699 normalized_username.strip_suffix(&format!(".{}", pds_hostname))
1700 {
1701 bare_handle.to_string()
1702 } else {
1703 normalized_username.to_string()
1704 };
1705
1706 let user = match sqlx::query!(
1707 r#"
1708 SELECT did, deactivated_at, takedown_ref,
1709 email_verified, discord_verified, telegram_verified, signal_verified
1710 FROM users
1711 WHERE handle = $1 OR email = $1
1712 "#,
1713 normalized_username
1714 )
1715 .fetch_optional(&state.db)
1716 .await
1717 {
1718 Ok(Some(u)) => u,
1719 Ok(None) => {
1720 return (
1721 StatusCode::FORBIDDEN,
1722 Json(serde_json::json!({
1723 "error": "access_denied",
1724 "error_description": "User not found or has no passkeys."
1725 })),
1726 )
1727 .into_response();
1728 }
1729 Err(_) => {
1730 return (
1731 StatusCode::INTERNAL_SERVER_ERROR,
1732 Json(serde_json::json!({
1733 "error": "server_error",
1734 "error_description": "An error occurred."
1735 })),
1736 )
1737 .into_response();
1738 }
1739 };
1740
1741 if user.deactivated_at.is_some() {
1742 return (
1743 StatusCode::FORBIDDEN,
1744 Json(serde_json::json!({
1745 "error": "access_denied",
1746 "error_description": "This account has been deactivated."
1747 })),
1748 )
1749 .into_response();
1750 }
1751
1752 if user.takedown_ref.is_some() {
1753 return (
1754 StatusCode::FORBIDDEN,
1755 Json(serde_json::json!({
1756 "error": "access_denied",
1757 "error_description": "This account has been taken down."
1758 })),
1759 )
1760 .into_response();
1761 }
1762
1763 let is_verified = user.email_verified
1764 || user.discord_verified
1765 || user.telegram_verified
1766 || user.signal_verified;
1767
1768 if !is_verified {
1769 return (
1770 StatusCode::FORBIDDEN,
1771 Json(serde_json::json!({
1772 "error": "access_denied",
1773 "error_description": "Please verify your account before logging in."
1774 })),
1775 )
1776 .into_response();
1777 }
1778
1779 let stored_passkeys =
1780 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await {
1781 Ok(pks) => pks,
1782 Err(e) => {
1783 tracing::error!(error = %e, "Failed to get passkeys");
1784 return (
1785 StatusCode::INTERNAL_SERVER_ERROR,
1786 Json(serde_json::json!({
1787 "error": "server_error",
1788 "error_description": "An error occurred."
1789 })),
1790 )
1791 .into_response();
1792 }
1793 };
1794
1795 if stored_passkeys.is_empty() {
1796 return (
1797 StatusCode::FORBIDDEN,
1798 Json(serde_json::json!({
1799 "error": "access_denied",
1800 "error_description": "User not found or has no passkeys."
1801 })),
1802 )
1803 .into_response();
1804 }
1805
1806 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
1807 .iter()
1808 .filter_map(|sp| sp.to_security_key().ok())
1809 .collect();
1810
1811 if passkeys.is_empty() {
1812 return (
1813 StatusCode::INTERNAL_SERVER_ERROR,
1814 Json(serde_json::json!({
1815 "error": "server_error",
1816 "error_description": "Failed to load passkeys."
1817 })),
1818 )
1819 .into_response();
1820 }
1821
1822 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
1823 Ok(w) => w,
1824 Err(e) => {
1825 tracing::error!(error = %e, "Failed to create WebAuthn config");
1826 return (
1827 StatusCode::INTERNAL_SERVER_ERROR,
1828 Json(serde_json::json!({
1829 "error": "server_error",
1830 "error_description": "WebAuthn configuration failed."
1831 })),
1832 )
1833 .into_response();
1834 }
1835 };
1836
1837 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
1838 Ok(result) => result,
1839 Err(e) => {
1840 tracing::error!(error = %e, "Failed to start passkey authentication");
1841 return (
1842 StatusCode::INTERNAL_SERVER_ERROR,
1843 Json(serde_json::json!({
1844 "error": "server_error",
1845 "error_description": "Failed to start authentication."
1846 })),
1847 )
1848 .into_response();
1849 }
1850 };
1851
1852 if let Err(e) =
1853 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await
1854 {
1855 tracing::error!(error = %e, "Failed to save authentication state");
1856 return (
1857 StatusCode::INTERNAL_SERVER_ERROR,
1858 Json(serde_json::json!({
1859 "error": "server_error",
1860 "error_description": "An error occurred."
1861 })),
1862 )
1863 .into_response();
1864 }
1865
1866 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
1867 .await
1868 .is_err()
1869 {
1870 return (
1871 StatusCode::INTERNAL_SERVER_ERROR,
1872 Json(serde_json::json!({
1873 "error": "server_error",
1874 "error_description": "An error occurred."
1875 })),
1876 )
1877 .into_response();
1878 }
1879
1880 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
1881
1882 Json(PasskeyStartResponse { options }).into_response()
1883}
1884
1885#[derive(Debug, Deserialize)]
1886pub struct PasskeyFinishInput {
1887 pub request_uri: String,
1888 pub credential: serde_json::Value,
1889}
1890
1891pub async fn passkey_finish(
1892 State(state): State<AppState>,
1893 headers: HeaderMap,
1894 Json(form): Json<PasskeyFinishInput>,
1895) -> Response {
1896 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1897 Ok(Some(data)) => data,
1898 Ok(None) => {
1899 return (
1900 StatusCode::BAD_REQUEST,
1901 Json(serde_json::json!({
1902 "error": "invalid_request",
1903 "error_description": "Invalid or expired request_uri."
1904 })),
1905 )
1906 .into_response();
1907 }
1908 Err(_) => {
1909 return (
1910 StatusCode::INTERNAL_SERVER_ERROR,
1911 Json(serde_json::json!({
1912 "error": "server_error",
1913 "error_description": "An error occurred."
1914 })),
1915 )
1916 .into_response();
1917 }
1918 };
1919
1920 if request_data.expires_at < Utc::now() {
1921 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1922 return (
1923 StatusCode::BAD_REQUEST,
1924 Json(serde_json::json!({
1925 "error": "invalid_request",
1926 "error_description": "Authorization request has expired."
1927 })),
1928 )
1929 .into_response();
1930 }
1931
1932 let did = match request_data.did {
1933 Some(d) => d,
1934 None => {
1935 return (
1936 StatusCode::BAD_REQUEST,
1937 Json(serde_json::json!({
1938 "error": "invalid_request",
1939 "error_description": "No passkey authentication in progress."
1940 })),
1941 )
1942 .into_response();
1943 }
1944 };
1945
1946 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await {
1947 Ok(Some(s)) => s,
1948 Ok(None) => {
1949 return (
1950 StatusCode::BAD_REQUEST,
1951 Json(serde_json::json!({
1952 "error": "invalid_request",
1953 "error_description": "No passkey authentication in progress or challenge expired."
1954 })),
1955 )
1956 .into_response();
1957 }
1958 Err(e) => {
1959 tracing::error!(error = %e, "Failed to load authentication state");
1960 return (
1961 StatusCode::INTERNAL_SERVER_ERROR,
1962 Json(serde_json::json!({
1963 "error": "server_error",
1964 "error_description": "An error occurred."
1965 })),
1966 )
1967 .into_response();
1968 }
1969 };
1970
1971 let credential: webauthn_rs::prelude::PublicKeyCredential =
1972 match serde_json::from_value(form.credential) {
1973 Ok(c) => c,
1974 Err(e) => {
1975 tracing::warn!(error = %e, "Failed to parse credential");
1976 return (
1977 StatusCode::BAD_REQUEST,
1978 Json(serde_json::json!({
1979 "error": "invalid_request",
1980 "error_description": "Failed to parse credential response."
1981 })),
1982 )
1983 .into_response();
1984 }
1985 };
1986
1987 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1988 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
1989 Ok(w) => w,
1990 Err(e) => {
1991 tracing::error!(error = %e, "Failed to create WebAuthn config");
1992 return (
1993 StatusCode::INTERNAL_SERVER_ERROR,
1994 Json(serde_json::json!({
1995 "error": "server_error",
1996 "error_description": "WebAuthn configuration failed."
1997 })),
1998 )
1999 .into_response();
2000 }
2001 };
2002
2003 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
2004 Ok(r) => r,
2005 Err(e) => {
2006 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication");
2007 return (
2008 StatusCode::FORBIDDEN,
2009 Json(serde_json::json!({
2010 "error": "access_denied",
2011 "error_description": "Passkey verification failed."
2012 })),
2013 )
2014 .into_response();
2015 }
2016 };
2017
2018 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await {
2019 tracing::warn!(error = %e, "Failed to delete authentication state");
2020 }
2021
2022 if auth_result.needs_update()
2023 && let Err(e) = crate::auth::webauthn::update_passkey_counter(
2024 &state.db,
2025 auth_result.cred_id(),
2026 auth_result.counter(),
2027 )
2028 .await
2029 {
2030 tracing::warn!(error = %e, "Failed to update passkey counter");
2031 }
2032
2033 tracing::info!(did = %did, "Passkey authentication successful");
2034
2035 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await;
2036 if has_totp {
2037 return Json(serde_json::json!({
2038 "needs_totp": true
2039 }))
2040 .into_response();
2041 }
2042
2043 let user = sqlx::query!(
2044 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1",
2045 did
2046 )
2047 .fetch_optional(&state.db)
2048 .await;
2049
2050 if let Ok(Some(user)) = user
2051 && user.two_factor_enabled
2052 {
2053 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
2054 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await {
2055 Ok(challenge) => {
2056 let hostname =
2057 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2058 if let Err(e) =
2059 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
2060 {
2061 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
2062 }
2063 let channel_name = channel_display_name(user.preferred_comms_channel);
2064 return Json(serde_json::json!({
2065 "needs_2fa": true,
2066 "channel": channel_name
2067 }))
2068 .into_response();
2069 }
2070 Err(_) => {
2071 return (
2072 StatusCode::INTERNAL_SERVER_ERROR,
2073 Json(serde_json::json!({
2074 "error": "server_error",
2075 "error_description": "An error occurred."
2076 })),
2077 )
2078 .into_response();
2079 }
2080 }
2081 }
2082
2083 let device_id = extract_device_cookie(&headers);
2084 let requested_scope_str = request_data
2085 .parameters
2086 .scope
2087 .as_deref()
2088 .unwrap_or("atproto");
2089 let requested_scopes: Vec<String> = requested_scope_str
2090 .split_whitespace()
2091 .map(|s| s.to_string())
2092 .collect();
2093
2094 let needs_consent = db::should_show_consent(
2095 &state.db,
2096 &did,
2097 &request_data.parameters.client_id,
2098 &requested_scopes,
2099 )
2100 .await
2101 .unwrap_or(true);
2102
2103 if needs_consent {
2104 let consent_url = format!(
2105 "/#/oauth/consent?request_uri={}",
2106 url_encode(&form.request_uri)
2107 );
2108 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
2109 }
2110
2111 let code = Code::generate();
2112 if db::update_authorization_request(
2113 &state.db,
2114 &form.request_uri,
2115 &did,
2116 device_id.as_deref(),
2117 &code.0,
2118 )
2119 .await
2120 .is_err()
2121 {
2122 return (
2123 StatusCode::INTERNAL_SERVER_ERROR,
2124 Json(serde_json::json!({
2125 "error": "server_error",
2126 "error_description": "An error occurred."
2127 })),
2128 )
2129 .into_response();
2130 }
2131
2132 let redirect_url = build_success_redirect(
2133 &request_data.parameters.redirect_uri,
2134 &code.0,
2135 request_data.parameters.state.as_deref(),
2136 request_data.parameters.response_mode.as_deref(),
2137 );
2138
2139 Json(serde_json::json!({
2140 "redirect_uri": redirect_url
2141 }))
2142 .into_response()
2143}