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 "/app/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 "/app/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 "/app/oauth/accounts?request_uri={}",
263 url_encode(&request_uri)
264 ));
265 }
266 redirect_see_other(&format!(
267 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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("identity:")
1468 || s.starts_with("include:")
1469 });
1470 if !has_valid_scope {
1471 return (
1472 StatusCode::BAD_REQUEST,
1473 Json(serde_json::json!({
1474 "error": "invalid_request",
1475 "error_description": "Invalid scope format"
1476 })),
1477 )
1478 .into_response();
1479 }
1480 if form.remember {
1481 let preferences: Vec<db::ScopePreference> = requested_scopes
1482 .iter()
1483 .map(|s| db::ScopePreference {
1484 scope: s.to_string(),
1485 granted: form.approved_scopes.contains(&s.to_string()),
1486 })
1487 .collect();
1488 let _ = db::upsert_scope_preferences(
1489 &state.db,
1490 &did,
1491 &request_data.parameters.client_id,
1492 &preferences,
1493 )
1494 .await;
1495 }
1496 if let Err(e) =
1497 db::update_request_scope(&state.db, &form.request_uri, &approved_scope_str).await
1498 {
1499 tracing::warn!("Failed to update request scope: {:?}", e);
1500 }
1501 let code = Code::generate();
1502 if db::update_authorization_request(
1503 &state.db,
1504 &form.request_uri,
1505 &did,
1506 request_data.device_id.as_deref(),
1507 &code.0,
1508 )
1509 .await
1510 .is_err()
1511 {
1512 return (
1513 StatusCode::INTERNAL_SERVER_ERROR,
1514 Json(serde_json::json!({
1515 "error": "server_error",
1516 "error_description": "Failed to complete authorization"
1517 })),
1518 )
1519 .into_response();
1520 }
1521 let redirect_url = build_success_redirect(
1522 &request_data.parameters.redirect_uri,
1523 &code.0,
1524 request_data.parameters.state.as_deref(),
1525 request_data.parameters.response_mode.as_deref(),
1526 );
1527 Json(serde_json::json!({
1528 "redirect_uri": redirect_url
1529 }))
1530 .into_response()
1531}
1532
1533pub async fn authorize_2fa_post(
1534 State(state): State<AppState>,
1535 headers: HeaderMap,
1536 Json(form): Json<Authorize2faSubmit>,
1537) -> Response {
1538 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
1539 (
1540 status,
1541 Json(serde_json::json!({
1542 "error": error,
1543 "error_description": description
1544 })),
1545 )
1546 .into_response()
1547 };
1548 let client_ip = extract_client_ip(&headers);
1549 if !state
1550 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1551 .await
1552 {
1553 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded");
1554 return json_error(
1555 StatusCode::TOO_MANY_REQUESTS,
1556 "RateLimitExceeded",
1557 "Too many attempts. Please try again later.",
1558 );
1559 }
1560 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1561 Ok(Some(d)) => d,
1562 Ok(None) => {
1563 return json_error(
1564 StatusCode::BAD_REQUEST,
1565 "invalid_request",
1566 "Authorization request not found.",
1567 );
1568 }
1569 Err(_) => {
1570 return json_error(
1571 StatusCode::INTERNAL_SERVER_ERROR,
1572 "server_error",
1573 "An error occurred.",
1574 );
1575 }
1576 };
1577 if request_data.expires_at < Utc::now() {
1578 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1579 return json_error(
1580 StatusCode::BAD_REQUEST,
1581 "invalid_request",
1582 "Authorization request has expired.",
1583 );
1584 }
1585 let challenge = db::get_2fa_challenge(&state.db, &form.request_uri)
1586 .await
1587 .ok()
1588 .flatten();
1589 if let Some(challenge) = challenge {
1590 if challenge.expires_at < Utc::now() {
1591 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1592 return json_error(
1593 StatusCode::BAD_REQUEST,
1594 "invalid_request",
1595 "2FA code has expired. Please start over.",
1596 );
1597 }
1598 if challenge.attempts >= MAX_2FA_ATTEMPTS {
1599 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1600 return json_error(
1601 StatusCode::FORBIDDEN,
1602 "access_denied",
1603 "Too many failed attempts. Please start over.",
1604 );
1605 }
1606 let code_valid: bool = form
1607 .code
1608 .trim()
1609 .as_bytes()
1610 .ct_eq(challenge.code.as_bytes())
1611 .into();
1612 if !code_valid {
1613 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await;
1614 return json_error(
1615 StatusCode::FORBIDDEN,
1616 "invalid_code",
1617 "Invalid verification code. Please try again.",
1618 );
1619 }
1620 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1621 let code = Code::generate();
1622 let device_id = extract_device_cookie(&headers);
1623 if db::update_authorization_request(
1624 &state.db,
1625 &form.request_uri,
1626 &challenge.did,
1627 device_id.as_deref(),
1628 &code.0,
1629 )
1630 .await
1631 .is_err()
1632 {
1633 return json_error(
1634 StatusCode::INTERNAL_SERVER_ERROR,
1635 "server_error",
1636 "An error occurred. Please try again.",
1637 );
1638 }
1639 let redirect_url = build_success_redirect(
1640 &request_data.parameters.redirect_uri,
1641 &code.0,
1642 request_data.parameters.state.as_deref(),
1643 request_data.parameters.response_mode.as_deref(),
1644 );
1645 return Json(serde_json::json!({
1646 "redirect_uri": redirect_url
1647 }))
1648 .into_response();
1649 }
1650 let did = match &request_data.did {
1651 Some(d) => d.clone(),
1652 None => {
1653 return json_error(
1654 StatusCode::BAD_REQUEST,
1655 "invalid_request",
1656 "No 2FA challenge found. Please start over.",
1657 );
1658 }
1659 };
1660 if !crate::api::server::has_totp_enabled(&state, &did).await {
1661 return json_error(
1662 StatusCode::BAD_REQUEST,
1663 "invalid_request",
1664 "No 2FA challenge found. Please start over.",
1665 );
1666 }
1667 if !state
1668 .check_rate_limit(RateLimitKind::TotpVerify, &did)
1669 .await
1670 {
1671 tracing::warn!(did = %did, "TOTP verification rate limit exceeded");
1672 return json_error(
1673 StatusCode::TOO_MANY_REQUESTS,
1674 "RateLimitExceeded",
1675 "Too many verification attempts. Please try again in a few minutes.",
1676 );
1677 }
1678 let totp_valid =
1679 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
1680 if !totp_valid {
1681 return json_error(
1682 StatusCode::FORBIDDEN,
1683 "invalid_code",
1684 "Invalid verification code. Please try again.",
1685 );
1686 }
1687 let device_id = extract_device_cookie(&headers);
1688 if form.trust_device
1689 && let Some(ref dev_id) = device_id
1690 {
1691 let _ = crate::api::server::trust_device(&state.db, dev_id).await;
1692 }
1693 let requested_scope_str = request_data
1694 .parameters
1695 .scope
1696 .as_deref()
1697 .unwrap_or("atproto");
1698 let requested_scopes: Vec<String> = requested_scope_str
1699 .split_whitespace()
1700 .map(|s| s.to_string())
1701 .collect();
1702 let needs_consent = db::should_show_consent(
1703 &state.db,
1704 &did,
1705 &request_data.parameters.client_id,
1706 &requested_scopes,
1707 )
1708 .await
1709 .unwrap_or(true);
1710 if needs_consent {
1711 let consent_url = format!(
1712 "/app/oauth/consent?request_uri={}",
1713 url_encode(&form.request_uri)
1714 );
1715 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
1716 }
1717 let code = Code::generate();
1718 if db::update_authorization_request(
1719 &state.db,
1720 &form.request_uri,
1721 &did,
1722 device_id.as_deref(),
1723 &code.0,
1724 )
1725 .await
1726 .is_err()
1727 {
1728 return json_error(
1729 StatusCode::INTERNAL_SERVER_ERROR,
1730 "server_error",
1731 "An error occurred. Please try again.",
1732 );
1733 }
1734 let redirect_url = build_success_redirect(
1735 &request_data.parameters.redirect_uri,
1736 &code.0,
1737 request_data.parameters.state.as_deref(),
1738 request_data.parameters.response_mode.as_deref(),
1739 );
1740 Json(serde_json::json!({
1741 "redirect_uri": redirect_url
1742 }))
1743 .into_response()
1744}
1745
1746#[derive(Debug, Deserialize)]
1747#[serde(rename_all = "camelCase")]
1748pub struct CheckPasskeysQuery {
1749 pub identifier: String,
1750}
1751
1752#[derive(Debug, Serialize)]
1753#[serde(rename_all = "camelCase")]
1754pub struct CheckPasskeysResponse {
1755 pub has_passkeys: bool,
1756}
1757
1758pub async fn check_user_has_passkeys(
1759 State(state): State<AppState>,
1760 Query(query): Query<CheckPasskeysQuery>,
1761) -> Response {
1762 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1763 let normalized_identifier = query.identifier.trim();
1764 let normalized_identifier = normalized_identifier
1765 .strip_prefix('@')
1766 .unwrap_or(normalized_identifier);
1767 let normalized_identifier = if let Some(bare_handle) =
1768 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname))
1769 {
1770 bare_handle.to_string()
1771 } else {
1772 normalized_identifier.to_string()
1773 };
1774
1775 let user = sqlx::query!(
1776 "SELECT did FROM users WHERE handle = $1 OR email = $1",
1777 normalized_identifier
1778 )
1779 .fetch_optional(&state.db)
1780 .await;
1781
1782 let has_passkeys = match user {
1783 Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await,
1784 _ => false,
1785 };
1786
1787 Json(CheckPasskeysResponse { has_passkeys }).into_response()
1788}
1789
1790#[derive(Debug, Serialize)]
1791#[serde(rename_all = "camelCase")]
1792pub struct SecurityStatusResponse {
1793 pub has_passkeys: bool,
1794 pub has_totp: bool,
1795 pub has_password: bool,
1796 pub is_delegated: bool,
1797 #[serde(skip_serializing_if = "Option::is_none")]
1798 pub did: Option<String>,
1799}
1800
1801pub async fn check_user_security_status(
1802 State(state): State<AppState>,
1803 Query(query): Query<CheckPasskeysQuery>,
1804) -> Response {
1805 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1806 let identifier = query.identifier.trim();
1807 let identifier = identifier.strip_prefix('@').unwrap_or(identifier);
1808 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") {
1809 identifier.to_string()
1810 } else if !identifier.contains('.') {
1811 format!("{}.{}", identifier.to_lowercase(), pds_hostname)
1812 } else {
1813 identifier.to_lowercase()
1814 };
1815
1816 let user = sqlx::query!(
1817 "SELECT did, password_hash FROM users WHERE handle = $1 OR email = $1",
1818 normalized_identifier
1819 )
1820 .fetch_optional(&state.db)
1821 .await;
1822
1823 let (has_passkeys, has_totp, has_password, is_delegated, did): (
1824 bool,
1825 bool,
1826 bool,
1827 bool,
1828 Option<String>,
1829 ) = match user {
1830 Ok(Some(u)) => {
1831 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await;
1832 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await;
1833 let has_pw = u.password_hash.is_some();
1834 let has_controllers = crate::delegation::is_delegated_account(&state.db, &u.did)
1835 .await
1836 .unwrap_or(false);
1837 (passkeys, totp, has_pw, has_controllers, Some(u.did))
1838 }
1839 _ => (false, false, false, false, None),
1840 };
1841
1842 Json(SecurityStatusResponse {
1843 has_passkeys,
1844 has_totp,
1845 has_password,
1846 is_delegated,
1847 did,
1848 })
1849 .into_response()
1850}
1851
1852#[derive(Debug, Deserialize)]
1853pub struct PasskeyStartInput {
1854 pub request_uri: String,
1855 pub identifier: String,
1856}
1857
1858#[derive(Debug, Serialize)]
1859#[serde(rename_all = "camelCase")]
1860pub struct PasskeyStartResponse {
1861 pub options: serde_json::Value,
1862}
1863
1864pub async fn passkey_start(
1865 State(state): State<AppState>,
1866 headers: HeaderMap,
1867 Json(form): Json<PasskeyStartInput>,
1868) -> Response {
1869 let client_ip = extract_client_ip(&headers);
1870
1871 if !state
1872 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
1873 .await
1874 {
1875 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded");
1876 return (
1877 StatusCode::TOO_MANY_REQUESTS,
1878 Json(serde_json::json!({
1879 "error": "RateLimitExceeded",
1880 "error_description": "Too many login attempts. Please try again later."
1881 })),
1882 )
1883 .into_response();
1884 }
1885
1886 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1887 Ok(Some(data)) => data,
1888 Ok(None) => {
1889 return (
1890 StatusCode::BAD_REQUEST,
1891 Json(serde_json::json!({
1892 "error": "invalid_request",
1893 "error_description": "Invalid or expired request_uri."
1894 })),
1895 )
1896 .into_response();
1897 }
1898 Err(_) => {
1899 return (
1900 StatusCode::INTERNAL_SERVER_ERROR,
1901 Json(serde_json::json!({
1902 "error": "server_error",
1903 "error_description": "An error occurred."
1904 })),
1905 )
1906 .into_response();
1907 }
1908 };
1909
1910 if request_data.expires_at < Utc::now() {
1911 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1912 return (
1913 StatusCode::BAD_REQUEST,
1914 Json(serde_json::json!({
1915 "error": "invalid_request",
1916 "error_description": "Authorization request has expired."
1917 })),
1918 )
1919 .into_response();
1920 }
1921
1922 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1923 let normalized_username = form.identifier.trim();
1924 let normalized_username = normalized_username
1925 .strip_prefix('@')
1926 .unwrap_or(normalized_username);
1927 let normalized_username = if normalized_username.contains('@') {
1928 normalized_username.to_string()
1929 } else if !normalized_username.contains('.') {
1930 format!("{}.{}", normalized_username, pds_hostname)
1931 } else {
1932 normalized_username.to_string()
1933 };
1934
1935 let user = match sqlx::query!(
1936 r#"
1937 SELECT did, deactivated_at, takedown_ref,
1938 email_verified, discord_verified, telegram_verified, signal_verified
1939 FROM users
1940 WHERE handle = $1 OR email = $1
1941 "#,
1942 normalized_username
1943 )
1944 .fetch_optional(&state.db)
1945 .await
1946 {
1947 Ok(Some(u)) => u,
1948 Ok(None) => {
1949 return (
1950 StatusCode::FORBIDDEN,
1951 Json(serde_json::json!({
1952 "error": "access_denied",
1953 "error_description": "User not found or has no passkeys."
1954 })),
1955 )
1956 .into_response();
1957 }
1958 Err(_) => {
1959 return (
1960 StatusCode::INTERNAL_SERVER_ERROR,
1961 Json(serde_json::json!({
1962 "error": "server_error",
1963 "error_description": "An error occurred."
1964 })),
1965 )
1966 .into_response();
1967 }
1968 };
1969
1970 if user.deactivated_at.is_some() {
1971 return (
1972 StatusCode::FORBIDDEN,
1973 Json(serde_json::json!({
1974 "error": "access_denied",
1975 "error_description": "This account has been deactivated."
1976 })),
1977 )
1978 .into_response();
1979 }
1980
1981 if user.takedown_ref.is_some() {
1982 return (
1983 StatusCode::FORBIDDEN,
1984 Json(serde_json::json!({
1985 "error": "access_denied",
1986 "error_description": "This account has been taken down."
1987 })),
1988 )
1989 .into_response();
1990 }
1991
1992 let is_verified = user.email_verified
1993 || user.discord_verified
1994 || user.telegram_verified
1995 || user.signal_verified;
1996
1997 if !is_verified {
1998 return (
1999 StatusCode::FORBIDDEN,
2000 Json(serde_json::json!({
2001 "error": "access_denied",
2002 "error_description": "Please verify your account before logging in."
2003 })),
2004 )
2005 .into_response();
2006 }
2007
2008 let stored_passkeys =
2009 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await {
2010 Ok(pks) => pks,
2011 Err(e) => {
2012 tracing::error!(error = %e, "Failed to get passkeys");
2013 return (
2014 StatusCode::INTERNAL_SERVER_ERROR,
2015 Json(serde_json::json!({
2016 "error": "server_error",
2017 "error_description": "An error occurred."
2018 })),
2019 )
2020 .into_response();
2021 }
2022 };
2023
2024 if stored_passkeys.is_empty() {
2025 return (
2026 StatusCode::FORBIDDEN,
2027 Json(serde_json::json!({
2028 "error": "access_denied",
2029 "error_description": "User not found or has no passkeys."
2030 })),
2031 )
2032 .into_response();
2033 }
2034
2035 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
2036 .iter()
2037 .filter_map(|sp| sp.to_security_key().ok())
2038 .collect();
2039
2040 if passkeys.is_empty() {
2041 return (
2042 StatusCode::INTERNAL_SERVER_ERROR,
2043 Json(serde_json::json!({
2044 "error": "server_error",
2045 "error_description": "Failed to load passkeys."
2046 })),
2047 )
2048 .into_response();
2049 }
2050
2051 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2052 Ok(w) => w,
2053 Err(e) => {
2054 tracing::error!(error = %e, "Failed to create WebAuthn config");
2055 return (
2056 StatusCode::INTERNAL_SERVER_ERROR,
2057 Json(serde_json::json!({
2058 "error": "server_error",
2059 "error_description": "WebAuthn configuration failed."
2060 })),
2061 )
2062 .into_response();
2063 }
2064 };
2065
2066 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
2067 Ok(result) => result,
2068 Err(e) => {
2069 tracing::error!(error = %e, "Failed to start passkey authentication");
2070 return (
2071 StatusCode::INTERNAL_SERVER_ERROR,
2072 Json(serde_json::json!({
2073 "error": "server_error",
2074 "error_description": "Failed to start authentication."
2075 })),
2076 )
2077 .into_response();
2078 }
2079 };
2080
2081 if let Err(e) =
2082 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await
2083 {
2084 tracing::error!(error = %e, "Failed to save authentication state");
2085 return (
2086 StatusCode::INTERNAL_SERVER_ERROR,
2087 Json(serde_json::json!({
2088 "error": "server_error",
2089 "error_description": "An error occurred."
2090 })),
2091 )
2092 .into_response();
2093 }
2094
2095 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
2096 .await
2097 .is_err()
2098 {
2099 return (
2100 StatusCode::INTERNAL_SERVER_ERROR,
2101 Json(serde_json::json!({
2102 "error": "server_error",
2103 "error_description": "An error occurred."
2104 })),
2105 )
2106 .into_response();
2107 }
2108
2109 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
2110
2111 Json(PasskeyStartResponse { options }).into_response()
2112}
2113
2114#[derive(Debug, Deserialize)]
2115pub struct PasskeyFinishInput {
2116 pub request_uri: String,
2117 pub credential: serde_json::Value,
2118}
2119
2120pub async fn passkey_finish(
2121 State(state): State<AppState>,
2122 headers: HeaderMap,
2123 Json(form): Json<PasskeyFinishInput>,
2124) -> Response {
2125 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
2126 Ok(Some(data)) => data,
2127 Ok(None) => {
2128 return (
2129 StatusCode::BAD_REQUEST,
2130 Json(serde_json::json!({
2131 "error": "invalid_request",
2132 "error_description": "Invalid or expired request_uri."
2133 })),
2134 )
2135 .into_response();
2136 }
2137 Err(_) => {
2138 return (
2139 StatusCode::INTERNAL_SERVER_ERROR,
2140 Json(serde_json::json!({
2141 "error": "server_error",
2142 "error_description": "An error occurred."
2143 })),
2144 )
2145 .into_response();
2146 }
2147 };
2148
2149 if request_data.expires_at < Utc::now() {
2150 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
2151 return (
2152 StatusCode::BAD_REQUEST,
2153 Json(serde_json::json!({
2154 "error": "invalid_request",
2155 "error_description": "Authorization request has expired."
2156 })),
2157 )
2158 .into_response();
2159 }
2160
2161 let did = match request_data.did {
2162 Some(d) => d,
2163 None => {
2164 return (
2165 StatusCode::BAD_REQUEST,
2166 Json(serde_json::json!({
2167 "error": "invalid_request",
2168 "error_description": "No passkey authentication in progress."
2169 })),
2170 )
2171 .into_response();
2172 }
2173 };
2174
2175 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await {
2176 Ok(Some(s)) => s,
2177 Ok(None) => {
2178 return (
2179 StatusCode::BAD_REQUEST,
2180 Json(serde_json::json!({
2181 "error": "invalid_request",
2182 "error_description": "No passkey authentication in progress or challenge expired."
2183 })),
2184 )
2185 .into_response();
2186 }
2187 Err(e) => {
2188 tracing::error!(error = %e, "Failed to load authentication state");
2189 return (
2190 StatusCode::INTERNAL_SERVER_ERROR,
2191 Json(serde_json::json!({
2192 "error": "server_error",
2193 "error_description": "An error occurred."
2194 })),
2195 )
2196 .into_response();
2197 }
2198 };
2199
2200 let credential: webauthn_rs::prelude::PublicKeyCredential =
2201 match serde_json::from_value(form.credential) {
2202 Ok(c) => c,
2203 Err(e) => {
2204 tracing::warn!(error = %e, "Failed to parse credential");
2205 return (
2206 StatusCode::BAD_REQUEST,
2207 Json(serde_json::json!({
2208 "error": "invalid_request",
2209 "error_description": "Failed to parse credential response."
2210 })),
2211 )
2212 .into_response();
2213 }
2214 };
2215
2216 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2217 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2218 Ok(w) => w,
2219 Err(e) => {
2220 tracing::error!(error = %e, "Failed to create WebAuthn config");
2221 return (
2222 StatusCode::INTERNAL_SERVER_ERROR,
2223 Json(serde_json::json!({
2224 "error": "server_error",
2225 "error_description": "WebAuthn configuration failed."
2226 })),
2227 )
2228 .into_response();
2229 }
2230 };
2231
2232 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
2233 Ok(r) => r,
2234 Err(e) => {
2235 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication");
2236 return (
2237 StatusCode::FORBIDDEN,
2238 Json(serde_json::json!({
2239 "error": "access_denied",
2240 "error_description": "Passkey verification failed."
2241 })),
2242 )
2243 .into_response();
2244 }
2245 };
2246
2247 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await {
2248 tracing::warn!(error = %e, "Failed to delete authentication state");
2249 }
2250
2251 if auth_result.needs_update() {
2252 match crate::auth::webauthn::update_passkey_counter(
2253 &state.db,
2254 auth_result.cred_id(),
2255 auth_result.counter(),
2256 )
2257 .await
2258 {
2259 Ok(false) => {
2260 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key");
2261 return (
2262 StatusCode::FORBIDDEN,
2263 Json(serde_json::json!({
2264 "error": "access_denied",
2265 "error_description": "Security key counter anomaly detected. This may indicate a cloned key."
2266 })),
2267 )
2268 .into_response();
2269 }
2270 Err(e) => {
2271 tracing::warn!(error = %e, "Failed to update passkey counter");
2272 }
2273 Ok(true) => {}
2274 }
2275 }
2276
2277 tracing::info!(did = %did, "Passkey authentication successful");
2278
2279 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await;
2280 if has_totp {
2281 return Json(serde_json::json!({
2282 "needs_totp": true
2283 }))
2284 .into_response();
2285 }
2286
2287 let user = sqlx::query!(
2288 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1",
2289 did
2290 )
2291 .fetch_optional(&state.db)
2292 .await;
2293
2294 if let Ok(Some(user)) = user
2295 && user.two_factor_enabled
2296 {
2297 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
2298 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await {
2299 Ok(challenge) => {
2300 let hostname =
2301 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2302 if let Err(e) =
2303 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
2304 {
2305 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
2306 }
2307 let channel_name = channel_display_name(user.preferred_comms_channel);
2308 return Json(serde_json::json!({
2309 "needs_2fa": true,
2310 "channel": channel_name
2311 }))
2312 .into_response();
2313 }
2314 Err(_) => {
2315 return (
2316 StatusCode::INTERNAL_SERVER_ERROR,
2317 Json(serde_json::json!({
2318 "error": "server_error",
2319 "error_description": "An error occurred."
2320 })),
2321 )
2322 .into_response();
2323 }
2324 }
2325 }
2326
2327 let device_id = extract_device_cookie(&headers);
2328 let requested_scope_str = request_data
2329 .parameters
2330 .scope
2331 .as_deref()
2332 .unwrap_or("atproto");
2333 let requested_scopes: Vec<String> = requested_scope_str
2334 .split_whitespace()
2335 .map(|s| s.to_string())
2336 .collect();
2337
2338 let needs_consent = db::should_show_consent(
2339 &state.db,
2340 &did,
2341 &request_data.parameters.client_id,
2342 &requested_scopes,
2343 )
2344 .await
2345 .unwrap_or(true);
2346
2347 if needs_consent {
2348 let consent_url = format!(
2349 "/app/oauth/consent?request_uri={}",
2350 url_encode(&form.request_uri)
2351 );
2352 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
2353 }
2354
2355 let code = Code::generate();
2356 if db::update_authorization_request(
2357 &state.db,
2358 &form.request_uri,
2359 &did,
2360 device_id.as_deref(),
2361 &code.0,
2362 )
2363 .await
2364 .is_err()
2365 {
2366 return (
2367 StatusCode::INTERNAL_SERVER_ERROR,
2368 Json(serde_json::json!({
2369 "error": "server_error",
2370 "error_description": "An error occurred."
2371 })),
2372 )
2373 .into_response();
2374 }
2375
2376 let redirect_url = build_success_redirect(
2377 &request_data.parameters.redirect_uri,
2378 &code.0,
2379 request_data.parameters.state.as_deref(),
2380 request_data.parameters.response_mode.as_deref(),
2381 );
2382
2383 Json(serde_json::json!({
2384 "redirect_uri": redirect_url
2385 }))
2386 .into_response()
2387}
2388
2389#[derive(Debug, Deserialize)]
2390pub struct AuthorizePasskeyQuery {
2391 pub request_uri: String,
2392}
2393
2394#[derive(Debug, Serialize)]
2395#[serde(rename_all = "camelCase")]
2396pub struct PasskeyAuthResponse {
2397 pub options: serde_json::Value,
2398 pub request_uri: String,
2399}
2400
2401pub async fn authorize_passkey_start(
2402 State(state): State<AppState>,
2403 Query(query): Query<AuthorizePasskeyQuery>,
2404) -> Response {
2405 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2406
2407 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
2408 Ok(Some(d)) => d,
2409 Ok(None) => {
2410 return (
2411 StatusCode::BAD_REQUEST,
2412 Json(serde_json::json!({
2413 "error": "invalid_request",
2414 "error_description": "Authorization request not found."
2415 })),
2416 )
2417 .into_response();
2418 }
2419 Err(_) => {
2420 return (
2421 StatusCode::INTERNAL_SERVER_ERROR,
2422 Json(serde_json::json!({
2423 "error": "server_error",
2424 "error_description": "An error occurred."
2425 })),
2426 )
2427 .into_response();
2428 }
2429 };
2430
2431 if request_data.expires_at < Utc::now() {
2432 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await;
2433 return (
2434 StatusCode::BAD_REQUEST,
2435 Json(serde_json::json!({
2436 "error": "invalid_request",
2437 "error_description": "Authorization request has expired."
2438 })),
2439 )
2440 .into_response();
2441 }
2442
2443 let did = match &request_data.did {
2444 Some(d) => d.clone(),
2445 None => {
2446 return (
2447 StatusCode::BAD_REQUEST,
2448 Json(serde_json::json!({
2449 "error": "invalid_request",
2450 "error_description": "User not authenticated yet."
2451 })),
2452 )
2453 .into_response();
2454 }
2455 };
2456
2457 let stored_passkeys = match crate::auth::webauthn::get_passkeys_for_user(&state.db, &did).await
2458 {
2459 Ok(pks) => pks,
2460 Err(e) => {
2461 tracing::error!("Failed to get passkeys: {:?}", e);
2462 return (
2463 StatusCode::INTERNAL_SERVER_ERROR,
2464 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2465 )
2466 .into_response();
2467 }
2468 };
2469
2470 if stored_passkeys.is_empty() {
2471 return (
2472 StatusCode::BAD_REQUEST,
2473 Json(serde_json::json!({
2474 "error": "invalid_request",
2475 "error_description": "No passkeys registered for this account."
2476 })),
2477 )
2478 .into_response();
2479 }
2480
2481 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
2482 .iter()
2483 .filter_map(|sp| sp.to_security_key().ok())
2484 .collect();
2485
2486 if passkeys.is_empty() {
2487 return (
2488 StatusCode::INTERNAL_SERVER_ERROR,
2489 Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})),
2490 )
2491 .into_response();
2492 }
2493
2494 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2495 Ok(w) => w,
2496 Err(e) => {
2497 tracing::error!("Failed to create WebAuthn config: {:?}", e);
2498 return (
2499 StatusCode::INTERNAL_SERVER_ERROR,
2500 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2501 )
2502 .into_response();
2503 }
2504 };
2505
2506 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
2507 Ok(result) => result,
2508 Err(e) => {
2509 tracing::error!("Failed to start passkey authentication: {:?}", e);
2510 return (
2511 StatusCode::INTERNAL_SERVER_ERROR,
2512 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2513 )
2514 .into_response();
2515 }
2516 };
2517
2518 if let Err(e) =
2519 crate::auth::webauthn::save_authentication_state(&state.db, &did, &auth_state).await
2520 {
2521 tracing::error!("Failed to save authentication state: {:?}", e);
2522 return (
2523 StatusCode::INTERNAL_SERVER_ERROR,
2524 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2525 )
2526 .into_response();
2527 }
2528
2529 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
2530 Json(PasskeyAuthResponse {
2531 options,
2532 request_uri: query.request_uri,
2533 })
2534 .into_response()
2535}
2536
2537#[derive(Debug, Deserialize)]
2538#[serde(rename_all = "camelCase")]
2539pub struct AuthorizePasskeySubmit {
2540 pub request_uri: String,
2541 pub credential: serde_json::Value,
2542}
2543
2544pub async fn authorize_passkey_finish(
2545 State(state): State<AppState>,
2546 headers: HeaderMap,
2547 Json(form): Json<AuthorizePasskeySubmit>,
2548) -> Response {
2549 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2550
2551 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
2552 Ok(Some(d)) => d,
2553 Ok(None) => {
2554 return (
2555 StatusCode::BAD_REQUEST,
2556 Json(serde_json::json!({
2557 "error": "invalid_request",
2558 "error_description": "Authorization request not found."
2559 })),
2560 )
2561 .into_response();
2562 }
2563 Err(_) => {
2564 return (
2565 StatusCode::INTERNAL_SERVER_ERROR,
2566 Json(serde_json::json!({
2567 "error": "server_error",
2568 "error_description": "An error occurred."
2569 })),
2570 )
2571 .into_response();
2572 }
2573 };
2574
2575 if request_data.expires_at < Utc::now() {
2576 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
2577 return (
2578 StatusCode::BAD_REQUEST,
2579 Json(serde_json::json!({
2580 "error": "invalid_request",
2581 "error_description": "Authorization request has expired."
2582 })),
2583 )
2584 .into_response();
2585 }
2586
2587 let did = match &request_data.did {
2588 Some(d) => d.clone(),
2589 None => {
2590 return (
2591 StatusCode::BAD_REQUEST,
2592 Json(serde_json::json!({
2593 "error": "invalid_request",
2594 "error_description": "User not authenticated yet."
2595 })),
2596 )
2597 .into_response();
2598 }
2599 };
2600
2601 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await {
2602 Ok(Some(s)) => s,
2603 Ok(None) => {
2604 return (
2605 StatusCode::BAD_REQUEST,
2606 Json(serde_json::json!({
2607 "error": "invalid_request",
2608 "error_description": "No passkey challenge found. Please start over."
2609 })),
2610 )
2611 .into_response();
2612 }
2613 Err(e) => {
2614 tracing::error!("Failed to load authentication state: {:?}", e);
2615 return (
2616 StatusCode::INTERNAL_SERVER_ERROR,
2617 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2618 )
2619 .into_response();
2620 }
2621 };
2622
2623 let credential: webauthn_rs::prelude::PublicKeyCredential =
2624 match serde_json::from_value(form.credential.clone()) {
2625 Ok(c) => c,
2626 Err(e) => {
2627 tracing::error!("Failed to parse credential: {:?}", e);
2628 return (
2629 StatusCode::BAD_REQUEST,
2630 Json(serde_json::json!({
2631 "error": "invalid_request",
2632 "error_description": "Invalid credential format."
2633 })),
2634 )
2635 .into_response();
2636 }
2637 };
2638
2639 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
2640 Ok(w) => w,
2641 Err(e) => {
2642 tracing::error!("Failed to create WebAuthn config: {:?}", e);
2643 return (
2644 StatusCode::INTERNAL_SERVER_ERROR,
2645 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2646 )
2647 .into_response();
2648 }
2649 };
2650
2651 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
2652 Ok(r) => r,
2653 Err(e) => {
2654 tracing::warn!("Passkey authentication failed: {:?}", e);
2655 return (
2656 StatusCode::FORBIDDEN,
2657 Json(serde_json::json!({
2658 "error": "access_denied",
2659 "error_description": "Passkey authentication failed."
2660 })),
2661 )
2662 .into_response();
2663 }
2664 };
2665
2666 let _ = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await;
2667
2668 match crate::auth::webauthn::update_passkey_counter(
2669 &state.db,
2670 credential.id.as_ref(),
2671 auth_result.counter(),
2672 )
2673 .await
2674 {
2675 Ok(false) => {
2676 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key");
2677 return (
2678 StatusCode::FORBIDDEN,
2679 Json(serde_json::json!({
2680 "error": "access_denied",
2681 "error_description": "Security key counter anomaly detected. This may indicate a cloned key."
2682 })),
2683 )
2684 .into_response();
2685 }
2686 Err(e) => {
2687 tracing::warn!("Failed to update passkey counter: {:?}", e);
2688 }
2689 Ok(true) => {}
2690 }
2691
2692 let has_totp = crate::api::server::has_totp_enabled_db(&state.db, &did).await;
2693 if has_totp {
2694 let device_cookie = extract_device_cookie(&headers);
2695 let device_is_trusted = if let Some(ref dev_id) = device_cookie {
2696 crate::api::server::is_device_trusted(&state.db, dev_id, &did).await
2697 } else {
2698 false
2699 };
2700
2701 if device_is_trusted {
2702 if let Some(ref dev_id) = device_cookie {
2703 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await;
2704 }
2705 } else {
2706 let user = match sqlx::query!(
2707 r#"SELECT id, preferred_comms_channel as "preferred_comms_channel: CommsChannel" FROM users WHERE did = $1"#,
2708 did
2709 )
2710 .fetch_optional(&state.db)
2711 .await
2712 {
2713 Ok(Some(u)) => u,
2714 _ => {
2715 return (
2716 StatusCode::INTERNAL_SERVER_ERROR,
2717 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2718 )
2719 .into_response();
2720 }
2721 };
2722
2723 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
2724 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await {
2725 Ok(challenge) => {
2726 if let Err(e) =
2727 enqueue_2fa_code(&state.db, user.id, &challenge.code, &pds_hostname).await
2728 {
2729 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
2730 }
2731 let channel_name = channel_display_name(user.preferred_comms_channel);
2732 let redirect_url = format!(
2733 "/app/oauth/2fa?request_uri={}&channel={}",
2734 url_encode(&form.request_uri),
2735 url_encode(channel_name)
2736 );
2737 return (
2738 StatusCode::OK,
2739 Json(serde_json::json!({
2740 "next": "2fa",
2741 "redirect": redirect_url
2742 })),
2743 )
2744 .into_response();
2745 }
2746 Err(_) => {
2747 return (
2748 StatusCode::INTERNAL_SERVER_ERROR,
2749 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2750 )
2751 .into_response();
2752 }
2753 }
2754 }
2755 }
2756
2757 let redirect_url = format!(
2758 "/app/oauth/consent?request_uri={}",
2759 url_encode(&form.request_uri)
2760 );
2761 (
2762 StatusCode::OK,
2763 Json(serde_json::json!({
2764 "next": "consent",
2765 "redirect": redirect_url
2766 })),
2767 )
2768 .into_response()
2769}