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