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, 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 identifier = query.identifier.trim();
1589 let identifier = identifier.strip_prefix('@').unwrap_or(identifier);
1590 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") {
1591 identifier.to_string()
1592 } else if !identifier.contains('.') {
1593 format!("{}.{}", identifier.to_lowercase(), pds_hostname)
1594 } else {
1595 identifier.to_lowercase()
1596 };
1597
1598 let user = sqlx::query!(
1599 "SELECT did FROM users WHERE handle = $1 OR email = $1",
1600 normalized_identifier
1601 )
1602 .fetch_optional(&state.db)
1603 .await;
1604
1605 let (has_passkeys, has_totp) = match user {
1606 Ok(Some(u)) => {
1607 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await;
1608 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await;
1609 (passkeys, totp)
1610 }
1611 _ => (false, false),
1612 };
1613
1614 Json(SecurityStatusResponse {
1615 has_passkeys,
1616 has_totp,
1617 })
1618 .into_response()
1619}
1620
1621#[derive(Debug, Deserialize)]
1622pub struct PasskeyStartInput {
1623 pub request_uri: String,
1624 pub identifier: String,
1625}
1626
1627#[derive(Debug, Serialize)]
1628#[serde(rename_all = "camelCase")]
1629pub struct PasskeyStartResponse {
1630 pub options: serde_json::Value,
1631}
1632
1633pub async fn passkey_start(
1634 State(state): State<AppState>,
1635 headers: HeaderMap,
1636 Json(form): Json<PasskeyStartInput>,
1637) -> Response {
1638 let client_ip = extract_client_ip(&headers);
1639
1640 if !state
1641 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1642 .await
1643 {
1644 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded");
1645 return (
1646 StatusCode::TOO_MANY_REQUESTS,
1647 Json(serde_json::json!({
1648 "error": "RateLimitExceeded",
1649 "error_description": "Too many login attempts. Please try again later."
1650 })),
1651 )
1652 .into_response();
1653 }
1654
1655 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1656 Ok(Some(data)) => data,
1657 Ok(None) => {
1658 return (
1659 StatusCode::BAD_REQUEST,
1660 Json(serde_json::json!({
1661 "error": "invalid_request",
1662 "error_description": "Invalid or expired request_uri."
1663 })),
1664 )
1665 .into_response();
1666 }
1667 Err(_) => {
1668 return (
1669 StatusCode::INTERNAL_SERVER_ERROR,
1670 Json(serde_json::json!({
1671 "error": "server_error",
1672 "error_description": "An error occurred."
1673 })),
1674 )
1675 .into_response();
1676 }
1677 };
1678
1679 if request_data.expires_at < Utc::now() {
1680 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1681 return (
1682 StatusCode::BAD_REQUEST,
1683 Json(serde_json::json!({
1684 "error": "invalid_request",
1685 "error_description": "Authorization request has expired."
1686 })),
1687 )
1688 .into_response();
1689 }
1690
1691 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1692 let normalized_username = form.identifier.trim();
1693 let normalized_username = normalized_username
1694 .strip_prefix('@')
1695 .unwrap_or(normalized_username);
1696 let normalized_username = if normalized_username.contains('@') {
1697 normalized_username.to_string()
1698 } else if !normalized_username.contains('.') {
1699 format!("{}.{}", normalized_username, pds_hostname)
1700 } else {
1701 normalized_username.to_string()
1702 };
1703
1704 let user = match sqlx::query!(
1705 r#"
1706 SELECT did, deactivated_at, takedown_ref,
1707 email_verified, discord_verified, telegram_verified, signal_verified
1708 FROM users
1709 WHERE handle = $1 OR email = $1
1710 "#,
1711 normalized_username
1712 )
1713 .fetch_optional(&state.db)
1714 .await
1715 {
1716 Ok(Some(u)) => u,
1717 Ok(None) => {
1718 return (
1719 StatusCode::FORBIDDEN,
1720 Json(serde_json::json!({
1721 "error": "access_denied",
1722 "error_description": "User not found or has no passkeys."
1723 })),
1724 )
1725 .into_response();
1726 }
1727 Err(_) => {
1728 return (
1729 StatusCode::INTERNAL_SERVER_ERROR,
1730 Json(serde_json::json!({
1731 "error": "server_error",
1732 "error_description": "An error occurred."
1733 })),
1734 )
1735 .into_response();
1736 }
1737 };
1738
1739 if user.deactivated_at.is_some() {
1740 return (
1741 StatusCode::FORBIDDEN,
1742 Json(serde_json::json!({
1743 "error": "access_denied",
1744 "error_description": "This account has been deactivated."
1745 })),
1746 )
1747 .into_response();
1748 }
1749
1750 if user.takedown_ref.is_some() {
1751 return (
1752 StatusCode::FORBIDDEN,
1753 Json(serde_json::json!({
1754 "error": "access_denied",
1755 "error_description": "This account has been taken down."
1756 })),
1757 )
1758 .into_response();
1759 }
1760
1761 let is_verified = user.email_verified
1762 || user.discord_verified
1763 || user.telegram_verified
1764 || user.signal_verified;
1765
1766 if !is_verified {
1767 return (
1768 StatusCode::FORBIDDEN,
1769 Json(serde_json::json!({
1770 "error": "access_denied",
1771 "error_description": "Please verify your account before logging in."
1772 })),
1773 )
1774 .into_response();
1775 }
1776
1777 let stored_passkeys =
1778 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await {
1779 Ok(pks) => pks,
1780 Err(e) => {
1781 tracing::error!(error = %e, "Failed to get passkeys");
1782 return (
1783 StatusCode::INTERNAL_SERVER_ERROR,
1784 Json(serde_json::json!({
1785 "error": "server_error",
1786 "error_description": "An error occurred."
1787 })),
1788 )
1789 .into_response();
1790 }
1791 };
1792
1793 if stored_passkeys.is_empty() {
1794 return (
1795 StatusCode::FORBIDDEN,
1796 Json(serde_json::json!({
1797 "error": "access_denied",
1798 "error_description": "User not found or has no passkeys."
1799 })),
1800 )
1801 .into_response();
1802 }
1803
1804 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
1805 .iter()
1806 .filter_map(|sp| sp.to_security_key().ok())
1807 .collect();
1808
1809 if passkeys.is_empty() {
1810 return (
1811 StatusCode::INTERNAL_SERVER_ERROR,
1812 Json(serde_json::json!({
1813 "error": "server_error",
1814 "error_description": "Failed to load passkeys."
1815 })),
1816 )
1817 .into_response();
1818 }
1819
1820 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
1821 Ok(w) => w,
1822 Err(e) => {
1823 tracing::error!(error = %e, "Failed to create WebAuthn config");
1824 return (
1825 StatusCode::INTERNAL_SERVER_ERROR,
1826 Json(serde_json::json!({
1827 "error": "server_error",
1828 "error_description": "WebAuthn configuration failed."
1829 })),
1830 )
1831 .into_response();
1832 }
1833 };
1834
1835 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
1836 Ok(result) => result,
1837 Err(e) => {
1838 tracing::error!(error = %e, "Failed to start passkey authentication");
1839 return (
1840 StatusCode::INTERNAL_SERVER_ERROR,
1841 Json(serde_json::json!({
1842 "error": "server_error",
1843 "error_description": "Failed to start authentication."
1844 })),
1845 )
1846 .into_response();
1847 }
1848 };
1849
1850 if let Err(e) =
1851 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await
1852 {
1853 tracing::error!(error = %e, "Failed to save authentication state");
1854 return (
1855 StatusCode::INTERNAL_SERVER_ERROR,
1856 Json(serde_json::json!({
1857 "error": "server_error",
1858 "error_description": "An error occurred."
1859 })),
1860 )
1861 .into_response();
1862 }
1863
1864 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
1865 .await
1866 .is_err()
1867 {
1868 return (
1869 StatusCode::INTERNAL_SERVER_ERROR,
1870 Json(serde_json::json!({
1871 "error": "server_error",
1872 "error_description": "An error occurred."
1873 })),
1874 )
1875 .into_response();
1876 }
1877
1878 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
1879
1880 Json(PasskeyStartResponse { options }).into_response()
1881}
1882
1883#[derive(Debug, Deserialize)]
1884pub struct PasskeyFinishInput {
1885 pub request_uri: String,
1886 pub credential: serde_json::Value,
1887}
1888
1889pub async fn passkey_finish(
1890 State(state): State<AppState>,
1891 headers: HeaderMap,
1892 Json(form): Json<PasskeyFinishInput>,
1893) -> Response {
1894 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1895 Ok(Some(data)) => data,
1896 Ok(None) => {
1897 return (
1898 StatusCode::BAD_REQUEST,
1899 Json(serde_json::json!({
1900 "error": "invalid_request",
1901 "error_description": "Invalid or expired request_uri."
1902 })),
1903 )
1904 .into_response();
1905 }
1906 Err(_) => {
1907 return (
1908 StatusCode::INTERNAL_SERVER_ERROR,
1909 Json(serde_json::json!({
1910 "error": "server_error",
1911 "error_description": "An error occurred."
1912 })),
1913 )
1914 .into_response();
1915 }
1916 };
1917
1918 if request_data.expires_at < Utc::now() {
1919 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1920 return (
1921 StatusCode::BAD_REQUEST,
1922 Json(serde_json::json!({
1923 "error": "invalid_request",
1924 "error_description": "Authorization request has expired."
1925 })),
1926 )
1927 .into_response();
1928 }
1929
1930 let did = match request_data.did {
1931 Some(d) => d,
1932 None => {
1933 return (
1934 StatusCode::BAD_REQUEST,
1935 Json(serde_json::json!({
1936 "error": "invalid_request",
1937 "error_description": "No passkey authentication in progress."
1938 })),
1939 )
1940 .into_response();
1941 }
1942 };
1943
1944 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await {
1945 Ok(Some(s)) => s,
1946 Ok(None) => {
1947 return (
1948 StatusCode::BAD_REQUEST,
1949 Json(serde_json::json!({
1950 "error": "invalid_request",
1951 "error_description": "No passkey authentication in progress or challenge expired."
1952 })),
1953 )
1954 .into_response();
1955 }
1956 Err(e) => {
1957 tracing::error!(error = %e, "Failed to load authentication state");
1958 return (
1959 StatusCode::INTERNAL_SERVER_ERROR,
1960 Json(serde_json::json!({
1961 "error": "server_error",
1962 "error_description": "An error occurred."
1963 })),
1964 )
1965 .into_response();
1966 }
1967 };
1968
1969 let credential: webauthn_rs::prelude::PublicKeyCredential =
1970 match serde_json::from_value(form.credential) {
1971 Ok(c) => c,
1972 Err(e) => {
1973 tracing::warn!(error = %e, "Failed to parse credential");
1974 return (
1975 StatusCode::BAD_REQUEST,
1976 Json(serde_json::json!({
1977 "error": "invalid_request",
1978 "error_description": "Failed to parse credential response."
1979 })),
1980 )
1981 .into_response();
1982 }
1983 };
1984
1985 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1986 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
1987 Ok(w) => w,
1988 Err(e) => {
1989 tracing::error!(error = %e, "Failed to create WebAuthn config");
1990 return (
1991 StatusCode::INTERNAL_SERVER_ERROR,
1992 Json(serde_json::json!({
1993 "error": "server_error",
1994 "error_description": "WebAuthn configuration failed."
1995 })),
1996 )
1997 .into_response();
1998 }
1999 };
2000
2001 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
2002 Ok(r) => r,
2003 Err(e) => {
2004 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication");
2005 return (
2006 StatusCode::FORBIDDEN,
2007 Json(serde_json::json!({
2008 "error": "access_denied",
2009 "error_description": "Passkey verification failed."
2010 })),
2011 )
2012 .into_response();
2013 }
2014 };
2015
2016 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await {
2017 tracing::warn!(error = %e, "Failed to delete authentication state");
2018 }
2019
2020 if auth_result.needs_update()
2021 && let Err(e) = crate::auth::webauthn::update_passkey_counter(
2022 &state.db,
2023 auth_result.cred_id(),
2024 auth_result.counter(),
2025 )
2026 .await
2027 {
2028 tracing::warn!(error = %e, "Failed to update passkey counter");
2029 }
2030
2031 tracing::info!(did = %did, "Passkey authentication successful");
2032
2033 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await;
2034 if has_totp {
2035 return Json(serde_json::json!({
2036 "needs_totp": true
2037 }))
2038 .into_response();
2039 }
2040
2041 let user = sqlx::query!(
2042 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1",
2043 did
2044 )
2045 .fetch_optional(&state.db)
2046 .await;
2047
2048 if let Ok(Some(user)) = user
2049 && user.two_factor_enabled
2050 {
2051 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
2052 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await {
2053 Ok(challenge) => {
2054 let hostname =
2055 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2056 if let Err(e) =
2057 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
2058 {
2059 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
2060 }
2061 let channel_name = channel_display_name(user.preferred_comms_channel);
2062 return Json(serde_json::json!({
2063 "needs_2fa": true,
2064 "channel": channel_name
2065 }))
2066 .into_response();
2067 }
2068 Err(_) => {
2069 return (
2070 StatusCode::INTERNAL_SERVER_ERROR,
2071 Json(serde_json::json!({
2072 "error": "server_error",
2073 "error_description": "An error occurred."
2074 })),
2075 )
2076 .into_response();
2077 }
2078 }
2079 }
2080
2081 let device_id = extract_device_cookie(&headers);
2082 let requested_scope_str = request_data
2083 .parameters
2084 .scope
2085 .as_deref()
2086 .unwrap_or("atproto");
2087 let requested_scopes: Vec<String> = requested_scope_str
2088 .split_whitespace()
2089 .map(|s| s.to_string())
2090 .collect();
2091
2092 let needs_consent = db::should_show_consent(
2093 &state.db,
2094 &did,
2095 &request_data.parameters.client_id,
2096 &requested_scopes,
2097 )
2098 .await
2099 .unwrap_or(true);
2100
2101 if needs_consent {
2102 let consent_url = format!(
2103 "/#/oauth/consent?request_uri={}",
2104 url_encode(&form.request_uri)
2105 );
2106 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
2107 }
2108
2109 let code = Code::generate();
2110 if db::update_authorization_request(
2111 &state.db,
2112 &form.request_uri,
2113 &did,
2114 device_id.as_deref(),
2115 &code.0,
2116 )
2117 .await
2118 .is_err()
2119 {
2120 return (
2121 StatusCode::INTERNAL_SERVER_ERROR,
2122 Json(serde_json::json!({
2123 "error": "server_error",
2124 "error_description": "An error occurred."
2125 })),
2126 )
2127 .into_response();
2128 }
2129
2130 let redirect_url = build_success_redirect(
2131 &request_data.parameters.redirect_uri,
2132 &code.0,
2133 request_data.parameters.state.as_deref(),
2134 request_data.parameters.response_mode.as_deref(),
2135 );
2136
2137 Json(serde_json::json!({
2138 "redirect_uri": redirect_url
2139 }))
2140 .into_response()
2141}