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