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