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 for cookie in cookie_str.split(';') {
106 let cookie = cookie.trim();
107 if let Some(value) = cookie.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) {
108 return crate::config::AuthConfig::get().verify_device_cookie(value);
109 }
110 }
111 None
112 })
113}
114
115fn extract_client_ip(headers: &HeaderMap) -> String {
116 if let Some(forwarded) = headers.get("x-forwarded-for")
117 && let Ok(value) = forwarded.to_str()
118 && let Some(first_ip) = value.split(',').next()
119 {
120 return first_ip.trim().to_string();
121 }
122 if let Some(real_ip) = headers.get("x-real-ip")
123 && let Ok(value) = real_ip.to_str()
124 {
125 return value.trim().to_string();
126 }
127 "0.0.0.0".to_string()
128}
129
130fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
131 headers
132 .get("user-agent")
133 .and_then(|v| v.to_str().ok())
134 .map(|s| s.to_string())
135}
136
137fn make_device_cookie(device_id: &str) -> String {
138 let signed_value = crate::config::AuthConfig::get().sign_device_cookie(device_id);
139 format!(
140 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000",
141 DEVICE_COOKIE_NAME, signed_value
142 )
143}
144
145#[derive(Debug, Deserialize)]
146pub struct AuthorizeQuery {
147 pub request_uri: Option<String>,
148 pub client_id: Option<String>,
149 pub new_account: Option<bool>,
150}
151
152#[derive(Debug, Serialize)]
153pub struct AuthorizeResponse {
154 pub client_id: String,
155 pub client_name: Option<String>,
156 pub scope: Option<String>,
157 pub redirect_uri: String,
158 pub state: Option<String>,
159 pub login_hint: Option<String>,
160}
161
162#[derive(Debug, Deserialize)]
163pub struct AuthorizeSubmit {
164 pub request_uri: String,
165 pub username: String,
166 pub password: PlainPassword,
167 #[serde(default)]
168 pub remember_device: bool,
169}
170
171#[derive(Debug, Deserialize)]
172pub struct AuthorizeSelectSubmit {
173 pub request_uri: String,
174 pub did: String,
175}
176
177fn wants_json(headers: &HeaderMap) -> bool {
178 headers
179 .get("accept")
180 .and_then(|v| v.to_str().ok())
181 .map(|accept| accept.contains("application/json"))
182 .unwrap_or(false)
183}
184
185pub async fn authorize_get(
186 State(state): State<AppState>,
187 headers: HeaderMap,
188 Query(query): Query<AuthorizeQuery>,
189) -> Response {
190 let request_uri = match query.request_uri {
191 Some(uri) => uri,
192 None => {
193 if wants_json(&headers) {
194 return (
195 StatusCode::BAD_REQUEST,
196 Json(serde_json::json!({
197 "error": "invalid_request",
198 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization."
199 })),
200 ).into_response();
201 }
202 return redirect_to_frontend_error(
203 "invalid_request",
204 "Missing request_uri parameter. Use PAR to initiate authorization.",
205 );
206 }
207 };
208 let request_data = match db::get_authorization_request(&state.db, &request_uri).await {
209 Ok(Some(data)) => data,
210 Ok(None) => {
211 if wants_json(&headers) {
212 return (
213 StatusCode::BAD_REQUEST,
214 Json(serde_json::json!({
215 "error": "invalid_request",
216 "error_description": "Invalid or expired request_uri. Please start a new authorization request."
217 })),
218 ).into_response();
219 }
220 return redirect_to_frontend_error(
221 "invalid_request",
222 "Invalid or expired request_uri. Please start a new authorization request.",
223 );
224 }
225 Err(e) => {
226 if wants_json(&headers) {
227 return (
228 StatusCode::INTERNAL_SERVER_ERROR,
229 Json(serde_json::json!({
230 "error": "server_error",
231 "error_description": format!("Database error: {:?}", e)
232 })),
233 )
234 .into_response();
235 }
236 return redirect_to_frontend_error("server_error", "A database error occurred.");
237 }
238 };
239 if request_data.expires_at < Utc::now() {
240 let _ = db::delete_authorization_request(&state.db, &request_uri).await;
241 if wants_json(&headers) {
242 return (
243 StatusCode::BAD_REQUEST,
244 Json(serde_json::json!({
245 "error": "invalid_request",
246 "error_description": "Authorization request has expired. Please start a new request."
247 })),
248 ).into_response();
249 }
250 return redirect_to_frontend_error(
251 "invalid_request",
252 "Authorization request has expired. Please start a new request.",
253 );
254 }
255 let client_cache = ClientMetadataCache::new(3600);
256 let client_name = client_cache
257 .get(&request_data.parameters.client_id)
258 .await
259 .ok()
260 .and_then(|m| m.client_name);
261 if wants_json(&headers) {
262 return Json(AuthorizeResponse {
263 client_id: request_data.parameters.client_id.clone(),
264 client_name: client_name.clone(),
265 scope: request_data.parameters.scope.clone(),
266 redirect_uri: request_data.parameters.redirect_uri.clone(),
267 state: request_data.parameters.state.clone(),
268 login_hint: request_data.parameters.login_hint.clone(),
269 })
270 .into_response();
271 }
272 let force_new_account = query.new_account.unwrap_or(false);
273
274 if let Some(ref login_hint) = request_data.parameters.login_hint {
275 tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation");
276 let pds_hostname =
277 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
278 let normalized = if login_hint.contains('@') || login_hint.starts_with("did:") {
279 login_hint.clone()
280 } else if !login_hint.contains('.') {
281 format!("{}.{}", login_hint.to_lowercase(), pds_hostname)
282 } else {
283 login_hint.to_lowercase()
284 };
285 tracing::info!(normalized = %normalized, "Normalized login_hint");
286
287 match sqlx::query!(
288 "SELECT did, password_hash FROM users WHERE handle = $1 OR email = $1",
289 normalized
290 )
291 .fetch_optional(&state.db)
292 .await
293 {
294 Ok(Some(user)) => {
295 tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint");
296 let is_delegated = crate::delegation::is_delegated_account(&state.db, &user.did)
297 .await
298 .unwrap_or(false);
299 let has_password = user.password_hash.is_some();
300 tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check");
301
302 if is_delegated && !has_password {
303 tracing::info!("Redirecting to delegation auth");
304 return redirect_see_other(&format!(
305 "/app/oauth/delegation?request_uri={}&delegated_did={}",
306 url_encode(&request_uri),
307 url_encode(&user.did)
308 ));
309 }
310 }
311 Ok(None) => {
312 tracing::info!(normalized = %normalized, "No user found for login_hint");
313 }
314 Err(e) => {
315 tracing::error!(error = %e, "Error looking up user for login_hint");
316 }
317 }
318 } else {
319 tracing::info!("No login_hint in request");
320 }
321
322 if !force_new_account
323 && let Some(device_id) = extract_device_cookie(&headers)
324 && let Ok(accounts) = db::get_device_accounts(&state.db, &device_id).await
325 && !accounts.is_empty()
326 {
327 return redirect_see_other(&format!(
328 "/app/oauth/accounts?request_uri={}",
329 url_encode(&request_uri)
330 ));
331 }
332 redirect_see_other(&format!(
333 "/app/oauth/login?request_uri={}",
334 url_encode(&request_uri)
335 ))
336}
337
338pub async fn authorize_get_json(
339 State(state): State<AppState>,
340 Query(query): Query<AuthorizeQuery>,
341) -> Result<Json<AuthorizeResponse>, OAuthError> {
342 let request_uri = query
343 .request_uri
344 .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?;
345 let request_data = db::get_authorization_request(&state.db, &request_uri)
346 .await?
347 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
348 if request_data.expires_at < Utc::now() {
349 db::delete_authorization_request(&state.db, &request_uri).await?;
350 return Err(OAuthError::InvalidRequest(
351 "request_uri has expired".to_string(),
352 ));
353 }
354 Ok(Json(AuthorizeResponse {
355 client_id: request_data.parameters.client_id.clone(),
356 client_name: None,
357 scope: request_data.parameters.scope.clone(),
358 redirect_uri: request_data.parameters.redirect_uri.clone(),
359 state: request_data.parameters.state.clone(),
360 login_hint: request_data.parameters.login_hint.clone(),
361 }))
362}
363
364#[derive(Debug, Serialize)]
365pub struct AccountInfo {
366 pub did: String,
367 pub handle: Handle,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub email: Option<String>,
370}
371
372#[derive(Debug, Serialize)]
373pub struct AccountsResponse {
374 pub accounts: Vec<AccountInfo>,
375 pub request_uri: String,
376}
377
378fn mask_email(email: &str) -> String {
379 if let Some(at_pos) = email.find('@') {
380 let local = &email[..at_pos];
381 let domain = &email[at_pos..];
382 if local.len() <= 2 {
383 format!("{}***{}", local.chars().next().unwrap_or('*'), domain)
384 } else {
385 let first = local.chars().next().unwrap_or('*');
386 let last = local.chars().last().unwrap_or('*');
387 format!("{}***{}{}", first, last, domain)
388 }
389 } else {
390 "***".to_string()
391 }
392}
393
394pub async fn authorize_accounts(
395 State(state): State<AppState>,
396 headers: HeaderMap,
397 Query(query): Query<AuthorizeQuery>,
398) -> Response {
399 let request_uri = match query.request_uri {
400 Some(uri) => uri,
401 None => {
402 return (
403 StatusCode::BAD_REQUEST,
404 Json(serde_json::json!({
405 "error": "invalid_request",
406 "error_description": "Missing request_uri parameter"
407 })),
408 )
409 .into_response();
410 }
411 };
412 let device_id = match extract_device_cookie(&headers) {
413 Some(id) => id,
414 None => {
415 return Json(AccountsResponse {
416 accounts: vec![],
417 request_uri,
418 })
419 .into_response();
420 }
421 };
422 let accounts = match db::get_device_accounts(&state.db, &device_id).await {
423 Ok(accts) => accts,
424 Err(_) => {
425 return Json(AccountsResponse {
426 accounts: vec![],
427 request_uri,
428 })
429 .into_response();
430 }
431 };
432 let account_infos: Vec<AccountInfo> = accounts
433 .into_iter()
434 .map(|row| AccountInfo {
435 did: row.did,
436 handle: row.handle,
437 email: row.email.map(|e| mask_email(&e)),
438 })
439 .collect();
440 Json(AccountsResponse {
441 accounts: account_infos,
442 request_uri,
443 })
444 .into_response()
445}
446
447pub async fn authorize_post(
448 State(state): State<AppState>,
449 headers: HeaderMap,
450 Json(form): Json<AuthorizeSubmit>,
451) -> Response {
452 let json_response = wants_json(&headers);
453 let client_ip = extract_client_ip(&headers);
454 if !state
455 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip)
456 .await
457 {
458 tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded");
459 if json_response {
460 return (
461 axum::http::StatusCode::TOO_MANY_REQUESTS,
462 Json(serde_json::json!({
463 "error": "RateLimitExceeded",
464 "error_description": "Too many login attempts. Please try again later."
465 })),
466 )
467 .into_response();
468 }
469 return redirect_to_frontend_error(
470 "RateLimitExceeded",
471 "Too many login attempts. Please try again later.",
472 );
473 }
474 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
475 Ok(Some(data)) => data,
476 Ok(None) => {
477 if json_response {
478 return (
479 axum::http::StatusCode::BAD_REQUEST,
480 Json(serde_json::json!({
481 "error": "invalid_request",
482 "error_description": "Invalid or expired request_uri."
483 })),
484 )
485 .into_response();
486 }
487 return redirect_to_frontend_error(
488 "invalid_request",
489 "Invalid or expired request_uri. Please start a new authorization request.",
490 );
491 }
492 Err(e) => {
493 if json_response {
494 return (
495 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
496 Json(serde_json::json!({
497 "error": "server_error",
498 "error_description": format!("Database error: {:?}", e)
499 })),
500 )
501 .into_response();
502 }
503 return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e));
504 }
505 };
506 if request_data.expires_at < Utc::now() {
507 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
508 if json_response {
509 return (
510 axum::http::StatusCode::BAD_REQUEST,
511 Json(serde_json::json!({
512 "error": "invalid_request",
513 "error_description": "Authorization request has expired."
514 })),
515 )
516 .into_response();
517 }
518 return redirect_to_frontend_error(
519 "invalid_request",
520 "Authorization request has expired. Please start a new request.",
521 );
522 }
523 let show_login_error = |error_msg: &str, json: bool| -> Response {
524 if json {
525 return (
526 axum::http::StatusCode::FORBIDDEN,
527 Json(serde_json::json!({
528 "error": "access_denied",
529 "error_description": error_msg
530 })),
531 )
532 .into_response();
533 }
534 redirect_see_other(&format!(
535 "/app/oauth/login?request_uri={}&error={}",
536 url_encode(&form.request_uri),
537 url_encode(error_msg)
538 ))
539 };
540 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
541 let normalized_username = form.username.trim();
542 let normalized_username = normalized_username
543 .strip_prefix('@')
544 .unwrap_or(normalized_username);
545 let normalized_username = if normalized_username.contains('@') {
546 normalized_username.to_string()
547 } else if !normalized_username.contains('.') {
548 format!("{}.{}", normalized_username, pds_hostname)
549 } else {
550 normalized_username.to_string()
551 };
552 tracing::debug!(
553 original_username = %form.username,
554 normalized_username = %normalized_username,
555 pds_hostname = %pds_hostname,
556 "Normalized username for lookup"
557 );
558 let user = match sqlx::query!(
559 r#"
560 SELECT id, did, email, password_hash, password_required, two_factor_enabled,
561 preferred_comms_channel as "preferred_comms_channel: CommsChannel",
562 deactivated_at, takedown_ref,
563 email_verified, discord_verified, telegram_verified, signal_verified,
564 account_type::text as "account_type!"
565 FROM users
566 WHERE handle = $1 OR email = $1
567 "#,
568 normalized_username
569 )
570 .fetch_optional(&state.db)
571 .await
572 {
573 Ok(Some(u)) => u,
574 Ok(None) => {
575 let _ = bcrypt::verify(
576 &form.password,
577 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK",
578 );
579 return show_login_error("Invalid handle/email or password.", json_response);
580 }
581 Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
582 };
583 if user.deactivated_at.is_some() {
584 return show_login_error("This account has been deactivated.", json_response);
585 }
586 if user.takedown_ref.is_some() {
587 return show_login_error("This account has been taken down.", json_response);
588 }
589 let is_verified = user.email_verified
590 || user.discord_verified
591 || user.telegram_verified
592 || user.signal_verified;
593 if !is_verified {
594 return show_login_error(
595 "Please verify your account before logging in.",
596 json_response,
597 );
598 }
599
600 if user.account_type == "delegated" {
601 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
602 .await
603 .is_err()
604 {
605 return show_login_error("An error occurred. Please try again.", json_response);
606 }
607 let redirect_url = format!(
608 "/app/oauth/delegation?request_uri={}&delegated_did={}",
609 url_encode(&form.request_uri),
610 url_encode(&user.did)
611 );
612 if json_response {
613 return (
614 StatusCode::OK,
615 Json(serde_json::json!({
616 "next": "delegation",
617 "delegated_did": user.did,
618 "redirect": redirect_url
619 })),
620 )
621 .into_response();
622 }
623 return redirect_see_other(&redirect_url);
624 }
625
626 if !user.password_required {
627 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
628 .await
629 .is_err()
630 {
631 return show_login_error("An error occurred. Please try again.", json_response);
632 }
633 let redirect_url = format!(
634 "/app/oauth/passkey?request_uri={}",
635 url_encode(&form.request_uri)
636 );
637 if json_response {
638 return (
639 StatusCode::OK,
640 Json(serde_json::json!({
641 "next": "passkey",
642 "redirect": redirect_url
643 })),
644 )
645 .into_response();
646 }
647 return redirect_see_other(&redirect_url);
648 }
649
650 let password_valid = match &user.password_hash {
651 Some(hash) => match bcrypt::verify(&form.password, hash) {
652 Ok(valid) => valid,
653 Err(_) => {
654 return show_login_error("An error occurred. Please try again.", json_response);
655 }
656 },
657 None => false,
658 };
659 if !password_valid {
660 return show_login_error("Invalid handle/email or password.", json_response);
661 }
662 let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await;
663 if has_totp {
664 let device_cookie = extract_device_cookie(&headers);
665 let device_is_trusted = if let Some(ref dev_id) = device_cookie {
666 crate::api::server::is_device_trusted(&state.db, dev_id, &user.did).await
667 } else {
668 false
669 };
670
671 if device_is_trusted {
672 if let Some(ref dev_id) = device_cookie {
673 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await;
674 }
675 } else {
676 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None)
677 .await
678 .is_err()
679 {
680 return show_login_error("An error occurred. Please try again.", json_response);
681 }
682 if json_response {
683 return Json(serde_json::json!({
684 "needs_totp": true
685 }))
686 .into_response();
687 }
688 return redirect_see_other(&format!(
689 "/app/oauth/totp?request_uri={}",
690 url_encode(&form.request_uri)
691 ));
692 }
693 }
694 if user.two_factor_enabled {
695 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
696 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await {
697 Ok(challenge) => {
698 let hostname =
699 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
700 if let Err(e) =
701 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
702 {
703 tracing::warn!(
704 did = %user.did,
705 error = %e,
706 "Failed to enqueue 2FA notification"
707 );
708 }
709 let channel_name = channel_display_name(user.preferred_comms_channel);
710 if json_response {
711 return Json(serde_json::json!({
712 "needs_2fa": true,
713 "channel": channel_name
714 }))
715 .into_response();
716 }
717 return redirect_see_other(&format!(
718 "/app/oauth/2fa?request_uri={}&channel={}",
719 url_encode(&form.request_uri),
720 url_encode(channel_name)
721 ));
722 }
723 Err(_) => {
724 return show_login_error("An error occurred. Please try again.", json_response);
725 }
726 }
727 }
728 let mut device_id: Option<String> = extract_device_cookie(&headers);
729 let mut new_cookie: Option<String> = None;
730 if form.remember_device {
731 let final_device_id = if let Some(existing_id) = &device_id {
732 existing_id.clone()
733 } else {
734 let new_id = DeviceId::generate();
735 let device_data = DeviceData {
736 session_id: SessionId::generate().0,
737 user_agent: extract_user_agent(&headers),
738 ip_address: extract_client_ip(&headers),
739 last_seen_at: Utc::now(),
740 };
741 if db::create_device(&state.db, &new_id.0, &device_data)
742 .await
743 .is_ok()
744 {
745 new_cookie = Some(make_device_cookie(&new_id.0));
746 device_id = Some(new_id.0.clone());
747 }
748 new_id.0
749 };
750 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await;
751 }
752 if db::set_authorization_did(
753 &state.db,
754 &form.request_uri,
755 &user.did,
756 device_id.as_deref(),
757 )
758 .await
759 .is_err()
760 {
761 return show_login_error("An error occurred. Please try again.", json_response);
762 }
763 let requested_scope_str = request_data
764 .parameters
765 .scope
766 .as_deref()
767 .unwrap_or("atproto");
768 let requested_scopes: Vec<String> = requested_scope_str
769 .split_whitespace()
770 .map(|s| s.to_string())
771 .collect();
772 let needs_consent = db::should_show_consent(
773 &state.db,
774 &user.did,
775 &request_data.parameters.client_id,
776 &requested_scopes,
777 )
778 .await
779 .unwrap_or(true);
780 if needs_consent {
781 let consent_url = format!(
782 "/app/oauth/consent?request_uri={}",
783 url_encode(&form.request_uri)
784 );
785 if json_response {
786 if let Some(cookie) = new_cookie {
787 return (
788 StatusCode::OK,
789 [(SET_COOKIE, cookie)],
790 Json(serde_json::json!({"redirect_uri": consent_url})),
791 )
792 .into_response();
793 }
794 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
795 }
796 if let Some(cookie) = new_cookie {
797 return (
798 StatusCode::SEE_OTHER,
799 [(SET_COOKIE, cookie), (LOCATION, consent_url)],
800 )
801 .into_response();
802 }
803 return redirect_see_other(&consent_url);
804 }
805 let code = Code::generate();
806 if db::update_authorization_request(
807 &state.db,
808 &form.request_uri,
809 &user.did,
810 device_id.as_deref(),
811 &code.0,
812 )
813 .await
814 .is_err()
815 {
816 return show_login_error("An error occurred. Please try again.", json_response);
817 }
818 if json_response {
819 let redirect_url = build_intermediate_redirect_url(
820 &request_data.parameters.redirect_uri,
821 &code.0,
822 request_data.parameters.state.as_deref(),
823 request_data.parameters.response_mode.as_deref(),
824 );
825 if let Some(cookie) = new_cookie {
826 (
827 StatusCode::OK,
828 [(SET_COOKIE, cookie)],
829 Json(serde_json::json!({"redirect_uri": redirect_url})),
830 )
831 .into_response()
832 } else {
833 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response()
834 }
835 } else {
836 let redirect_url = build_success_redirect(
837 &request_data.parameters.redirect_uri,
838 &code.0,
839 request_data.parameters.state.as_deref(),
840 request_data.parameters.response_mode.as_deref(),
841 );
842 if let Some(cookie) = new_cookie {
843 (
844 StatusCode::SEE_OTHER,
845 [(SET_COOKIE, cookie), (LOCATION, redirect_url)],
846 )
847 .into_response()
848 } else {
849 redirect_see_other(&redirect_url)
850 }
851 }
852}
853
854pub async fn authorize_select(
855 State(state): State<AppState>,
856 headers: HeaderMap,
857 Json(form): Json<AuthorizeSelectSubmit>,
858) -> Response {
859 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
860 (
861 status,
862 Json(serde_json::json!({
863 "error": error,
864 "error_description": description
865 })),
866 )
867 .into_response()
868 };
869 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
870 Ok(Some(data)) => data,
871 Ok(None) => {
872 return json_error(
873 StatusCode::BAD_REQUEST,
874 "invalid_request",
875 "Invalid or expired request_uri. Please start a new authorization request.",
876 );
877 }
878 Err(_) => {
879 return json_error(
880 StatusCode::INTERNAL_SERVER_ERROR,
881 "server_error",
882 "An error occurred. Please try again.",
883 );
884 }
885 };
886 if request_data.expires_at < Utc::now() {
887 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
888 return json_error(
889 StatusCode::BAD_REQUEST,
890 "invalid_request",
891 "Authorization request has expired. Please start a new request.",
892 );
893 }
894 let device_id = match extract_device_cookie(&headers) {
895 Some(id) => id,
896 None => {
897 return json_error(
898 StatusCode::BAD_REQUEST,
899 "invalid_request",
900 "No device session found. Please sign in.",
901 );
902 }
903 };
904 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await {
905 Ok(valid) => valid,
906 Err(_) => {
907 return json_error(
908 StatusCode::INTERNAL_SERVER_ERROR,
909 "server_error",
910 "An error occurred. Please try again.",
911 );
912 }
913 };
914 if !account_valid {
915 return json_error(
916 StatusCode::FORBIDDEN,
917 "access_denied",
918 "This account is not available on this device. Please sign in.",
919 );
920 }
921 let user = match sqlx::query!(
922 r#"
923 SELECT id, two_factor_enabled,
924 preferred_comms_channel as "preferred_comms_channel: CommsChannel",
925 email_verified, discord_verified, telegram_verified, signal_verified
926 FROM users
927 WHERE did = $1
928 "#,
929 form.did
930 )
931 .fetch_optional(&state.db)
932 .await
933 {
934 Ok(Some(u)) => u,
935 Ok(None) => {
936 return json_error(
937 StatusCode::FORBIDDEN,
938 "access_denied",
939 "Account not found. Please sign in.",
940 );
941 }
942 Err(_) => {
943 return json_error(
944 StatusCode::INTERNAL_SERVER_ERROR,
945 "server_error",
946 "An error occurred. Please try again.",
947 );
948 }
949 };
950 let is_verified = user.email_verified
951 || user.discord_verified
952 || user.telegram_verified
953 || user.signal_verified;
954 if !is_verified {
955 return json_error(
956 StatusCode::FORBIDDEN,
957 "access_denied",
958 "Please verify your account before logging in.",
959 );
960 }
961 let has_totp = crate::api::server::has_totp_enabled(&state, &form.did).await;
962 if has_totp {
963 if db::set_authorization_did(&state.db, &form.request_uri, &form.did, Some(&device_id))
964 .await
965 .is_err()
966 {
967 return json_error(
968 StatusCode::INTERNAL_SERVER_ERROR,
969 "server_error",
970 "An error occurred. Please try again.",
971 );
972 }
973 return Json(serde_json::json!({
974 "needs_totp": true
975 }))
976 .into_response();
977 }
978 if user.two_factor_enabled {
979 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
980 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await {
981 Ok(challenge) => {
982 let hostname =
983 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
984 if let Err(e) =
985 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await
986 {
987 tracing::warn!(
988 did = %form.did,
989 error = %e,
990 "Failed to enqueue 2FA notification"
991 );
992 }
993 let channel_name = channel_display_name(user.preferred_comms_channel);
994 return Json(serde_json::json!({
995 "needs_2fa": true,
996 "channel": channel_name
997 }))
998 .into_response();
999 }
1000 Err(_) => {
1001 return json_error(
1002 StatusCode::INTERNAL_SERVER_ERROR,
1003 "server_error",
1004 "An error occurred. Please try again.",
1005 );
1006 }
1007 }
1008 }
1009 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await;
1010 let code = Code::generate();
1011 if db::update_authorization_request(
1012 &state.db,
1013 &form.request_uri,
1014 &form.did,
1015 Some(&device_id),
1016 &code.0,
1017 )
1018 .await
1019 .is_err()
1020 {
1021 return json_error(
1022 StatusCode::INTERNAL_SERVER_ERROR,
1023 "server_error",
1024 "An error occurred. Please try again.",
1025 );
1026 }
1027 let redirect_url = build_intermediate_redirect_url(
1028 &request_data.parameters.redirect_uri,
1029 &code.0,
1030 request_data.parameters.state.as_deref(),
1031 request_data.parameters.response_mode.as_deref(),
1032 );
1033 Json(serde_json::json!({
1034 "redirect_uri": redirect_url
1035 }))
1036 .into_response()
1037}
1038
1039fn build_success_redirect(
1040 redirect_uri: &str,
1041 code: &str,
1042 state: Option<&str>,
1043 response_mode: Option<&str>,
1044) -> String {
1045 let mut redirect_url = redirect_uri.to_string();
1046 let use_fragment = response_mode == Some("fragment");
1047 let separator = if use_fragment {
1048 '#'
1049 } else if redirect_url.contains('?') {
1050 '&'
1051 } else {
1052 '?'
1053 };
1054 redirect_url.push(separator);
1055 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1056 redirect_url.push_str(&format!(
1057 "iss={}",
1058 url_encode(&format!("https://{}", pds_hostname))
1059 ));
1060 if let Some(req_state) = state {
1061 redirect_url.push_str(&format!("&state={}", url_encode(req_state)));
1062 }
1063 redirect_url.push_str(&format!("&code={}", url_encode(code)));
1064 redirect_url
1065}
1066
1067fn build_intermediate_redirect_url(
1068 redirect_uri: &str,
1069 code: &str,
1070 state: Option<&str>,
1071 response_mode: Option<&str>,
1072) -> String {
1073 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1074 let mut url = format!(
1075 "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}",
1076 pds_hostname,
1077 url_encode(redirect_uri),
1078 url_encode(code)
1079 );
1080 if let Some(s) = state {
1081 url.push_str(&format!("&state={}", url_encode(s)));
1082 }
1083 if let Some(rm) = response_mode {
1084 url.push_str(&format!("&response_mode={}", url_encode(rm)));
1085 }
1086 url
1087}
1088
1089#[derive(Debug, Deserialize)]
1090pub struct AuthorizeRedirectParams {
1091 redirect_uri: String,
1092 code: String,
1093 state: Option<String>,
1094 response_mode: Option<String>,
1095}
1096
1097pub async fn authorize_redirect(Query(params): Query<AuthorizeRedirectParams>) -> Response {
1098 let final_url = build_success_redirect(
1099 ¶ms.redirect_uri,
1100 ¶ms.code,
1101 params.state.as_deref(),
1102 params.response_mode.as_deref(),
1103 );
1104 tracing::info!(
1105 final_url = %final_url,
1106 client_redirect = %params.redirect_uri,
1107 "authorize_redirect performing 303 redirect"
1108 );
1109 (
1110 StatusCode::SEE_OTHER,
1111 [
1112 (axum::http::header::LOCATION, final_url),
1113 (axum::http::header::CACHE_CONTROL, "no-store".to_string()),
1114 ],
1115 )
1116 .into_response()
1117}
1118
1119#[derive(Debug, Serialize)]
1120pub struct AuthorizeDenyResponse {
1121 pub error: String,
1122 pub error_description: String,
1123}
1124
1125pub async fn authorize_deny(
1126 State(state): State<AppState>,
1127 Json(form): Json<AuthorizeDenyForm>,
1128) -> Response {
1129 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await {
1130 Ok(Some(data)) => data,
1131 Ok(None) => {
1132 return (
1133 StatusCode::BAD_REQUEST,
1134 Json(serde_json::json!({
1135 "error": "invalid_request",
1136 "error_description": "Invalid request_uri"
1137 })),
1138 )
1139 .into_response();
1140 }
1141 Err(_) => {
1142 return (
1143 StatusCode::INTERNAL_SERVER_ERROR,
1144 Json(serde_json::json!({
1145 "error": "server_error",
1146 "error_description": "An error occurred"
1147 })),
1148 )
1149 .into_response();
1150 }
1151 };
1152 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await;
1153 let redirect_uri = &request_data.parameters.redirect_uri;
1154 let mut redirect_url = redirect_uri.to_string();
1155 let separator = if redirect_url.contains('?') { '&' } else { '?' };
1156 redirect_url.push(separator);
1157 redirect_url.push_str("error=access_denied");
1158 redirect_url.push_str("&error_description=User%20denied%20the%20request");
1159 if let Some(state) = &request_data.parameters.state {
1160 redirect_url.push_str(&format!("&state={}", url_encode(state)));
1161 }
1162 Json(serde_json::json!({
1163 "redirect_uri": redirect_url
1164 }))
1165 .into_response()
1166}
1167
1168#[derive(Debug, Deserialize)]
1169pub struct AuthorizeDenyForm {
1170 pub request_uri: String,
1171}
1172
1173#[derive(Debug, Deserialize)]
1174pub struct Authorize2faQuery {
1175 pub request_uri: String,
1176 pub channel: Option<String>,
1177}
1178
1179#[derive(Debug, Deserialize)]
1180pub struct Authorize2faSubmit {
1181 pub request_uri: String,
1182 pub code: String,
1183 #[serde(default)]
1184 pub trust_device: bool,
1185}
1186
1187const MAX_2FA_ATTEMPTS: i32 = 5;
1188
1189pub async fn authorize_2fa_get(
1190 State(state): State<AppState>,
1191 Query(query): Query<Authorize2faQuery>,
1192) -> Response {
1193 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await {
1194 Ok(Some(c)) => c,
1195 Ok(None) => {
1196 return redirect_to_frontend_error(
1197 "invalid_request",
1198 "No 2FA challenge found. Please start over.",
1199 );
1200 }
1201 Err(_) => {
1202 return redirect_to_frontend_error(
1203 "server_error",
1204 "An error occurred. Please try again.",
1205 );
1206 }
1207 };
1208 if challenge.expires_at < Utc::now() {
1209 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await;
1210 return redirect_to_frontend_error(
1211 "invalid_request",
1212 "2FA code has expired. Please start over.",
1213 );
1214 }
1215 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await {
1216 Ok(Some(d)) => d,
1217 Ok(None) => {
1218 return redirect_to_frontend_error(
1219 "invalid_request",
1220 "Authorization request not found. Please start over.",
1221 );
1222 }
1223 Err(_) => {
1224 return redirect_to_frontend_error(
1225 "server_error",
1226 "An error occurred. Please try again.",
1227 );
1228 }
1229 };
1230 let channel = query.channel.as_deref().unwrap_or("email");
1231 redirect_see_other(&format!(
1232 "/app/oauth/2fa?request_uri={}&channel={}",
1233 url_encode(&query.request_uri),
1234 url_encode(channel)
1235 ))
1236}
1237
1238#[derive(Debug, Serialize)]
1239pub struct ScopeInfo {
1240 pub scope: String,
1241 pub category: String,
1242 pub required: bool,
1243 pub description: String,
1244 pub display_name: String,
1245 pub granted: Option<bool>,
1246}
1247
1248#[derive(Debug, Serialize)]
1249pub struct ConsentResponse {
1250 pub request_uri: String,
1251 pub client_id: String,
1252 pub client_name: Option<String>,
1253 pub client_uri: Option<String>,
1254 pub logo_uri: Option<String>,
1255 pub scopes: Vec<ScopeInfo>,
1256 pub show_consent: bool,
1257 pub did: String,
1258 #[serde(skip_serializing_if = "Option::is_none")]
1259 pub is_delegation: Option<bool>,
1260 #[serde(skip_serializing_if = "Option::is_none")]
1261 pub controller_did: Option<String>,
1262 #[serde(skip_serializing_if = "Option::is_none")]
1263 pub controller_handle: Option<String>,
1264 #[serde(skip_serializing_if = "Option::is_none")]
1265 pub delegation_level: Option<String>,
1266}
1267
1268#[derive(Debug, Deserialize)]
1269pub struct ConsentQuery {
1270 pub request_uri: String,
1271}
1272
1273#[derive(Debug, Deserialize)]
1274pub struct ConsentSubmit {
1275 pub request_uri: String,
1276 pub approved_scopes: Vec<String>,
1277 pub remember: bool,
1278}
1279
1280pub async fn consent_get(
1281 State(state): State<AppState>,
1282 Query(query): Query<ConsentQuery>,
1283) -> Response {
1284 let (request_data, flow_state) =
1285 match db::get_authorization_request_with_state(&state.db, &query.request_uri).await {
1286 Ok(Some(result)) => result,
1287 Ok(None) => {
1288 return json_error(
1289 StatusCode::BAD_REQUEST,
1290 "invalid_request",
1291 "Invalid or expired request_uri",
1292 );
1293 }
1294 Err(e) => {
1295 return json_error(
1296 StatusCode::INTERNAL_SERVER_ERROR,
1297 "server_error",
1298 &format!("Database error: {:?}", e),
1299 );
1300 }
1301 };
1302
1303 if let Some(err_response) = validate_auth_flow_state(&flow_state, true) {
1304 if flow_state.is_expired() {
1305 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await;
1306 }
1307 return err_response;
1308 }
1309
1310 let did = flow_state.did().unwrap().to_string();
1311 let client_cache = ClientMetadataCache::new(3600);
1312 let client_metadata = client_cache
1313 .get(&request_data.parameters.client_id)
1314 .await
1315 .ok();
1316 let requested_scope_str = request_data
1317 .parameters
1318 .scope
1319 .as_deref()
1320 .filter(|s| !s.trim().is_empty())
1321 .unwrap_or("atproto");
1322
1323 let delegation_grant = if let Some(ref ctrl_did) = request_data.controller_did {
1324 crate::delegation::get_delegation(&state.db, &did, ctrl_did)
1325 .await
1326 .ok()
1327 .flatten()
1328 } else {
1329 None
1330 };
1331
1332 let effective_scope_str = if let Some(ref grant) = delegation_grant {
1333 crate::delegation::scopes::intersect_scopes(requested_scope_str, &grant.granted_scopes)
1334 } else {
1335 requested_scope_str.to_string()
1336 };
1337
1338 let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect();
1339 let preferences =
1340 db::get_scope_preferences(&state.db, &did, &request_data.parameters.client_id)
1341 .await
1342 .unwrap_or_default();
1343 let pref_map: std::collections::HashMap<_, _> = preferences
1344 .iter()
1345 .map(|p| (p.scope.as_str(), p.granted))
1346 .collect();
1347 let requested_scope_strings: Vec<String> =
1348 requested_scopes.iter().map(|s| s.to_string()).collect();
1349 let show_consent = db::should_show_consent(
1350 &state.db,
1351 &did,
1352 &request_data.parameters.client_id,
1353 &requested_scope_strings,
1354 )
1355 .await
1356 .unwrap_or(true);
1357 let mut scopes = Vec::new();
1358 for scope in &requested_scopes {
1359 let (category, required, description, display_name) =
1360 if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) {
1361 (
1362 def.category.display_name().to_string(),
1363 def.required,
1364 def.description.to_string(),
1365 def.display_name.to_string(),
1366 )
1367 } else if scope.starts_with("ref:") {
1368 (
1369 "Reference".to_string(),
1370 false,
1371 "Referenced scope".to_string(),
1372 scope.to_string(),
1373 )
1374 } else {
1375 (
1376 "Other".to_string(),
1377 false,
1378 format!("Access to {}", scope),
1379 scope.to_string(),
1380 )
1381 };
1382 let granted = pref_map.get(*scope).copied();
1383 scopes.push(ScopeInfo {
1384 scope: scope.to_string(),
1385 category,
1386 required,
1387 description,
1388 display_name,
1389 granted,
1390 });
1391 }
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}