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