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