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