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