forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
116 kB
3594 lines
1use axum::{
2 Json,
3 extract::{Query, State},
4 http::{
5 HeaderMap, StatusCode,
6 header::{LOCATION, SET_COOKIE},
7 },
8 response::{IntoResponse, Response},
9};
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use subtle::ConstantTimeEq;
13use tranquil_db_traits::{ScopePreference, WebauthnChallengeType};
14use tranquil_pds::auth::{BareLoginIdentifier, NormalizedLoginIdentifier};
15use tranquil_pds::comms::comms_repo::enqueue_2fa_code;
16use tranquil_pds::oauth::{
17 AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId,
18 db::should_show_consent, scopes::expand_include_scopes,
19};
20use tranquil_pds::rate_limit::{
21 OAuthAuthorizeLimit, OAuthRateLimited, OAuthRegisterCompleteLimit, TotpVerifyLimit,
22 check_user_rate_limit,
23};
24use tranquil_pds::state::AppState;
25use tranquil_pds::types::{Did, Handle, PlainPassword};
26use tranquil_pds::util::extract_client_ip;
27use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId};
28use urlencoding::encode as url_encode;
29
30const DEVICE_COOKIE_NAME: &str = "oauth_device_id";
31const RENEW_EXPIRY_SECONDS: i64 = 600;
32const MAX_RENEWAL_STALENESS_SECONDS: i64 = 3600;
33
34fn redirect_see_other(uri: &str) -> Response {
35 (
36 StatusCode::SEE_OTHER,
37 [
38 (LOCATION, uri.to_string()),
39 (axum::http::header::CACHE_CONTROL, "no-store".to_string()),
40 (
41 SET_COOKIE,
42 "bfCacheBypass=foo; max-age=1; SameSite=Lax".to_string(),
43 ),
44 ],
45 )
46 .into_response()
47}
48
49fn redirect_to_frontend_error(error: &str, description: &str) -> Response {
50 redirect_see_other(&format!(
51 "/app/oauth/error?error={}&error_description={}",
52 url_encode(error),
53 url_encode(description)
54 ))
55}
56
57fn json_error(status: StatusCode, error: &str, description: &str) -> Response {
58 (
59 status,
60 Json(serde_json::json!({
61 "error": error,
62 "error_description": description
63 })),
64 )
65 .into_response()
66}
67
68fn is_granular_scope(s: &str) -> bool {
69 s.starts_with("repo:")
70 || s.starts_with("repo?")
71 || s == "repo"
72 || s.starts_with("blob:")
73 || s.starts_with("blob?")
74 || s == "blob"
75 || s.starts_with("rpc:")
76 || s.starts_with("rpc?")
77 || s.starts_with("account:")
78 || s.starts_with("identity:")
79}
80
81fn is_valid_scope(s: &str) -> bool {
82 s == "atproto"
83 || s == "transition:generic"
84 || s == "transition:chat.bsky"
85 || s == "transition:email"
86 || is_granular_scope(s)
87 || s.starts_with("include:")
88}
89
90fn extract_device_cookie(headers: &HeaderMap) -> Option<tranquil_types::DeviceId> {
91 headers
92 .get("cookie")
93 .and_then(|v| v.to_str().ok())
94 .and_then(|cookie_str| {
95 cookie_str.split(';').map(|c| c.trim()).find_map(|cookie| {
96 cookie
97 .strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME))
98 .and_then(|value| {
99 tranquil_pds::config::AuthConfig::get().verify_device_cookie(value)
100 })
101 .map(tranquil_types::DeviceId::new)
102 })
103 })
104}
105
106fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
107 headers
108 .get("user-agent")
109 .and_then(|v| v.to_str().ok())
110 .map(|s| s.to_string())
111}
112
113fn make_device_cookie(device_id: &tranquil_types::DeviceId) -> String {
114 let signed_value =
115 tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id.as_str());
116 format!(
117 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000",
118 DEVICE_COOKIE_NAME, signed_value
119 )
120}
121
122#[derive(Debug, Deserialize)]
123pub struct AuthorizeQuery {
124 pub request_uri: Option<String>,
125 pub client_id: Option<String>,
126 pub new_account: Option<bool>,
127}
128
129#[derive(Debug, Serialize)]
130pub struct AuthorizeResponse {
131 pub client_id: String,
132 pub client_name: Option<String>,
133 pub scope: Option<String>,
134 pub redirect_uri: String,
135 pub state: Option<String>,
136 pub login_hint: Option<String>,
137}
138
139#[derive(Debug, Deserialize)]
140pub struct AuthorizeSubmit {
141 pub request_uri: String,
142 pub username: String,
143 pub password: PlainPassword,
144 #[serde(default)]
145 pub remember_device: bool,
146}
147
148#[derive(Debug, Deserialize)]
149pub struct AuthorizeSelectSubmit {
150 pub request_uri: String,
151 pub did: String,
152}
153
154fn wants_json(headers: &HeaderMap) -> bool {
155 headers
156 .get("accept")
157 .and_then(|v| v.to_str().ok())
158 .map(|accept| accept.contains("application/json"))
159 .unwrap_or(false)
160}
161
162pub async fn authorize_get(
163 State(state): State<AppState>,
164 headers: HeaderMap,
165 Query(query): Query<AuthorizeQuery>,
166) -> Response {
167 let request_uri = match query.request_uri {
168 Some(uri) => uri,
169 None => {
170 if wants_json(&headers) {
171 return (
172 StatusCode::BAD_REQUEST,
173 Json(serde_json::json!({
174 "error": "invalid_request",
175 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization."
176 })),
177 ).into_response();
178 }
179 return redirect_to_frontend_error(
180 "invalid_request",
181 "Missing request_uri parameter. Use PAR to initiate authorization.",
182 );
183 }
184 };
185 let request_id = RequestId::from(request_uri.clone());
186 let request_data = match state
187 .oauth_repo
188 .get_authorization_request(&request_id)
189 .await
190 {
191 Ok(Some(data)) => data,
192 Ok(None) => {
193 if wants_json(&headers) {
194 return (
195 StatusCode::BAD_REQUEST,
196 Json(serde_json::json!({
197 "error": "invalid_request",
198 "error_description": "Invalid or expired request_uri. Please start a new authorization request."
199 })),
200 ).into_response();
201 }
202 return redirect_to_frontend_error(
203 "invalid_request",
204 "Invalid or expired request_uri. Please start a new authorization request.",
205 );
206 }
207 Err(e) => {
208 if wants_json(&headers) {
209 return (
210 StatusCode::INTERNAL_SERVER_ERROR,
211 Json(serde_json::json!({
212 "error": "server_error",
213 "error_description": format!("Database error: {:?}", e)
214 })),
215 )
216 .into_response();
217 }
218 return redirect_to_frontend_error("server_error", "A database error occurred.");
219 }
220 };
221 if request_data.expires_at < Utc::now() {
222 let _ = state
223 .oauth_repo
224 .delete_authorization_request(&request_id)
225 .await;
226 if wants_json(&headers) {
227 return (
228 StatusCode::BAD_REQUEST,
229 Json(serde_json::json!({
230 "error": "invalid_request",
231 "error_description": "Authorization request has expired. Please start a new request."
232 })),
233 ).into_response();
234 }
235 return redirect_to_frontend_error(
236 "invalid_request",
237 "Authorization request has expired. Please start a new request.",
238 );
239 }
240 let client_cache = ClientMetadataCache::new(3600);
241 let client_name = client_cache
242 .get(&request_data.parameters.client_id)
243 .await
244 .ok()
245 .and_then(|m| m.client_name);
246 if wants_json(&headers) {
247 return Json(AuthorizeResponse {
248 client_id: request_data.parameters.client_id.clone(),
249 client_name: client_name.clone(),
250 scope: request_data.parameters.scope.clone(),
251 redirect_uri: request_data.parameters.redirect_uri.clone(),
252 state: request_data.parameters.state.clone(),
253 login_hint: request_data.parameters.login_hint.clone(),
254 })
255 .into_response();
256 }
257 let force_new_account = query.new_account.unwrap_or(false);
258
259 if let Some(ref login_hint) = request_data.parameters.login_hint {
260 tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation");
261 let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
262 let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles);
263 tracing::info!(normalized = %normalized, "Normalized login_hint");
264
265 match state
266 .user_repo
267 .get_login_check_by_handle_or_email(normalized.as_str())
268 .await
269 {
270 Ok(Some(user)) => {
271 tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint");
272 let is_delegated = state
273 .delegation_repo
274 .is_delegated_account(&user.did)
275 .await
276 .unwrap_or(false);
277 let has_password = user.password_hash.is_some();
278 tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check");
279
280 if is_delegated {
281 tracing::info!("Redirecting to delegation auth");
282 if let Err(e) = state
283 .oauth_repo
284 .set_request_did(&request_id, &user.did)
285 .await
286 {
287 tracing::error!(error = %e, "Failed to set delegated DID on authorization request");
288 return redirect_to_frontend_error(
289 "server_error",
290 "Failed to initialize delegation flow",
291 );
292 }
293 return redirect_see_other(&format!(
294 "/app/oauth/delegation?request_uri={}&delegated_did={}",
295 url_encode(&request_uri),
296 url_encode(&user.did)
297 ));
298 }
299 }
300 Ok(None) => {
301 tracing::info!(normalized = %normalized, "No user found for login_hint");
302 }
303 Err(e) => {
304 tracing::error!(error = %e, "Error looking up user for login_hint");
305 }
306 }
307 } else {
308 tracing::info!("No login_hint in request");
309 }
310
311 if request_data.parameters.prompt == Some(Prompt::Create) {
312 return redirect_see_other(&format!(
313 "/app/oauth/register?request_uri={}",
314 url_encode(&request_uri)
315 ));
316 }
317
318 if !force_new_account
319 && let Some(device_id) = extract_device_cookie(&headers)
320 && let Ok(accounts) = state
321 .oauth_repo
322 .get_device_accounts(&device_id.clone())
323 .await
324 && !accounts.is_empty()
325 {
326 return redirect_see_other(&format!(
327 "/app/oauth/accounts?request_uri={}",
328 url_encode(&request_uri)
329 ));
330 }
331 redirect_see_other(&format!(
332 "/app/oauth/login?request_uri={}",
333 url_encode(&request_uri)
334 ))
335}
336
337pub async fn authorize_get_json(
338 State(state): State<AppState>,
339 Query(query): Query<AuthorizeQuery>,
340) -> Result<Json<AuthorizeResponse>, OAuthError> {
341 let request_uri = query
342 .request_uri
343 .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?;
344 let request_id_json = RequestId::from(request_uri.clone());
345 let request_data = state
346 .oauth_repo
347 .get_authorization_request(&request_id_json)
348 .await
349 .map_err(tranquil_pds::oauth::db_err_to_oauth)?
350 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
351 if request_data.expires_at < Utc::now() {
352 let _ = state
353 .oauth_repo
354 .delete_authorization_request(&request_id_json)
355 .await;
356 return Err(OAuthError::InvalidRequest(
357 "request_uri has expired".to_string(),
358 ));
359 }
360 Ok(Json(AuthorizeResponse {
361 client_id: request_data.parameters.client_id.clone(),
362 client_name: None,
363 scope: request_data.parameters.scope.clone(),
364 redirect_uri: request_data.parameters.redirect_uri.clone(),
365 state: request_data.parameters.state.clone(),
366 login_hint: request_data.parameters.login_hint.clone(),
367 }))
368}
369
370#[derive(Debug, Serialize)]
371pub struct AccountInfo {
372 pub did: String,
373 pub handle: Handle,
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub email: Option<String>,
376}
377
378#[derive(Debug, Serialize)]
379pub struct AccountsResponse {
380 pub accounts: Vec<AccountInfo>,
381 pub request_uri: String,
382}
383
384fn mask_email(email: &str) -> String {
385 if let Some(at_pos) = email.find('@') {
386 let local = &email[..at_pos];
387 let domain = &email[at_pos..];
388 if local.len() <= 2 {
389 format!("{}***{}", local.chars().next().unwrap_or('*'), domain)
390 } else {
391 let first = local.chars().next().unwrap_or('*');
392 let last = local.chars().last().unwrap_or('*');
393 format!("{}***{}{}", first, last, domain)
394 }
395 } else {
396 "***".to_string()
397 }
398}
399
400pub async fn authorize_accounts(
401 State(state): State<AppState>,
402 headers: HeaderMap,
403 Query(query): Query<AuthorizeQuery>,
404) -> Response {
405 let request_uri = match query.request_uri {
406 Some(uri) => uri,
407 None => {
408 return (
409 StatusCode::BAD_REQUEST,
410 Json(serde_json::json!({
411 "error": "invalid_request",
412 "error_description": "Missing request_uri parameter"
413 })),
414 )
415 .into_response();
416 }
417 };
418 let device_id = match extract_device_cookie(&headers) {
419 Some(id) => id,
420 None => {
421 return Json(AccountsResponse {
422 accounts: vec![],
423 request_uri,
424 })
425 .into_response();
426 }
427 };
428 let accounts = match state.oauth_repo.get_device_accounts(&device_id).await {
429 Ok(accts) => accts,
430 Err(_) => {
431 return Json(AccountsResponse {
432 accounts: vec![],
433 request_uri,
434 })
435 .into_response();
436 }
437 };
438 let account_infos: Vec<AccountInfo> = accounts
439 .into_iter()
440 .map(|row| AccountInfo {
441 did: row.did.to_string(),
442 handle: row.handle,
443 email: row.email.map(|e| mask_email(&e)),
444 })
445 .collect();
446 Json(AccountsResponse {
447 accounts: account_infos,
448 request_uri,
449 })
450 .into_response()
451}
452
453pub async fn authorize_post(
454 State(state): State<AppState>,
455 _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>,
456 headers: HeaderMap,
457 Json(form): Json<AuthorizeSubmit>,
458) -> Response {
459 let json_response = wants_json(&headers);
460 let form_request_id = RequestId::from(form.request_uri.clone());
461 let request_data = match state
462 .oauth_repo
463 .get_authorization_request(&form_request_id)
464 .await
465 {
466 Ok(Some(data)) => data,
467 Ok(None) => {
468 if json_response {
469 return (
470 axum::http::StatusCode::BAD_REQUEST,
471 Json(serde_json::json!({
472 "error": "invalid_request",
473 "error_description": "Invalid or expired request_uri."
474 })),
475 )
476 .into_response();
477 }
478 return redirect_to_frontend_error(
479 "invalid_request",
480 "Invalid or expired request_uri. Please start a new authorization request.",
481 );
482 }
483 Err(e) => {
484 if json_response {
485 return (
486 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
487 Json(serde_json::json!({
488 "error": "server_error",
489 "error_description": format!("Database error: {:?}", e)
490 })),
491 )
492 .into_response();
493 }
494 return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e));
495 }
496 };
497 if request_data.expires_at < Utc::now() {
498 let _ = state
499 .oauth_repo
500 .delete_authorization_request(&form_request_id)
501 .await;
502 if json_response {
503 return (
504 axum::http::StatusCode::BAD_REQUEST,
505 Json(serde_json::json!({
506 "error": "invalid_request",
507 "error_description": "Authorization request has expired."
508 })),
509 )
510 .into_response();
511 }
512 return redirect_to_frontend_error(
513 "invalid_request",
514 "Authorization request has expired. Please start a new request.",
515 );
516 }
517 let show_login_error = |error_msg: &str, json: bool| -> Response {
518 if json {
519 return (
520 axum::http::StatusCode::FORBIDDEN,
521 Json(serde_json::json!({
522 "error": "access_denied",
523 "error_description": error_msg
524 })),
525 )
526 .into_response();
527 }
528 redirect_see_other(&format!(
529 "/app/oauth/login?request_uri={}&error={}",
530 url_encode(&form.request_uri),
531 url_encode(error_msg)
532 ))
533 };
534 let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
535 let normalized_username =
536 NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles);
537 tracing::debug!(
538 original_username = %form.username,
539 normalized_username = %normalized_username,
540 pds_hostname = %tranquil_config::get().server.hostname,
541 "Normalized username for lookup"
542 );
543 let user = match state
544 .user_repo
545 .get_login_info_by_handle_or_email(normalized_username.as_str())
546 .await
547 {
548 Ok(Some(u)) => u,
549 Ok(None) => {
550 let _ = bcrypt::verify(
551 &form.password,
552 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK",
553 );
554 return show_login_error("Invalid handle/email or password.", json_response);
555 }
556 Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
557 };
558 if user.deactivated_at.is_some() {
559 return show_login_error("This account has been deactivated.", json_response);
560 }
561 if user.takedown_ref.is_some() {
562 return show_login_error("This account has been taken down.", json_response);
563 }
564 if user.account_type.is_delegated() {
565 if state
566 .oauth_repo
567 .set_authorization_did(&form_request_id, &user.did, None)
568 .await
569 .is_err()
570 {
571 return show_login_error("An error occurred. Please try again.", json_response);
572 }
573 let redirect_url = format!(
574 "/app/oauth/delegation?request_uri={}&delegated_did={}",
575 url_encode(&form.request_uri),
576 url_encode(&user.did)
577 );
578 if json_response {
579 return (
580 StatusCode::OK,
581 Json(serde_json::json!({
582 "next": "delegation",
583 "delegated_did": user.did,
584 "redirect": redirect_url
585 })),
586 )
587 .into_response();
588 }
589 return redirect_see_other(&redirect_url);
590 }
591
592 if !user.password_required {
593 if state
594 .oauth_repo
595 .set_authorization_did(&form_request_id, &user.did, None)
596 .await
597 .is_err()
598 {
599 return show_login_error("An error occurred. Please try again.", json_response);
600 }
601 let redirect_url = format!(
602 "/app/oauth/passkey?request_uri={}",
603 url_encode(&form.request_uri)
604 );
605 if json_response {
606 return (
607 StatusCode::OK,
608 Json(serde_json::json!({
609 "next": "passkey",
610 "redirect": redirect_url
611 })),
612 )
613 .into_response();
614 }
615 return redirect_see_other(&redirect_url);
616 }
617
618 let password_valid = match &user.password_hash {
619 Some(hash) => match bcrypt::verify(&form.password, hash) {
620 Ok(valid) => valid,
621 Err(_) => {
622 return show_login_error("An error occurred. Please try again.", json_response);
623 }
624 },
625 None => false,
626 };
627 if !password_valid {
628 return show_login_error("Invalid handle/email or password.", json_response);
629 }
630 let is_verified = user.channel_verification.has_any_verified();
631 if !is_verified {
632 let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await;
633 let handle = resend_info
634 .as_ref()
635 .map(|r| r.handle.to_string())
636 .unwrap_or_else(|| form.username.clone());
637 let channel = resend_info
638 .map(|r| r.channel.as_str().to_owned())
639 .unwrap_or_else(|| user.preferred_comms_channel.as_str().to_owned());
640 if json_response {
641 return (
642 axum::http::StatusCode::FORBIDDEN,
643 Json(serde_json::json!({
644 "error": "account_not_verified",
645 "error_description": "Please verify your account before logging in.",
646 "did": user.did,
647 "handle": handle,
648 "channel": channel
649 })),
650 )
651 .into_response();
652 }
653 return redirect_see_other(&format!(
654 "/app/oauth/login?request_uri={}&error={}",
655 url_encode(&form.request_uri),
656 url_encode("account_not_verified")
657 ));
658 }
659 let has_totp = tranquil_api::server::has_totp_enabled(&state, &user.did).await;
660 if has_totp {
661 let device_cookie = extract_device_cookie(&headers);
662 let device_is_trusted = if let Some(ref dev_id) = device_cookie {
663 tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did)
664 .await
665 } else {
666 false
667 };
668
669 if device_is_trusted {
670 if let Some(ref dev_id) = device_cookie {
671 let _ =
672 tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id)
673 .await;
674 }
675 } else {
676 if state
677 .oauth_repo
678 .set_authorization_did(&form_request_id, &user.did, None)
679 .await
680 .is_err()
681 {
682 return show_login_error("An error occurred. Please try again.", json_response);
683 }
684 if json_response {
685 return Json(serde_json::json!({
686 "needs_totp": true
687 }))
688 .into_response();
689 }
690 return redirect_see_other(&format!(
691 "/app/oauth/totp?request_uri={}",
692 url_encode(&form.request_uri)
693 ));
694 }
695 }
696 if user.two_factor_enabled {
697 let _ = state
698 .oauth_repo
699 .delete_2fa_challenge_by_request_uri(&form_request_id)
700 .await;
701 match state
702 .oauth_repo
703 .create_2fa_challenge(&user.did, &form_request_id)
704 .await
705 {
706 Ok(challenge) => {
707 let hostname = &tranquil_config::get().server.hostname;
708 if let Err(e) = enqueue_2fa_code(
709 state.user_repo.as_ref(),
710 state.infra_repo.as_ref(),
711 user.id,
712 &challenge.code,
713 hostname,
714 )
715 .await
716 {
717 tracing::warn!(
718 did = %user.did,
719 error = %e,
720 "Failed to enqueue 2FA notification"
721 );
722 }
723 let channel_name = user.preferred_comms_channel.display_name();
724 if json_response {
725 return Json(serde_json::json!({
726 "needs_2fa": true,
727 "channel": channel_name
728 }))
729 .into_response();
730 }
731 return redirect_see_other(&format!(
732 "/app/oauth/2fa?request_uri={}&channel={}",
733 url_encode(&form.request_uri),
734 url_encode(channel_name)
735 ));
736 }
737 Err(_) => {
738 return show_login_error("An error occurred. Please try again.", json_response);
739 }
740 }
741 }
742 let mut device_id: Option<DeviceIdType> = extract_device_cookie(&headers);
743 let mut new_cookie: Option<String> = None;
744 if form.remember_device {
745 let final_device_id = if let Some(existing_id) = &device_id {
746 existing_id.clone()
747 } else {
748 let new_id = DeviceId::generate();
749 let new_device_id_typed = DeviceIdType::new(new_id.0.clone());
750 let device_data = DeviceData {
751 session_id: SessionId::generate(),
752 user_agent: extract_user_agent(&headers),
753 ip_address: extract_client_ip(&headers, None),
754 last_seen_at: Utc::now(),
755 };
756 if state
757 .oauth_repo
758 .create_device(&new_device_id_typed, &device_data)
759 .await
760 .is_ok()
761 {
762 new_cookie = Some(make_device_cookie(&new_device_id_typed));
763 device_id = Some(new_device_id_typed.clone());
764 }
765 new_device_id_typed
766 };
767 let _ = state
768 .oauth_repo
769 .upsert_account_device(&user.did, &final_device_id)
770 .await;
771 }
772 let set_auth_device_id = device_id.clone();
773 if state
774 .oauth_repo
775 .set_authorization_did(&form_request_id, &user.did, set_auth_device_id.as_ref())
776 .await
777 .is_err()
778 {
779 return show_login_error("An error occurred. Please try again.", json_response);
780 }
781 let requested_scope_str = request_data
782 .parameters
783 .scope
784 .as_deref()
785 .unwrap_or("atproto");
786 let requested_scopes: Vec<String> = requested_scope_str
787 .split_whitespace()
788 .map(|s| s.to_string())
789 .collect();
790 let client_id_typed = ClientId::from(request_data.parameters.client_id.clone());
791 let needs_consent = should_show_consent(
792 state.oauth_repo.as_ref(),
793 &user.did,
794 &client_id_typed,
795 &requested_scopes,
796 )
797 .await
798 .unwrap_or(true);
799 if needs_consent {
800 let consent_url = format!(
801 "/app/oauth/consent?request_uri={}",
802 url_encode(&form.request_uri)
803 );
804 if json_response {
805 if let Some(cookie) = new_cookie {
806 return (
807 StatusCode::OK,
808 [(SET_COOKIE, cookie)],
809 Json(serde_json::json!({"redirect_uri": consent_url})),
810 )
811 .into_response();
812 }
813 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
814 }
815 if let Some(cookie) = new_cookie {
816 return (
817 StatusCode::SEE_OTHER,
818 [(SET_COOKIE, cookie), (LOCATION, consent_url)],
819 )
820 .into_response();
821 }
822 return redirect_see_other(&consent_url);
823 }
824 let code = Code::generate();
825 let auth_post_device_id = device_id.clone();
826 let auth_post_code = AuthorizationCode::from(code.0.clone());
827 if state
828 .oauth_repo
829 .update_authorization_request(
830 &form_request_id,
831 &user.did,
832 auth_post_device_id.as_ref(),
833 &auth_post_code,
834 )
835 .await
836 .is_err()
837 {
838 return show_login_error("An error occurred. Please try again.", json_response);
839 }
840 if json_response {
841 let redirect_url = build_intermediate_redirect_url(
842 &request_data.parameters.redirect_uri,
843 &code.0,
844 request_data.parameters.state.as_deref(),
845 request_data.parameters.response_mode.map(|m| m.as_str()),
846 );
847 if let Some(cookie) = new_cookie {
848 (
849 StatusCode::OK,
850 [(SET_COOKIE, cookie)],
851 Json(serde_json::json!({"redirect_uri": redirect_url})),
852 )
853 .into_response()
854 } else {
855 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response()
856 }
857 } else {
858 let redirect_url = build_success_redirect(
859 &request_data.parameters.redirect_uri,
860 &code.0,
861 request_data.parameters.state.as_deref(),
862 request_data.parameters.response_mode.map(|m| m.as_str()),
863 );
864 if let Some(cookie) = new_cookie {
865 (
866 StatusCode::SEE_OTHER,
867 [(SET_COOKIE, cookie), (LOCATION, redirect_url)],
868 )
869 .into_response()
870 } else {
871 redirect_see_other(&redirect_url)
872 }
873 }
874}
875
876pub async fn authorize_select(
877 State(state): State<AppState>,
878 headers: HeaderMap,
879 Json(form): Json<AuthorizeSelectSubmit>,
880) -> Response {
881 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
882 (
883 status,
884 Json(serde_json::json!({
885 "error": error,
886 "error_description": description
887 })),
888 )
889 .into_response()
890 };
891 let select_request_id = RequestId::from(form.request_uri.clone());
892 let request_data = match state
893 .oauth_repo
894 .get_authorization_request(&select_request_id)
895 .await
896 {
897 Ok(Some(data)) => data,
898 Ok(None) => {
899 return json_error(
900 StatusCode::BAD_REQUEST,
901 "invalid_request",
902 "Invalid or expired request_uri. Please start a new authorization request.",
903 );
904 }
905 Err(_) => {
906 return json_error(
907 StatusCode::INTERNAL_SERVER_ERROR,
908 "server_error",
909 "An error occurred. Please try again.",
910 );
911 }
912 };
913 if request_data.expires_at < Utc::now() {
914 let _ = state
915 .oauth_repo
916 .delete_authorization_request(&select_request_id)
917 .await;
918 return json_error(
919 StatusCode::BAD_REQUEST,
920 "invalid_request",
921 "Authorization request has expired. Please start a new request.",
922 );
923 }
924 let device_id = match extract_device_cookie(&headers) {
925 Some(id) => id,
926 None => {
927 return json_error(
928 StatusCode::BAD_REQUEST,
929 "invalid_request",
930 "No device session found. Please sign in.",
931 );
932 }
933 };
934 let did: Did = match form.did.parse() {
935 Ok(d) => d,
936 Err(_) => {
937 return json_error(
938 StatusCode::BAD_REQUEST,
939 "invalid_request",
940 "Invalid DID format.",
941 );
942 }
943 };
944 let verify_device_id = device_id.clone();
945 let account_valid = match state
946 .oauth_repo
947 .verify_account_on_device(&verify_device_id, &did)
948 .await
949 {
950 Ok(valid) => valid,
951 Err(_) => {
952 return json_error(
953 StatusCode::INTERNAL_SERVER_ERROR,
954 "server_error",
955 "An error occurred. Please try again.",
956 );
957 }
958 };
959 if !account_valid {
960 return json_error(
961 StatusCode::FORBIDDEN,
962 "access_denied",
963 "This account is not available on this device. Please sign in.",
964 );
965 }
966 let user = match state.user_repo.get_2fa_status_by_did(&did).await {
967 Ok(Some(u)) => u,
968 Ok(None) => {
969 return json_error(
970 StatusCode::FORBIDDEN,
971 "access_denied",
972 "Account not found. Please sign in.",
973 );
974 }
975 Err(_) => {
976 return json_error(
977 StatusCode::INTERNAL_SERVER_ERROR,
978 "server_error",
979 "An error occurred. Please try again.",
980 );
981 }
982 };
983 let is_verified = user.channel_verification.has_any_verified();
984 if !is_verified {
985 let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await;
986 return (
987 StatusCode::FORBIDDEN,
988 Json(serde_json::json!({
989 "error": "account_not_verified",
990 "error_description": "Please verify your account before logging in.",
991 "did": did,
992 "handle": resend_info.as_ref().map(|r| r.handle.to_string()),
993 "channel": resend_info.as_ref().map(|r| r.channel.as_str())
994 })),
995 )
996 .into_response();
997 }
998 let has_totp = tranquil_api::server::has_totp_enabled(&state, &did).await;
999 let select_early_device_typed = device_id.clone();
1000 if has_totp {
1001 let device_is_trusted =
1002 tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), &device_id, &did)
1003 .await;
1004 if !device_is_trusted {
1005 if state
1006 .oauth_repo
1007 .set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed))
1008 .await
1009 .is_err()
1010 {
1011 return json_error(
1012 StatusCode::INTERNAL_SERVER_ERROR,
1013 "server_error",
1014 "An error occurred. Please try again.",
1015 );
1016 }
1017 return Json(serde_json::json!({
1018 "needs_totp": true
1019 }))
1020 .into_response();
1021 }
1022 let _ =
1023 tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), &device_id).await;
1024 }
1025 if user.two_factor_enabled {
1026 let _ = state
1027 .oauth_repo
1028 .delete_2fa_challenge_by_request_uri(&select_request_id)
1029 .await;
1030 match state
1031 .oauth_repo
1032 .create_2fa_challenge(&did, &select_request_id)
1033 .await
1034 {
1035 Ok(challenge) => {
1036 let hostname = &tranquil_config::get().server.hostname;
1037 if let Err(e) = enqueue_2fa_code(
1038 state.user_repo.as_ref(),
1039 state.infra_repo.as_ref(),
1040 user.id,
1041 &challenge.code,
1042 hostname,
1043 )
1044 .await
1045 {
1046 tracing::warn!(
1047 did = %form.did,
1048 error = %e,
1049 "Failed to enqueue 2FA notification"
1050 );
1051 }
1052 let channel_name = user.preferred_comms_channel.display_name();
1053 return Json(serde_json::json!({
1054 "needs_2fa": true,
1055 "channel": channel_name
1056 }))
1057 .into_response();
1058 }
1059 Err(_) => {
1060 return json_error(
1061 StatusCode::INTERNAL_SERVER_ERROR,
1062 "server_error",
1063 "An error occurred. Please try again.",
1064 );
1065 }
1066 }
1067 }
1068 let select_device_typed = device_id.clone();
1069 let _ = state
1070 .oauth_repo
1071 .upsert_account_device(&did, &select_device_typed)
1072 .await;
1073
1074 if state
1075 .oauth_repo
1076 .set_authorization_did(&select_request_id, &did, Some(&select_device_typed))
1077 .await
1078 .is_err()
1079 {
1080 return json_error(
1081 StatusCode::INTERNAL_SERVER_ERROR,
1082 "server_error",
1083 "An error occurred. Please try again.",
1084 );
1085 }
1086 let consent_url = format!(
1087 "/app/oauth/consent?request_uri={}",
1088 url_encode(&form.request_uri)
1089 );
1090 Json(serde_json::json!({"redirect_uri": consent_url})).into_response()
1091}
1092
1093fn build_success_redirect(
1094 redirect_uri: &str,
1095 code: &str,
1096 state: Option<&str>,
1097 response_mode: Option<&str>,
1098) -> String {
1099 let mut redirect_url = redirect_uri.to_string();
1100 let use_fragment = response_mode == Some("fragment");
1101 let separator = if use_fragment {
1102 '#'
1103 } else if redirect_url.contains('?') {
1104 '&'
1105 } else {
1106 '?'
1107 };
1108 redirect_url.push(separator);
1109 let pds_host = &tranquil_config::get().server.hostname;
1110 redirect_url.push_str(&format!(
1111 "iss={}",
1112 url_encode(&format!("https://{}", pds_host))
1113 ));
1114 if let Some(req_state) = state {
1115 redirect_url.push_str(&format!("&state={}", url_encode(req_state)));
1116 }
1117 redirect_url.push_str(&format!("&code={}", url_encode(code)));
1118 redirect_url
1119}
1120
1121fn build_intermediate_redirect_url(
1122 redirect_uri: &str,
1123 code: &str,
1124 state: Option<&str>,
1125 response_mode: Option<&str>,
1126) -> String {
1127 let pds_host = &tranquil_config::get().server.hostname;
1128 let mut url = format!(
1129 "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}",
1130 pds_host,
1131 url_encode(redirect_uri),
1132 url_encode(code)
1133 );
1134 if let Some(s) = state {
1135 url.push_str(&format!("&state={}", url_encode(s)));
1136 }
1137 if let Some(rm) = response_mode {
1138 url.push_str(&format!("&response_mode={}", url_encode(rm)));
1139 }
1140 url
1141}
1142
1143#[derive(Debug, Deserialize)]
1144pub struct AuthorizeRedirectParams {
1145 redirect_uri: String,
1146 code: String,
1147 state: Option<String>,
1148 response_mode: Option<String>,
1149}
1150
1151pub async fn authorize_redirect(Query(params): Query<AuthorizeRedirectParams>) -> Response {
1152 let final_url = build_success_redirect(
1153 ¶ms.redirect_uri,
1154 ¶ms.code,
1155 params.state.as_deref(),
1156 params.response_mode.as_deref(),
1157 );
1158 tracing::info!(
1159 final_url = %final_url,
1160 client_redirect = %params.redirect_uri,
1161 "authorize_redirect performing 303 redirect"
1162 );
1163 (
1164 StatusCode::SEE_OTHER,
1165 [
1166 (axum::http::header::LOCATION, final_url),
1167 (axum::http::header::CACHE_CONTROL, "no-store".to_string()),
1168 ],
1169 )
1170 .into_response()
1171}
1172
1173#[derive(Debug, Serialize)]
1174pub struct AuthorizeDenyResponse {
1175 pub error: String,
1176 pub error_description: String,
1177}
1178
1179pub async fn authorize_deny(
1180 State(state): State<AppState>,
1181 Json(form): Json<AuthorizeDenyForm>,
1182) -> Response {
1183 let deny_request_id = RequestId::from(form.request_uri.clone());
1184 let request_data = match state
1185 .oauth_repo
1186 .get_authorization_request(&deny_request_id)
1187 .await
1188 {
1189 Ok(Some(data)) => data,
1190 Ok(None) => {
1191 return (
1192 StatusCode::BAD_REQUEST,
1193 Json(serde_json::json!({
1194 "error": "invalid_request",
1195 "error_description": "Invalid request_uri"
1196 })),
1197 )
1198 .into_response();
1199 }
1200 Err(_) => {
1201 return (
1202 StatusCode::INTERNAL_SERVER_ERROR,
1203 Json(serde_json::json!({
1204 "error": "server_error",
1205 "error_description": "An error occurred"
1206 })),
1207 )
1208 .into_response();
1209 }
1210 };
1211 let _ = state
1212 .oauth_repo
1213 .delete_authorization_request(&deny_request_id)
1214 .await;
1215 let redirect_uri = &request_data.parameters.redirect_uri;
1216 let mut redirect_url = redirect_uri.to_string();
1217 let separator = if redirect_url.contains('?') { '&' } else { '?' };
1218 redirect_url.push(separator);
1219 redirect_url.push_str("error=access_denied");
1220 redirect_url.push_str("&error_description=User%20denied%20the%20request");
1221 if let Some(state) = &request_data.parameters.state {
1222 redirect_url.push_str(&format!("&state={}", url_encode(state)));
1223 }
1224 Json(serde_json::json!({
1225 "redirect_uri": redirect_url
1226 }))
1227 .into_response()
1228}
1229
1230#[derive(Debug, Deserialize)]
1231pub struct AuthorizeDenyForm {
1232 pub request_uri: String,
1233}
1234
1235#[derive(Debug, Deserialize)]
1236pub struct Authorize2faQuery {
1237 pub request_uri: String,
1238 pub channel: Option<String>,
1239}
1240
1241#[derive(Debug, Deserialize)]
1242pub struct Authorize2faSubmit {
1243 pub request_uri: String,
1244 pub code: String,
1245 #[serde(default)]
1246 pub trust_device: bool,
1247}
1248
1249const MAX_2FA_ATTEMPTS: i32 = 5;
1250
1251pub async fn authorize_2fa_get(
1252 State(state): State<AppState>,
1253 Query(query): Query<Authorize2faQuery>,
1254) -> Response {
1255 let twofa_request_id = RequestId::from(query.request_uri.clone());
1256 let challenge = match state.oauth_repo.get_2fa_challenge(&twofa_request_id).await {
1257 Ok(Some(c)) => c,
1258 Ok(None) => {
1259 return redirect_to_frontend_error(
1260 "invalid_request",
1261 "No 2FA challenge found. Please start over.",
1262 );
1263 }
1264 Err(_) => {
1265 return redirect_to_frontend_error(
1266 "server_error",
1267 "An error occurred. Please try again.",
1268 );
1269 }
1270 };
1271 if challenge.expires_at < Utc::now() {
1272 let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await;
1273 return redirect_to_frontend_error(
1274 "invalid_request",
1275 "2FA code has expired. Please start over.",
1276 );
1277 }
1278 let _request_data = match state
1279 .oauth_repo
1280 .get_authorization_request(&twofa_request_id)
1281 .await
1282 {
1283 Ok(Some(d)) => d,
1284 Ok(None) => {
1285 return redirect_to_frontend_error(
1286 "invalid_request",
1287 "Authorization request not found. Please start over.",
1288 );
1289 }
1290 Err(_) => {
1291 return redirect_to_frontend_error(
1292 "server_error",
1293 "An error occurred. Please try again.",
1294 );
1295 }
1296 };
1297 let channel = query.channel.as_deref().unwrap_or("email");
1298 redirect_see_other(&format!(
1299 "/app/oauth/2fa?request_uri={}&channel={}",
1300 url_encode(&query.request_uri),
1301 url_encode(channel)
1302 ))
1303}
1304
1305#[derive(Debug, Serialize)]
1306pub struct ScopeInfo {
1307 pub scope: String,
1308 pub category: String,
1309 pub required: bool,
1310 pub description: String,
1311 pub display_name: String,
1312 pub granted: Option<bool>,
1313}
1314
1315#[derive(Debug, Serialize)]
1316pub struct ConsentResponse {
1317 pub request_uri: String,
1318 pub client_id: String,
1319 pub client_name: Option<String>,
1320 pub client_uri: Option<String>,
1321 pub logo_uri: Option<String>,
1322 pub scopes: Vec<ScopeInfo>,
1323 pub show_consent: bool,
1324 pub did: String,
1325 #[serde(skip_serializing_if = "Option::is_none")]
1326 pub handle: Option<String>,
1327 #[serde(skip_serializing_if = "Option::is_none")]
1328 pub is_delegation: Option<bool>,
1329 #[serde(skip_serializing_if = "Option::is_none")]
1330 pub controller_did: Option<String>,
1331 #[serde(skip_serializing_if = "Option::is_none")]
1332 pub controller_handle: Option<String>,
1333 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub delegation_level: Option<String>,
1335}
1336
1337#[derive(Debug, Deserialize)]
1338pub struct ConsentQuery {
1339 pub request_uri: String,
1340}
1341
1342#[derive(Debug, Deserialize)]
1343pub struct ConsentSubmit {
1344 pub request_uri: String,
1345 pub approved_scopes: Vec<String>,
1346 pub remember: bool,
1347}
1348
1349pub async fn consent_get(
1350 State(state): State<AppState>,
1351 Query(query): Query<ConsentQuery>,
1352) -> Response {
1353 let consent_request_id = RequestId::from(query.request_uri.clone());
1354 let request_data = match state
1355 .oauth_repo
1356 .get_authorization_request(&consent_request_id)
1357 .await
1358 {
1359 Ok(Some(data)) => data,
1360 Ok(None) => {
1361 return json_error(
1362 StatusCode::BAD_REQUEST,
1363 "invalid_request",
1364 "Invalid or expired request_uri",
1365 );
1366 }
1367 Err(e) => {
1368 return json_error(
1369 StatusCode::INTERNAL_SERVER_ERROR,
1370 "server_error",
1371 &format!("Database error: {:?}", e),
1372 );
1373 }
1374 };
1375 let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) {
1376 Ok(flow) => match flow.require_user() {
1377 Ok(u) => u,
1378 Err(_) => {
1379 return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated");
1380 }
1381 },
1382 Err(_) => {
1383 return json_error(
1384 StatusCode::BAD_REQUEST,
1385 "expired_request",
1386 "Authorization request has expired",
1387 );
1388 }
1389 };
1390
1391 let did = flow_with_user.did().clone();
1392 let client_cache = ClientMetadataCache::new(3600);
1393 let client_metadata = client_cache
1394 .get(&request_data.parameters.client_id)
1395 .await
1396 .ok();
1397 let requested_scope_str = request_data
1398 .parameters
1399 .scope
1400 .as_deref()
1401 .filter(|s| !s.trim().is_empty())
1402 .unwrap_or("atproto");
1403
1404 let controller_did_parsed: Option<Did> = request_data
1405 .controller_did
1406 .as_ref()
1407 .and_then(|s| s.parse().ok());
1408 let delegation_grant = if let Some(ref ctrl_did) = controller_did_parsed {
1409 state
1410 .delegation_repo
1411 .get_delegation(&did, ctrl_did)
1412 .await
1413 .ok()
1414 .flatten()
1415 } else {
1416 None
1417 };
1418
1419 let effective_scope_str = if let Some(ref grant) = delegation_grant {
1420 tranquil_pds::delegation::intersect_scopes(
1421 requested_scope_str,
1422 grant.granted_scopes.as_str(),
1423 )
1424 } else {
1425 requested_scope_str.to_string()
1426 };
1427
1428 let expanded_scope_str = expand_include_scopes(&effective_scope_str).await;
1429 let requested_scopes: Vec<&str> = expanded_scope_str.split_whitespace().collect();
1430 let consent_client_id = ClientId::from(request_data.parameters.client_id.clone());
1431 let preferences = state
1432 .oauth_repo
1433 .get_scope_preferences(&did, &consent_client_id)
1434 .await
1435 .unwrap_or_default();
1436 let pref_map: std::collections::HashMap<_, _> = preferences
1437 .iter()
1438 .map(|p| (p.scope.as_str(), p.granted))
1439 .collect();
1440 let requested_scope_strings: Vec<String> =
1441 requested_scopes.iter().map(|s| s.to_string()).collect();
1442 let show_consent = should_show_consent(
1443 state.oauth_repo.as_ref(),
1444 &did,
1445 &consent_client_id,
1446 &requested_scope_strings,
1447 )
1448 .await
1449 .unwrap_or(true);
1450 let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s));
1451 let scopes: Vec<ScopeInfo> = requested_scopes
1452 .iter()
1453 .map(|scope| {
1454 let (category, required, description, display_name) = if let Some(def) =
1455 tranquil_pds::oauth::scopes::SCOPE_DEFINITIONS.get(*scope)
1456 {
1457 let desc = if *scope == "atproto" && has_granular_scopes {
1458 "AT Protocol baseline scope (permissions determined by selected options below)"
1459 .to_string()
1460 } else {
1461 def.description.to_string()
1462 };
1463 let name = if *scope == "atproto" && has_granular_scopes {
1464 "AT Protocol Access".to_string()
1465 } else {
1466 def.display_name.to_string()
1467 };
1468 (
1469 def.category.display_name().to_string(),
1470 def.required,
1471 desc,
1472 name,
1473 )
1474 } else if scope.starts_with("ref:") {
1475 (
1476 "Reference".to_string(),
1477 false,
1478 "Referenced scope".to_string(),
1479 scope.to_string(),
1480 )
1481 } else {
1482 (
1483 "Other".to_string(),
1484 false,
1485 format!("Access to {}", scope),
1486 scope.to_string(),
1487 )
1488 };
1489 let granted = pref_map.get(*scope).copied();
1490 ScopeInfo {
1491 scope: scope.to_string(),
1492 category,
1493 required,
1494 description,
1495 display_name,
1496 granted,
1497 }
1498 })
1499 .collect();
1500
1501 let account_handle = state
1502 .user_repo
1503 .get_handle_by_did(&did)
1504 .await
1505 .ok()
1506 .flatten()
1507 .map(|h| h.to_string());
1508
1509 let (is_delegation, controller_did_resp, controller_handle, delegation_level) =
1510 if let Some(ref ctrl_did) = controller_did_parsed {
1511 let ctrl_handle = state
1512 .user_repo
1513 .get_handle_by_did(ctrl_did)
1514 .await
1515 .ok()
1516 .flatten()
1517 .map(|h| h.to_string());
1518
1519 let level = if let Some(ref grant) = delegation_grant {
1520 let preset = tranquil_pds::delegation::SCOPE_PRESETS
1521 .iter()
1522 .find(|p| p.scopes == grant.granted_scopes.as_str());
1523 preset
1524 .map(|p| p.label.to_string())
1525 .unwrap_or_else(|| "Custom".to_string())
1526 } else {
1527 "Unknown".to_string()
1528 };
1529
1530 (
1531 Some(true),
1532 Some(ctrl_did.to_string()),
1533 ctrl_handle,
1534 Some(level),
1535 )
1536 } else {
1537 (None, None, None, None)
1538 };
1539
1540 Json(ConsentResponse {
1541 request_uri: query.request_uri.clone(),
1542 client_id: request_data.parameters.client_id.clone(),
1543 client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()),
1544 client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()),
1545 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()),
1546 scopes,
1547 show_consent,
1548 did: did.to_string(),
1549 handle: account_handle,
1550 is_delegation,
1551 controller_did: controller_did_resp,
1552 controller_handle,
1553 delegation_level,
1554 })
1555 .into_response()
1556}
1557
1558pub async fn consent_post(
1559 State(state): State<AppState>,
1560 Json(form): Json<ConsentSubmit>,
1561) -> Response {
1562 tracing::info!(
1563 "consent_post: approved_scopes={:?}, remember={}",
1564 form.approved_scopes,
1565 form.remember
1566 );
1567 let consent_post_request_id = RequestId::from(form.request_uri.clone());
1568 let request_data = match state
1569 .oauth_repo
1570 .get_authorization_request(&consent_post_request_id)
1571 .await
1572 {
1573 Ok(Some(data)) => data,
1574 Ok(None) => {
1575 return json_error(
1576 StatusCode::BAD_REQUEST,
1577 "invalid_request",
1578 "Invalid or expired request_uri",
1579 );
1580 }
1581 Err(e) => {
1582 return json_error(
1583 StatusCode::INTERNAL_SERVER_ERROR,
1584 "server_error",
1585 &format!("Database error: {:?}", e),
1586 );
1587 }
1588 };
1589 let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) {
1590 Ok(flow) => match flow.require_user() {
1591 Ok(u) => u,
1592 Err(_) => {
1593 return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated");
1594 }
1595 },
1596 Err(_) => {
1597 let _ = state
1598 .oauth_repo
1599 .delete_authorization_request(&consent_post_request_id)
1600 .await;
1601 return json_error(
1602 StatusCode::BAD_REQUEST,
1603 "invalid_request",
1604 "Authorization request has expired",
1605 );
1606 }
1607 };
1608
1609 let did = flow_with_user.did().clone();
1610 let original_scope_str = request_data
1611 .parameters
1612 .scope
1613 .as_deref()
1614 .unwrap_or("atproto");
1615
1616 let controller_did_parsed: Option<Did> = request_data
1617 .controller_did
1618 .as_ref()
1619 .and_then(|s| s.parse().ok());
1620
1621 let delegation_grant = match controller_did_parsed.as_ref() {
1622 Some(ctrl_did) => state
1623 .delegation_repo
1624 .get_delegation(&did, ctrl_did)
1625 .await
1626 .ok()
1627 .flatten(),
1628 None => None,
1629 };
1630
1631 let effective_scope_str = if let Some(ref grant) = delegation_grant {
1632 tranquil_pds::delegation::intersect_scopes(
1633 original_scope_str,
1634 grant.granted_scopes.as_str(),
1635 )
1636 } else {
1637 original_scope_str.to_string()
1638 };
1639
1640 let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect();
1641 let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s));
1642 let user_denied_some_granular = has_granular_scopes
1643 && requested_scopes
1644 .iter()
1645 .filter(|s| is_granular_scope(s))
1646 .any(|s| !form.approved_scopes.contains(&s.to_string()));
1647 let atproto_was_requested = requested_scopes.contains(&"atproto");
1648 if atproto_was_requested
1649 && !has_granular_scopes
1650 && !form.approved_scopes.contains(&"atproto".to_string())
1651 {
1652 return json_error(
1653 StatusCode::BAD_REQUEST,
1654 "invalid_request",
1655 "The atproto scope was requested and must be approved",
1656 );
1657 }
1658 let final_approved: Vec<String> = if user_denied_some_granular {
1659 form.approved_scopes
1660 .iter()
1661 .filter(|s| *s != "atproto")
1662 .cloned()
1663 .collect()
1664 } else {
1665 form.approved_scopes.clone()
1666 };
1667 if final_approved.is_empty() {
1668 return json_error(
1669 StatusCode::BAD_REQUEST,
1670 "invalid_request",
1671 "At least one scope must be approved",
1672 );
1673 }
1674 let approved_scope_str = final_approved.join(" ");
1675 let has_valid_scope = final_approved.iter().all(|s| is_valid_scope(s));
1676 if !has_valid_scope {
1677 return json_error(
1678 StatusCode::BAD_REQUEST,
1679 "invalid_request",
1680 "Invalid scope format",
1681 );
1682 }
1683 if form.remember {
1684 let preferences: Vec<ScopePreference> = requested_scopes
1685 .iter()
1686 .map(|s| ScopePreference {
1687 scope: s.to_string(),
1688 granted: form.approved_scopes.contains(&s.to_string()),
1689 })
1690 .collect();
1691 let consent_post_client_id = ClientId::from(request_data.parameters.client_id.clone());
1692 let _ = state
1693 .oauth_repo
1694 .upsert_scope_preferences(&did, &consent_post_client_id, &preferences)
1695 .await;
1696 }
1697 if let Err(e) = state
1698 .oauth_repo
1699 .update_request_scope(&consent_post_request_id, &approved_scope_str)
1700 .await
1701 {
1702 tracing::warn!("Failed to update request scope: {:?}", e);
1703 }
1704 let code = Code::generate();
1705 let consent_post_device_id = request_data
1706 .device_id
1707 .as_ref()
1708 .map(|d| DeviceIdType::new(d.0.clone()));
1709 let consent_post_code = AuthorizationCode::from(code.0.clone());
1710 if state
1711 .oauth_repo
1712 .update_authorization_request(
1713 &consent_post_request_id,
1714 &did,
1715 consent_post_device_id.as_ref(),
1716 &consent_post_code,
1717 )
1718 .await
1719 .is_err()
1720 {
1721 return json_error(
1722 StatusCode::INTERNAL_SERVER_ERROR,
1723 "server_error",
1724 "Failed to complete authorization",
1725 );
1726 }
1727 let redirect_uri = &request_data.parameters.redirect_uri;
1728 let intermediate_url = build_intermediate_redirect_url(
1729 redirect_uri,
1730 &code.0,
1731 request_data.parameters.state.as_deref(),
1732 request_data.parameters.response_mode.map(|m| m.as_str()),
1733 );
1734 tracing::info!(
1735 intermediate_url = %intermediate_url,
1736 client_redirect = %redirect_uri,
1737 "consent_post returning JSON with intermediate URL (for 303 redirect)"
1738 );
1739 Json(serde_json::json!({ "redirect_uri": intermediate_url })).into_response()
1740}
1741
1742#[derive(Debug, Deserialize)]
1743pub struct RenewRequest {
1744 pub request_uri: String,
1745}
1746
1747pub async fn authorize_renew(
1748 State(state): State<AppState>,
1749 _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>,
1750 Json(form): Json<RenewRequest>,
1751) -> Response {
1752 let request_id = RequestId::from(form.request_uri.clone());
1753 let request_data = match state
1754 .oauth_repo
1755 .get_authorization_request(&request_id)
1756 .await
1757 {
1758 Ok(Some(data)) => data,
1759 Ok(None) => {
1760 return json_error(
1761 StatusCode::BAD_REQUEST,
1762 "invalid_request",
1763 "Unknown authorization request",
1764 );
1765 }
1766 Err(_) => {
1767 return json_error(
1768 StatusCode::INTERNAL_SERVER_ERROR,
1769 "server_error",
1770 "Database error",
1771 );
1772 }
1773 };
1774
1775 if request_data.did.is_none() {
1776 return json_error(
1777 StatusCode::BAD_REQUEST,
1778 "invalid_request",
1779 "Authorization request not yet authenticated",
1780 );
1781 }
1782
1783 let now = Utc::now();
1784 if request_data.expires_at >= now {
1785 return Json(serde_json::json!({
1786 "request_uri": form.request_uri,
1787 "renewed": false
1788 }))
1789 .into_response();
1790 }
1791
1792 let staleness = now - request_data.expires_at;
1793 if staleness.num_seconds() > MAX_RENEWAL_STALENESS_SECONDS {
1794 let _ = state
1795 .oauth_repo
1796 .delete_authorization_request(&request_id)
1797 .await;
1798 return json_error(
1799 StatusCode::BAD_REQUEST,
1800 "invalid_request",
1801 "Authorization request expired too long ago to renew",
1802 );
1803 }
1804
1805 let new_expires_at = now + chrono::Duration::seconds(RENEW_EXPIRY_SECONDS);
1806 match state
1807 .oauth_repo
1808 .extend_authorization_request_expiry(&request_id, new_expires_at)
1809 .await
1810 {
1811 Ok(true) => Json(serde_json::json!({
1812 "request_uri": form.request_uri,
1813 "renewed": true
1814 }))
1815 .into_response(),
1816 Ok(false) => json_error(
1817 StatusCode::BAD_REQUEST,
1818 "invalid_request",
1819 "Authorization request could not be renewed",
1820 ),
1821 Err(_) => json_error(
1822 StatusCode::INTERNAL_SERVER_ERROR,
1823 "server_error",
1824 "Database error",
1825 ),
1826 }
1827}
1828
1829pub async fn authorize_2fa_post(
1830 State(state): State<AppState>,
1831 _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>,
1832 headers: HeaderMap,
1833 Json(form): Json<Authorize2faSubmit>,
1834) -> Response {
1835 let json_error = |status: StatusCode, error: &str, description: &str| -> Response {
1836 (
1837 status,
1838 Json(serde_json::json!({
1839 "error": error,
1840 "error_description": description
1841 })),
1842 )
1843 .into_response()
1844 };
1845 let twofa_post_request_id = RequestId::from(form.request_uri.clone());
1846 let request_data = match state
1847 .oauth_repo
1848 .get_authorization_request(&twofa_post_request_id)
1849 .await
1850 {
1851 Ok(Some(d)) => d,
1852 Ok(None) => {
1853 return json_error(
1854 StatusCode::BAD_REQUEST,
1855 "invalid_request",
1856 "Authorization request not found.",
1857 );
1858 }
1859 Err(_) => {
1860 return json_error(
1861 StatusCode::INTERNAL_SERVER_ERROR,
1862 "server_error",
1863 "An error occurred.",
1864 );
1865 }
1866 };
1867 if request_data.expires_at < Utc::now() {
1868 let _ = state
1869 .oauth_repo
1870 .delete_authorization_request(&twofa_post_request_id)
1871 .await;
1872 return json_error(
1873 StatusCode::BAD_REQUEST,
1874 "invalid_request",
1875 "Authorization request has expired.",
1876 );
1877 }
1878 let challenge = state
1879 .oauth_repo
1880 .get_2fa_challenge(&twofa_post_request_id)
1881 .await
1882 .ok()
1883 .flatten();
1884 if let Some(challenge) = challenge {
1885 if challenge.expires_at < Utc::now() {
1886 let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await;
1887 return json_error(
1888 StatusCode::BAD_REQUEST,
1889 "invalid_request",
1890 "2FA code has expired. Please start over.",
1891 );
1892 }
1893 if challenge.attempts >= MAX_2FA_ATTEMPTS {
1894 let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await;
1895 return json_error(
1896 StatusCode::FORBIDDEN,
1897 "access_denied",
1898 "Too many failed attempts. Please start over.",
1899 );
1900 }
1901 let code_valid: bool = form
1902 .code
1903 .trim()
1904 .as_bytes()
1905 .ct_eq(challenge.code.as_bytes())
1906 .into();
1907 if !code_valid {
1908 let _ = state.oauth_repo.increment_2fa_attempts(challenge.id).await;
1909 return json_error(
1910 StatusCode::FORBIDDEN,
1911 "invalid_code",
1912 "Invalid verification code. Please try again.",
1913 );
1914 }
1915 let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await;
1916 let code = Code::generate();
1917 let device_id = extract_device_cookie(&headers);
1918 let twofa_totp_device_id = device_id.clone();
1919 let twofa_totp_code = AuthorizationCode::from(code.0.clone());
1920 if state
1921 .oauth_repo
1922 .update_authorization_request(
1923 &twofa_post_request_id,
1924 &challenge.did,
1925 twofa_totp_device_id.as_ref(),
1926 &twofa_totp_code,
1927 )
1928 .await
1929 .is_err()
1930 {
1931 return json_error(
1932 StatusCode::INTERNAL_SERVER_ERROR,
1933 "server_error",
1934 "An error occurred. Please try again.",
1935 );
1936 }
1937 let redirect_url = build_intermediate_redirect_url(
1938 &request_data.parameters.redirect_uri,
1939 &code.0,
1940 request_data.parameters.state.as_deref(),
1941 request_data.parameters.response_mode.map(|m| m.as_str()),
1942 );
1943 return Json(serde_json::json!({
1944 "redirect_uri": redirect_url
1945 }))
1946 .into_response();
1947 }
1948 let did_str = match &request_data.did {
1949 Some(d) => d.clone(),
1950 None => {
1951 return json_error(
1952 StatusCode::BAD_REQUEST,
1953 "invalid_request",
1954 "No 2FA challenge found. Please start over.",
1955 );
1956 }
1957 };
1958 let did: tranquil_types::Did = match did_str.parse() {
1959 Ok(d) => d,
1960 Err(_) => {
1961 return json_error(
1962 StatusCode::BAD_REQUEST,
1963 "invalid_request",
1964 "Invalid DID format.",
1965 );
1966 }
1967 };
1968 if !tranquil_api::server::has_totp_enabled(&state, &did).await {
1969 return json_error(
1970 StatusCode::BAD_REQUEST,
1971 "invalid_request",
1972 "No 2FA challenge found. Please start over.",
1973 );
1974 }
1975 let _rate_proof = match check_user_rate_limit::<TotpVerifyLimit>(&state, &did).await {
1976 Ok(proof) => proof,
1977 Err(_) => {
1978 return json_error(
1979 StatusCode::TOO_MANY_REQUESTS,
1980 "RateLimitExceeded",
1981 "Too many verification attempts. Please try again in a few minutes.",
1982 );
1983 }
1984 };
1985 let totp_valid =
1986 tranquil_api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
1987 if !totp_valid {
1988 return json_error(
1989 StatusCode::FORBIDDEN,
1990 "invalid_code",
1991 "Invalid verification code. Please try again.",
1992 );
1993 }
1994 let mut device_id = extract_device_cookie(&headers);
1995 let mut new_cookie: Option<String> = None;
1996 if form.trust_device {
1997 let trust_device_id = match &device_id {
1998 Some(existing_id) => existing_id.clone(),
1999 None => {
2000 let new_id = DeviceId::generate();
2001 let new_device_id_typed = DeviceIdType::new(new_id.0.clone());
2002 let device_data = DeviceData {
2003 session_id: SessionId::generate(),
2004 user_agent: extract_user_agent(&headers),
2005 ip_address: extract_client_ip(&headers, None),
2006 last_seen_at: Utc::now(),
2007 };
2008 if state
2009 .oauth_repo
2010 .create_device(&new_device_id_typed, &device_data)
2011 .await
2012 .is_ok()
2013 {
2014 new_cookie = Some(make_device_cookie(&new_device_id_typed));
2015 device_id = Some(new_device_id_typed.clone());
2016 }
2017 new_device_id_typed
2018 }
2019 };
2020 let _ = state
2021 .oauth_repo
2022 .upsert_account_device(&did, &trust_device_id)
2023 .await;
2024 let _ =
2025 tranquil_api::server::trust_device(state.oauth_repo.as_ref(), &trust_device_id).await;
2026 }
2027 let requested_scope_str = request_data
2028 .parameters
2029 .scope
2030 .as_deref()
2031 .unwrap_or("atproto");
2032 let requested_scopes: Vec<String> = requested_scope_str
2033 .split_whitespace()
2034 .map(|s| s.to_string())
2035 .collect();
2036 let twofa_post_client_id = ClientId::from(request_data.parameters.client_id.clone());
2037 let needs_consent = should_show_consent(
2038 state.oauth_repo.as_ref(),
2039 &did,
2040 &twofa_post_client_id,
2041 &requested_scopes,
2042 )
2043 .await
2044 .unwrap_or(true);
2045 if needs_consent {
2046 let consent_url = format!(
2047 "/app/oauth/consent?request_uri={}",
2048 url_encode(&form.request_uri)
2049 );
2050 if let Some(cookie) = new_cookie {
2051 return (
2052 StatusCode::OK,
2053 [(SET_COOKIE, cookie)],
2054 Json(serde_json::json!({"redirect_uri": consent_url})),
2055 )
2056 .into_response();
2057 }
2058 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
2059 }
2060 let code = Code::generate();
2061 let twofa_final_device_id = device_id.clone();
2062 let twofa_final_code = AuthorizationCode::from(code.0.clone());
2063 if state
2064 .oauth_repo
2065 .update_authorization_request(
2066 &twofa_post_request_id,
2067 &did,
2068 twofa_final_device_id.as_ref(),
2069 &twofa_final_code,
2070 )
2071 .await
2072 .is_err()
2073 {
2074 return json_error(
2075 StatusCode::INTERNAL_SERVER_ERROR,
2076 "server_error",
2077 "An error occurred. Please try again.",
2078 );
2079 }
2080 let redirect_url = build_intermediate_redirect_url(
2081 &request_data.parameters.redirect_uri,
2082 &code.0,
2083 request_data.parameters.state.as_deref(),
2084 request_data.parameters.response_mode.map(|m| m.as_str()),
2085 );
2086 if let Some(cookie) = new_cookie {
2087 (
2088 StatusCode::OK,
2089 [(SET_COOKIE, cookie)],
2090 Json(serde_json::json!({"redirect_uri": redirect_url})),
2091 )
2092 .into_response()
2093 } else {
2094 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response()
2095 }
2096}
2097
2098#[derive(Debug, Deserialize)]
2099#[serde(rename_all = "camelCase")]
2100pub struct CheckPasskeysQuery {
2101 pub identifier: String,
2102}
2103
2104#[derive(Debug, Serialize)]
2105#[serde(rename_all = "camelCase")]
2106pub struct CheckPasskeysResponse {
2107 pub has_passkeys: bool,
2108}
2109
2110pub async fn check_user_has_passkeys(
2111 State(state): State<AppState>,
2112 Query(query): Query<CheckPasskeysQuery>,
2113) -> Response {
2114 let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
2115 let bare_identifier =
2116 BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles);
2117
2118 let user = state
2119 .user_repo
2120 .get_login_check_by_handle_or_email(bare_identifier.as_str())
2121 .await;
2122
2123 let has_passkeys = match user {
2124 Ok(Some(u)) => tranquil_api::server::has_passkeys_for_user(&state, &u.did).await,
2125 _ => false,
2126 };
2127
2128 Json(CheckPasskeysResponse { has_passkeys }).into_response()
2129}
2130
2131#[derive(Debug, Serialize)]
2132#[serde(rename_all = "camelCase")]
2133pub struct SecurityStatusResponse {
2134 pub has_passkeys: bool,
2135 pub has_totp: bool,
2136 pub has_password: bool,
2137 pub is_delegated: bool,
2138 #[serde(skip_serializing_if = "Option::is_none")]
2139 pub did: Option<String>,
2140}
2141
2142pub async fn check_user_security_status(
2143 State(state): State<AppState>,
2144 Query(query): Query<CheckPasskeysQuery>,
2145) -> Response {
2146 let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
2147 let normalized_identifier =
2148 NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles);
2149
2150 let user = state
2151 .user_repo
2152 .get_login_check_by_handle_or_email(normalized_identifier.as_str())
2153 .await;
2154
2155 let (has_passkeys, has_totp, has_password, is_delegated, did): (
2156 bool,
2157 bool,
2158 bool,
2159 bool,
2160 Option<String>,
2161 ) = match user {
2162 Ok(Some(u)) => {
2163 let passkeys = tranquil_api::server::has_passkeys_for_user(&state, &u.did).await;
2164 let totp = tranquil_api::server::has_totp_enabled(&state, &u.did).await;
2165 let has_pw = u.password_hash.is_some();
2166 let has_controllers = state
2167 .delegation_repo
2168 .is_delegated_account(&u.did)
2169 .await
2170 .unwrap_or(false);
2171 (
2172 passkeys,
2173 totp,
2174 has_pw,
2175 has_controllers,
2176 Some(u.did.to_string()),
2177 )
2178 }
2179 _ => (false, false, false, false, None),
2180 };
2181
2182 Json(SecurityStatusResponse {
2183 has_passkeys,
2184 has_totp,
2185 has_password,
2186 is_delegated,
2187 did,
2188 })
2189 .into_response()
2190}
2191
2192#[derive(Debug, Deserialize)]
2193pub struct PasskeyStartInput {
2194 pub request_uri: String,
2195 pub identifier: String,
2196 pub delegated_did: Option<String>,
2197}
2198
2199#[derive(Debug, Serialize)]
2200#[serde(rename_all = "camelCase")]
2201pub struct PasskeyStartResponse {
2202 pub options: serde_json::Value,
2203}
2204
2205pub async fn passkey_start(
2206 State(state): State<AppState>,
2207 _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>,
2208 Json(form): Json<PasskeyStartInput>,
2209) -> Response {
2210 let passkey_start_request_id = RequestId::from(form.request_uri.clone());
2211 let request_data = match state
2212 .oauth_repo
2213 .get_authorization_request(&passkey_start_request_id)
2214 .await
2215 {
2216 Ok(Some(data)) => data,
2217 Ok(None) => {
2218 return (
2219 StatusCode::BAD_REQUEST,
2220 Json(serde_json::json!({
2221 "error": "invalid_request",
2222 "error_description": "Invalid or expired request_uri."
2223 })),
2224 )
2225 .into_response();
2226 }
2227 Err(_) => {
2228 return (
2229 StatusCode::INTERNAL_SERVER_ERROR,
2230 Json(serde_json::json!({
2231 "error": "server_error",
2232 "error_description": "An error occurred."
2233 })),
2234 )
2235 .into_response();
2236 }
2237 };
2238
2239 if request_data.expires_at < Utc::now() {
2240 let _ = state
2241 .oauth_repo
2242 .delete_authorization_request(&passkey_start_request_id)
2243 .await;
2244 return (
2245 StatusCode::BAD_REQUEST,
2246 Json(serde_json::json!({
2247 "error": "invalid_request",
2248 "error_description": "Authorization request has expired."
2249 })),
2250 )
2251 .into_response();
2252 }
2253
2254 let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
2255 let normalized_username =
2256 NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles);
2257
2258 let user = match state
2259 .user_repo
2260 .get_login_info_by_handle_or_email(normalized_username.as_str())
2261 .await
2262 {
2263 Ok(Some(u)) => u,
2264 Ok(None) => {
2265 return (
2266 StatusCode::FORBIDDEN,
2267 Json(serde_json::json!({
2268 "error": "access_denied",
2269 "error_description": "User not found or has no passkeys."
2270 })),
2271 )
2272 .into_response();
2273 }
2274 Err(_) => {
2275 return (
2276 StatusCode::INTERNAL_SERVER_ERROR,
2277 Json(serde_json::json!({
2278 "error": "server_error",
2279 "error_description": "An error occurred."
2280 })),
2281 )
2282 .into_response();
2283 }
2284 };
2285
2286 if user.deactivated_at.is_some() {
2287 return (
2288 StatusCode::FORBIDDEN,
2289 Json(serde_json::json!({
2290 "error": "access_denied",
2291 "error_description": "This account has been deactivated."
2292 })),
2293 )
2294 .into_response();
2295 }
2296
2297 if user.takedown_ref.is_some() {
2298 return (
2299 StatusCode::FORBIDDEN,
2300 Json(serde_json::json!({
2301 "error": "access_denied",
2302 "error_description": "This account has been taken down."
2303 })),
2304 )
2305 .into_response();
2306 }
2307
2308 let is_verified = user.channel_verification.has_any_verified();
2309
2310 if !is_verified {
2311 let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await;
2312 return (
2313 StatusCode::FORBIDDEN,
2314 Json(serde_json::json!({
2315 "error": "account_not_verified",
2316 "error_description": "Please verify your account before logging in.",
2317 "did": user.did,
2318 "handle": resend_info.as_ref().map(|r| r.handle.to_string()),
2319 "channel": resend_info.as_ref().map(|r| r.channel.as_str())
2320 })),
2321 )
2322 .into_response();
2323 }
2324
2325 let stored_passkeys = match state.user_repo.get_passkeys_for_user(&user.did).await {
2326 Ok(pks) => pks,
2327 Err(e) => {
2328 tracing::error!(error = %e, "Failed to get passkeys");
2329 return (
2330 StatusCode::INTERNAL_SERVER_ERROR,
2331 Json(serde_json::json!({
2332 "error": "server_error",
2333 "error_description": "An error occurred."
2334 })),
2335 )
2336 .into_response();
2337 }
2338 };
2339
2340 if stored_passkeys.is_empty() {
2341 return (
2342 StatusCode::FORBIDDEN,
2343 Json(serde_json::json!({
2344 "error": "access_denied",
2345 "error_description": "User not found or has no passkeys."
2346 })),
2347 )
2348 .into_response();
2349 }
2350
2351 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
2352 .iter()
2353 .filter_map(|sp| serde_json::from_slice(&sp.public_key).ok())
2354 .collect();
2355
2356 if passkeys.is_empty() {
2357 return (
2358 StatusCode::INTERNAL_SERVER_ERROR,
2359 Json(serde_json::json!({
2360 "error": "server_error",
2361 "error_description": "Failed to load passkeys."
2362 })),
2363 )
2364 .into_response();
2365 }
2366
2367 let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) {
2368 Ok(result) => result,
2369 Err(e) => {
2370 tracing::error!(error = %e, "Failed to start passkey authentication");
2371 return (
2372 StatusCode::INTERNAL_SERVER_ERROR,
2373 Json(serde_json::json!({
2374 "error": "server_error",
2375 "error_description": "Failed to start authentication."
2376 })),
2377 )
2378 .into_response();
2379 }
2380 };
2381
2382 let state_json = match serde_json::to_string(&auth_state) {
2383 Ok(j) => j,
2384 Err(e) => {
2385 tracing::error!(error = %e, "Failed to serialize authentication state");
2386 return (
2387 StatusCode::INTERNAL_SERVER_ERROR,
2388 Json(serde_json::json!({
2389 "error": "server_error",
2390 "error_description": "An error occurred."
2391 })),
2392 )
2393 .into_response();
2394 }
2395 };
2396
2397 if let Err(e) = state
2398 .user_repo
2399 .save_webauthn_challenge(
2400 &user.did,
2401 WebauthnChallengeType::Authentication,
2402 &state_json,
2403 )
2404 .await
2405 {
2406 tracing::error!(error = %e, "Failed to save authentication state");
2407 return (
2408 StatusCode::INTERNAL_SERVER_ERROR,
2409 Json(serde_json::json!({
2410 "error": "server_error",
2411 "error_description": "An error occurred."
2412 })),
2413 )
2414 .into_response();
2415 }
2416
2417 let delegation_from_param = match &form.delegated_did {
2418 Some(delegated_did_str) => match delegated_did_str.parse::<tranquil_types::Did>() {
2419 Ok(delegated_did) if delegated_did != user.did => {
2420 match state
2421 .delegation_repo
2422 .get_delegation(&delegated_did, &user.did)
2423 .await
2424 {
2425 Ok(Some(_)) => Some(delegated_did),
2426 Ok(None) => None,
2427 Err(e) => {
2428 tracing::warn!(
2429 error = %e,
2430 delegated_did = %delegated_did,
2431 controller_did = %user.did,
2432 "Failed to verify delegation relationship"
2433 );
2434 None
2435 }
2436 }
2437 }
2438 _ => None,
2439 },
2440 None => None,
2441 };
2442
2443 let is_delegation_flow = delegation_from_param.is_some()
2444 || request_data.did.as_ref().is_some_and(|existing_did| {
2445 existing_did
2446 .parse::<tranquil_types::Did>()
2447 .ok()
2448 .is_some_and(|parsed| parsed != user.did)
2449 });
2450
2451 if let Some(delegated_did) = delegation_from_param {
2452 tracing::info!(
2453 delegated_did = %delegated_did,
2454 controller_did = %user.did,
2455 "Passkey auth with delegated_did param - setting delegation flow"
2456 );
2457 if state
2458 .oauth_repo
2459 .set_authorization_did(&passkey_start_request_id, &delegated_did, None)
2460 .await
2461 .is_err()
2462 {
2463 return OAuthError::ServerError("An error occurred.".into()).into_response();
2464 }
2465 if state
2466 .oauth_repo
2467 .set_controller_did(&passkey_start_request_id, &user.did)
2468 .await
2469 .is_err()
2470 {
2471 return OAuthError::ServerError("An error occurred.".into()).into_response();
2472 }
2473 } else if is_delegation_flow {
2474 tracing::info!(
2475 delegated_did = ?request_data.did,
2476 controller_did = %user.did,
2477 "Passkey auth in delegation flow - preserving delegated DID"
2478 );
2479 if state
2480 .oauth_repo
2481 .set_controller_did(&passkey_start_request_id, &user.did)
2482 .await
2483 .is_err()
2484 {
2485 return OAuthError::ServerError("An error occurred.".into()).into_response();
2486 }
2487 } else if state
2488 .oauth_repo
2489 .set_authorization_did(&passkey_start_request_id, &user.did, None)
2490 .await
2491 .is_err()
2492 {
2493 return OAuthError::ServerError("An error occurred.".into()).into_response();
2494 }
2495
2496 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
2497
2498 Json(PasskeyStartResponse { options }).into_response()
2499}
2500
2501#[derive(Debug, Deserialize)]
2502pub struct PasskeyFinishInput {
2503 pub request_uri: String,
2504 pub credential: serde_json::Value,
2505}
2506
2507pub async fn passkey_finish(
2508 State(state): State<AppState>,
2509 headers: HeaderMap,
2510 Json(form): Json<PasskeyFinishInput>,
2511) -> Response {
2512 let passkey_finish_request_id = RequestId::from(form.request_uri.clone());
2513 let request_data = match state
2514 .oauth_repo
2515 .get_authorization_request(&passkey_finish_request_id)
2516 .await
2517 {
2518 Ok(Some(data)) => data,
2519 Ok(None) => {
2520 return (
2521 StatusCode::BAD_REQUEST,
2522 Json(serde_json::json!({
2523 "error": "invalid_request",
2524 "error_description": "Invalid or expired request_uri."
2525 })),
2526 )
2527 .into_response();
2528 }
2529 Err(_) => {
2530 return (
2531 StatusCode::INTERNAL_SERVER_ERROR,
2532 Json(serde_json::json!({
2533 "error": "server_error",
2534 "error_description": "An error occurred."
2535 })),
2536 )
2537 .into_response();
2538 }
2539 };
2540
2541 if request_data.expires_at < Utc::now() {
2542 let _ = state
2543 .oauth_repo
2544 .delete_authorization_request(&passkey_finish_request_id)
2545 .await;
2546 return (
2547 StatusCode::BAD_REQUEST,
2548 Json(serde_json::json!({
2549 "error": "invalid_request",
2550 "error_description": "Authorization request has expired."
2551 })),
2552 )
2553 .into_response();
2554 }
2555
2556 let did_str = match request_data.did {
2557 Some(d) => d,
2558 None => {
2559 return (
2560 StatusCode::BAD_REQUEST,
2561 Json(serde_json::json!({
2562 "error": "invalid_request",
2563 "error_description": "No passkey authentication in progress."
2564 })),
2565 )
2566 .into_response();
2567 }
2568 };
2569 let did: tranquil_types::Did = match did_str.parse() {
2570 Ok(d) => d,
2571 Err(_) => {
2572 return (
2573 StatusCode::BAD_REQUEST,
2574 Json(serde_json::json!({
2575 "error": "invalid_request",
2576 "error_description": "Invalid DID format."
2577 })),
2578 )
2579 .into_response();
2580 }
2581 };
2582
2583 let controller_did: Option<tranquil_types::Did> = request_data
2584 .controller_did
2585 .as_ref()
2586 .and_then(|s| s.parse().ok());
2587 let passkey_owner_did = controller_did.as_ref().unwrap_or(&did);
2588
2589 let auth_state_json = match state
2590 .user_repo
2591 .load_webauthn_challenge(passkey_owner_did, WebauthnChallengeType::Authentication)
2592 .await
2593 {
2594 Ok(Some(s)) => s,
2595 Ok(None) => {
2596 return (
2597 StatusCode::BAD_REQUEST,
2598 Json(serde_json::json!({
2599 "error": "invalid_request",
2600 "error_description": "No passkey authentication in progress or challenge expired."
2601 })),
2602 )
2603 .into_response();
2604 }
2605 Err(e) => {
2606 tracing::error!(error = %e, "Failed to load authentication state");
2607 return (
2608 StatusCode::INTERNAL_SERVER_ERROR,
2609 Json(serde_json::json!({
2610 "error": "server_error",
2611 "error_description": "An error occurred."
2612 })),
2613 )
2614 .into_response();
2615 }
2616 };
2617
2618 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication =
2619 match serde_json::from_str(&auth_state_json) {
2620 Ok(s) => s,
2621 Err(e) => {
2622 tracing::error!(error = %e, "Failed to deserialize authentication state");
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 let credential: webauthn_rs::prelude::PublicKeyCredential =
2635 match serde_json::from_value(form.credential) {
2636 Ok(c) => c,
2637 Err(e) => {
2638 tracing::warn!(error = %e, "Failed to parse credential");
2639 return (
2640 StatusCode::BAD_REQUEST,
2641 Json(serde_json::json!({
2642 "error": "invalid_request",
2643 "error_description": "Failed to parse credential response."
2644 })),
2645 )
2646 .into_response();
2647 }
2648 };
2649
2650 let auth_result = match state
2651 .webauthn_config
2652 .finish_authentication(&credential, &auth_state)
2653 {
2654 Ok(r) => r,
2655 Err(e) => {
2656 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication");
2657 return (
2658 StatusCode::FORBIDDEN,
2659 Json(serde_json::json!({
2660 "error": "access_denied",
2661 "error_description": "Passkey verification failed."
2662 })),
2663 )
2664 .into_response();
2665 }
2666 };
2667
2668 if let Err(e) = state
2669 .user_repo
2670 .delete_webauthn_challenge(passkey_owner_did, WebauthnChallengeType::Authentication)
2671 .await
2672 {
2673 tracing::warn!(error = %e, "Failed to delete authentication state");
2674 }
2675
2676 if auth_result.needs_update() {
2677 let cred_id_bytes = auth_result.cred_id().as_slice();
2678 match state
2679 .user_repo
2680 .update_passkey_counter(
2681 cred_id_bytes,
2682 i32::try_from(auth_result.counter()).unwrap_or(i32::MAX),
2683 )
2684 .await
2685 {
2686 Ok(false) => {
2687 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key");
2688 return (
2689 StatusCode::FORBIDDEN,
2690 Json(serde_json::json!({
2691 "error": "access_denied",
2692 "error_description": "Security key counter anomaly detected. This may indicate a cloned key."
2693 })),
2694 )
2695 .into_response();
2696 }
2697 Err(e) => {
2698 tracing::warn!(error = %e, "Failed to update passkey counter");
2699 }
2700 Ok(true) => {}
2701 }
2702 }
2703
2704 tracing::info!(did = %did, "Passkey authentication successful");
2705
2706 let device_id = extract_device_cookie(&headers);
2707 let requested_scope_str = request_data
2708 .parameters
2709 .scope
2710 .as_deref()
2711 .unwrap_or("atproto");
2712 let requested_scopes: Vec<String> = requested_scope_str
2713 .split_whitespace()
2714 .map(|s| s.to_string())
2715 .collect();
2716
2717 let passkey_finish_client_id = ClientId::from(request_data.parameters.client_id.clone());
2718 let needs_consent = should_show_consent(
2719 state.oauth_repo.as_ref(),
2720 &did,
2721 &passkey_finish_client_id,
2722 &requested_scopes,
2723 )
2724 .await
2725 .unwrap_or(true);
2726
2727 if needs_consent {
2728 let consent_url = format!(
2729 "/app/oauth/consent?request_uri={}",
2730 url_encode(&form.request_uri)
2731 );
2732 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
2733 }
2734
2735 let code = Code::generate();
2736 let passkey_final_device_id = device_id.clone();
2737 let passkey_final_code = AuthorizationCode::from(code.0.clone());
2738 if state
2739 .oauth_repo
2740 .update_authorization_request(
2741 &passkey_finish_request_id,
2742 &did,
2743 passkey_final_device_id.as_ref(),
2744 &passkey_final_code,
2745 )
2746 .await
2747 .is_err()
2748 {
2749 return (
2750 StatusCode::INTERNAL_SERVER_ERROR,
2751 Json(serde_json::json!({
2752 "error": "server_error",
2753 "error_description": "An error occurred."
2754 })),
2755 )
2756 .into_response();
2757 }
2758
2759 let redirect_url = build_intermediate_redirect_url(
2760 &request_data.parameters.redirect_uri,
2761 &code.0,
2762 request_data.parameters.state.as_deref(),
2763 request_data.parameters.response_mode.map(|m| m.as_str()),
2764 );
2765
2766 Json(serde_json::json!({
2767 "redirect_uri": redirect_url
2768 }))
2769 .into_response()
2770}
2771
2772#[derive(Debug, Deserialize)]
2773pub struct AuthorizePasskeyQuery {
2774 pub request_uri: String,
2775}
2776
2777#[derive(Debug, Serialize)]
2778#[serde(rename_all = "camelCase")]
2779pub struct PasskeyAuthResponse {
2780 pub options: serde_json::Value,
2781 pub request_uri: String,
2782}
2783
2784pub async fn authorize_passkey_start(
2785 State(state): State<AppState>,
2786 Query(query): Query<AuthorizePasskeyQuery>,
2787) -> Response {
2788 let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone());
2789 let request_data = match state
2790 .oauth_repo
2791 .get_authorization_request(&auth_passkey_start_request_id)
2792 .await
2793 {
2794 Ok(Some(d)) => d,
2795 Ok(None) => {
2796 return (
2797 StatusCode::BAD_REQUEST,
2798 Json(serde_json::json!({
2799 "error": "invalid_request",
2800 "error_description": "Authorization request not found."
2801 })),
2802 )
2803 .into_response();
2804 }
2805 Err(_) => {
2806 return (
2807 StatusCode::INTERNAL_SERVER_ERROR,
2808 Json(serde_json::json!({
2809 "error": "server_error",
2810 "error_description": "An error occurred."
2811 })),
2812 )
2813 .into_response();
2814 }
2815 };
2816
2817 if request_data.expires_at < Utc::now() {
2818 let _ = state
2819 .oauth_repo
2820 .delete_authorization_request(&auth_passkey_start_request_id)
2821 .await;
2822 return (
2823 StatusCode::BAD_REQUEST,
2824 Json(serde_json::json!({
2825 "error": "invalid_request",
2826 "error_description": "Authorization request has expired."
2827 })),
2828 )
2829 .into_response();
2830 }
2831
2832 let did_str = match &request_data.did {
2833 Some(d) => d.clone(),
2834 None => {
2835 return (
2836 StatusCode::BAD_REQUEST,
2837 Json(serde_json::json!({
2838 "error": "invalid_request",
2839 "error_description": "User not authenticated yet."
2840 })),
2841 )
2842 .into_response();
2843 }
2844 };
2845
2846 let did: tranquil_types::Did = match did_str.parse() {
2847 Ok(d) => d,
2848 Err(_) => {
2849 return (
2850 StatusCode::BAD_REQUEST,
2851 Json(serde_json::json!({
2852 "error": "invalid_request",
2853 "error_description": "Invalid DID format."
2854 })),
2855 )
2856 .into_response();
2857 }
2858 };
2859
2860 let stored_passkeys = match state.user_repo.get_passkeys_for_user(&did).await {
2861 Ok(pks) => pks,
2862 Err(e) => {
2863 tracing::error!("Failed to get passkeys: {:?}", e);
2864 return (
2865 StatusCode::INTERNAL_SERVER_ERROR,
2866 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2867 )
2868 .into_response();
2869 }
2870 };
2871
2872 if stored_passkeys.is_empty() {
2873 return (
2874 StatusCode::BAD_REQUEST,
2875 Json(serde_json::json!({
2876 "error": "invalid_request",
2877 "error_description": "No passkeys registered for this account."
2878 })),
2879 )
2880 .into_response();
2881 }
2882
2883 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
2884 .iter()
2885 .filter_map(|sp| serde_json::from_slice(&sp.public_key).ok())
2886 .collect();
2887
2888 if passkeys.is_empty() {
2889 return (
2890 StatusCode::INTERNAL_SERVER_ERROR,
2891 Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})),
2892 )
2893 .into_response();
2894 }
2895
2896 let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) {
2897 Ok(result) => result,
2898 Err(e) => {
2899 tracing::error!("Failed to start passkey authentication: {:?}", e);
2900 return (
2901 StatusCode::INTERNAL_SERVER_ERROR,
2902 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2903 )
2904 .into_response();
2905 }
2906 };
2907
2908 let state_json = match serde_json::to_string(&auth_state) {
2909 Ok(j) => j,
2910 Err(e) => {
2911 tracing::error!("Failed to serialize authentication state: {:?}", e);
2912 return (
2913 StatusCode::INTERNAL_SERVER_ERROR,
2914 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2915 )
2916 .into_response();
2917 }
2918 };
2919
2920 if let Err(e) = state
2921 .user_repo
2922 .save_webauthn_challenge(&did, WebauthnChallengeType::Authentication, &state_json)
2923 .await
2924 {
2925 tracing::error!("Failed to save authentication state: {:?}", e);
2926 return (
2927 StatusCode::INTERNAL_SERVER_ERROR,
2928 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
2929 )
2930 .into_response();
2931 }
2932
2933 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
2934 Json(PasskeyAuthResponse {
2935 options,
2936 request_uri: query.request_uri,
2937 })
2938 .into_response()
2939}
2940
2941#[derive(Debug, Deserialize)]
2942#[serde(rename_all = "camelCase")]
2943pub struct AuthorizePasskeySubmit {
2944 pub request_uri: String,
2945 pub credential: serde_json::Value,
2946}
2947
2948pub async fn authorize_passkey_finish(
2949 State(state): State<AppState>,
2950 headers: HeaderMap,
2951 Json(form): Json<AuthorizePasskeySubmit>,
2952) -> Response {
2953 let pds_hostname = &tranquil_config::get().server.hostname;
2954 let passkey_finish_request_id = RequestId::from(form.request_uri.clone());
2955
2956 let request_data = match state
2957 .oauth_repo
2958 .get_authorization_request(&passkey_finish_request_id)
2959 .await
2960 {
2961 Ok(Some(d)) => d,
2962 Ok(None) => {
2963 return (
2964 StatusCode::BAD_REQUEST,
2965 Json(serde_json::json!({
2966 "error": "invalid_request",
2967 "error_description": "Authorization request not found."
2968 })),
2969 )
2970 .into_response();
2971 }
2972 Err(_) => {
2973 return (
2974 StatusCode::INTERNAL_SERVER_ERROR,
2975 Json(serde_json::json!({
2976 "error": "server_error",
2977 "error_description": "An error occurred."
2978 })),
2979 )
2980 .into_response();
2981 }
2982 };
2983
2984 if request_data.expires_at < Utc::now() {
2985 let _ = state
2986 .oauth_repo
2987 .delete_authorization_request(&passkey_finish_request_id)
2988 .await;
2989 return (
2990 StatusCode::BAD_REQUEST,
2991 Json(serde_json::json!({
2992 "error": "invalid_request",
2993 "error_description": "Authorization request has expired."
2994 })),
2995 )
2996 .into_response();
2997 }
2998
2999 let did_str = match &request_data.did {
3000 Some(d) => d.clone(),
3001 None => {
3002 return (
3003 StatusCode::BAD_REQUEST,
3004 Json(serde_json::json!({
3005 "error": "invalid_request",
3006 "error_description": "User not authenticated yet."
3007 })),
3008 )
3009 .into_response();
3010 }
3011 };
3012
3013 let did: tranquil_types::Did = match did_str.parse() {
3014 Ok(d) => d,
3015 Err(_) => {
3016 return (
3017 StatusCode::BAD_REQUEST,
3018 Json(serde_json::json!({
3019 "error": "invalid_request",
3020 "error_description": "Invalid DID format."
3021 })),
3022 )
3023 .into_response();
3024 }
3025 };
3026
3027 let auth_state_json = match state
3028 .user_repo
3029 .load_webauthn_challenge(&did, WebauthnChallengeType::Authentication)
3030 .await
3031 {
3032 Ok(Some(s)) => s,
3033 Ok(None) => {
3034 return (
3035 StatusCode::BAD_REQUEST,
3036 Json(serde_json::json!({
3037 "error": "invalid_request",
3038 "error_description": "No passkey challenge found. Please start over."
3039 })),
3040 )
3041 .into_response();
3042 }
3043 Err(e) => {
3044 tracing::error!("Failed to load authentication state: {:?}", e);
3045 return (
3046 StatusCode::INTERNAL_SERVER_ERROR,
3047 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
3048 )
3049 .into_response();
3050 }
3051 };
3052
3053 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = match serde_json::from_str(
3054 &auth_state_json,
3055 ) {
3056 Ok(s) => s,
3057 Err(e) => {
3058 tracing::error!("Failed to deserialize authentication state: {:?}", e);
3059 return (
3060 StatusCode::INTERNAL_SERVER_ERROR,
3061 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
3062 )
3063 .into_response();
3064 }
3065 };
3066
3067 let credential: webauthn_rs::prelude::PublicKeyCredential =
3068 match serde_json::from_value(form.credential.clone()) {
3069 Ok(c) => c,
3070 Err(e) => {
3071 tracing::error!("Failed to parse credential: {:?}", e);
3072 return (
3073 StatusCode::BAD_REQUEST,
3074 Json(serde_json::json!({
3075 "error": "invalid_request",
3076 "error_description": "Invalid credential format."
3077 })),
3078 )
3079 .into_response();
3080 }
3081 };
3082
3083 let auth_result = match state
3084 .webauthn_config
3085 .finish_authentication(&credential, &auth_state)
3086 {
3087 Ok(r) => r,
3088 Err(e) => {
3089 tracing::warn!("Passkey authentication failed: {:?}", e);
3090 return (
3091 StatusCode::FORBIDDEN,
3092 Json(serde_json::json!({
3093 "error": "access_denied",
3094 "error_description": "Passkey authentication failed."
3095 })),
3096 )
3097 .into_response();
3098 }
3099 };
3100
3101 let _ = state
3102 .user_repo
3103 .delete_webauthn_challenge(&did, WebauthnChallengeType::Authentication)
3104 .await;
3105
3106 match state
3107 .user_repo
3108 .update_passkey_counter(
3109 credential.id.as_ref(),
3110 i32::try_from(auth_result.counter()).unwrap_or(i32::MAX),
3111 )
3112 .await
3113 {
3114 Ok(false) => {
3115 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key");
3116 return (
3117 StatusCode::FORBIDDEN,
3118 Json(serde_json::json!({
3119 "error": "access_denied",
3120 "error_description": "Security key counter anomaly detected. This may indicate a cloned key."
3121 })),
3122 )
3123 .into_response();
3124 }
3125 Err(e) => {
3126 tracing::warn!("Failed to update passkey counter: {:?}", e);
3127 }
3128 Ok(true) => {}
3129 }
3130
3131 let has_totp = state
3132 .user_repo
3133 .has_totp_enabled(&did)
3134 .await
3135 .unwrap_or(false);
3136 if has_totp {
3137 let device_cookie = extract_device_cookie(&headers);
3138 let device_is_trusted = if let Some(ref dev_id) = device_cookie {
3139 tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &did).await
3140 } else {
3141 false
3142 };
3143
3144 if device_is_trusted {
3145 if let Some(ref dev_id) = device_cookie {
3146 let _ =
3147 tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id)
3148 .await;
3149 }
3150 } else {
3151 let user = match state.user_repo.get_2fa_status_by_did(&did).await {
3152 Ok(Some(u)) => u,
3153 _ => {
3154 return (
3155 StatusCode::INTERNAL_SERVER_ERROR,
3156 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
3157 )
3158 .into_response();
3159 }
3160 };
3161
3162 let _ = state
3163 .oauth_repo
3164 .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id)
3165 .await;
3166 match state
3167 .oauth_repo
3168 .create_2fa_challenge(&did, &passkey_finish_request_id)
3169 .await
3170 {
3171 Ok(challenge) => {
3172 if let Err(e) = enqueue_2fa_code(
3173 state.user_repo.as_ref(),
3174 state.infra_repo.as_ref(),
3175 user.id,
3176 &challenge.code,
3177 pds_hostname,
3178 )
3179 .await
3180 {
3181 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification");
3182 }
3183 let channel_name = user.preferred_comms_channel.display_name();
3184 let redirect_url = format!(
3185 "/app/oauth/2fa?request_uri={}&channel={}",
3186 url_encode(&form.request_uri),
3187 url_encode(channel_name)
3188 );
3189 return (
3190 StatusCode::OK,
3191 Json(serde_json::json!({
3192 "next": "2fa",
3193 "redirect": redirect_url
3194 })),
3195 )
3196 .into_response();
3197 }
3198 Err(_) => {
3199 return (
3200 StatusCode::INTERNAL_SERVER_ERROR,
3201 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})),
3202 )
3203 .into_response();
3204 }
3205 }
3206 }
3207 }
3208
3209 let redirect_url = format!(
3210 "/app/oauth/consent?request_uri={}",
3211 url_encode(&form.request_uri)
3212 );
3213 (
3214 StatusCode::OK,
3215 Json(serde_json::json!({
3216 "next": "consent",
3217 "redirect": redirect_url
3218 })),
3219 )
3220 .into_response()
3221}
3222
3223#[derive(Debug, Deserialize)]
3224pub struct RegisterCompleteInput {
3225 pub request_uri: String,
3226 pub did: String,
3227 pub app_password: String,
3228}
3229
3230pub async fn register_complete(
3231 State(state): State<AppState>,
3232 _rate_limit: OAuthRateLimited<OAuthRegisterCompleteLimit>,
3233 Json(form): Json<RegisterCompleteInput>,
3234) -> Response {
3235 let did = Did::from(form.did.clone());
3236
3237 let request_id = RequestId::from(form.request_uri.clone());
3238 let request_data = match state
3239 .oauth_repo
3240 .get_authorization_request(&request_id)
3241 .await
3242 {
3243 Ok(Some(data)) => data,
3244 Ok(None) => {
3245 return (
3246 StatusCode::BAD_REQUEST,
3247 Json(serde_json::json!({
3248 "error": "invalid_request",
3249 "error_description": "Invalid or expired request_uri."
3250 })),
3251 )
3252 .into_response();
3253 }
3254 Err(e) => {
3255 tracing::error!(
3256 request_uri = %form.request_uri,
3257 error = ?e,
3258 "register_complete: failed to fetch authorization request"
3259 );
3260 return (
3261 StatusCode::INTERNAL_SERVER_ERROR,
3262 Json(serde_json::json!({
3263 "error": "server_error",
3264 "error_description": "An error occurred."
3265 })),
3266 )
3267 .into_response();
3268 }
3269 };
3270
3271 if request_data.expires_at < Utc::now() {
3272 let _ = state
3273 .oauth_repo
3274 .delete_authorization_request(&request_id)
3275 .await;
3276 return (
3277 StatusCode::BAD_REQUEST,
3278 Json(serde_json::json!({
3279 "error": "invalid_request",
3280 "error_description": "Authorization request has expired."
3281 })),
3282 )
3283 .into_response();
3284 }
3285
3286 if request_data.parameters.prompt != Some(Prompt::Create) {
3287 tracing::warn!(
3288 request_uri = %form.request_uri,
3289 prompt = ?request_data.parameters.prompt,
3290 "register_complete called on non-registration OAuth flow"
3291 );
3292 return (
3293 StatusCode::BAD_REQUEST,
3294 Json(serde_json::json!({
3295 "error": "invalid_request",
3296 "error_description": "This endpoint is only for registration flows."
3297 })),
3298 )
3299 .into_response();
3300 }
3301
3302 if request_data.code.is_some() {
3303 tracing::warn!(
3304 request_uri = %form.request_uri,
3305 "register_complete called on already-completed OAuth flow"
3306 );
3307 return (
3308 StatusCode::BAD_REQUEST,
3309 Json(serde_json::json!({
3310 "error": "invalid_request",
3311 "error_description": "Authorization has already been completed."
3312 })),
3313 )
3314 .into_response();
3315 }
3316
3317 if let Some(existing_did) = &request_data.did
3318 && existing_did != &form.did
3319 {
3320 tracing::warn!(
3321 request_uri = %form.request_uri,
3322 existing_did = %existing_did,
3323 attempted_did = %form.did,
3324 "register_complete attempted with different DID than already bound"
3325 );
3326 return (
3327 StatusCode::BAD_REQUEST,
3328 Json(serde_json::json!({
3329 "error": "invalid_request",
3330 "error_description": "Authorization request is already bound to a different account."
3331 })),
3332 )
3333 .into_response();
3334 }
3335
3336 let password_hashes = match state
3337 .session_repo
3338 .get_app_password_hashes_by_did(&did)
3339 .await
3340 {
3341 Ok(hashes) => hashes,
3342 Err(e) => {
3343 tracing::error!(
3344 did = %did,
3345 error = ?e,
3346 "register_complete: failed to fetch app password hashes"
3347 );
3348 return (
3349 StatusCode::INTERNAL_SERVER_ERROR,
3350 Json(serde_json::json!({
3351 "error": "server_error",
3352 "error_description": "An error occurred."
3353 })),
3354 )
3355 .into_response();
3356 }
3357 };
3358
3359 let mut password_valid = password_hashes.iter().fold(false, |acc, hash| {
3360 acc | bcrypt::verify(&form.app_password, hash).unwrap_or(false)
3361 });
3362
3363 if !password_valid
3364 && let Ok(Some(account_hash)) = state.user_repo.get_password_hash_by_did(&did).await
3365 {
3366 password_valid = bcrypt::verify(&form.app_password, &account_hash).unwrap_or(false);
3367 }
3368
3369 if !password_valid {
3370 return (
3371 StatusCode::FORBIDDEN,
3372 Json(serde_json::json!({
3373 "error": "access_denied",
3374 "error_description": "Invalid credentials."
3375 })),
3376 )
3377 .into_response();
3378 }
3379
3380 let is_verified = match state.user_repo.get_session_info_by_did(&did).await {
3381 Ok(Some(info)) => info.channel_verification.has_any_verified(),
3382 Ok(None) => {
3383 return (
3384 StatusCode::FORBIDDEN,
3385 Json(serde_json::json!({
3386 "error": "access_denied",
3387 "error_description": "Account not found."
3388 })),
3389 )
3390 .into_response();
3391 }
3392 Err(e) => {
3393 tracing::error!(
3394 did = %did,
3395 error = ?e,
3396 "register_complete: failed to fetch session info"
3397 );
3398 return (
3399 StatusCode::INTERNAL_SERVER_ERROR,
3400 Json(serde_json::json!({
3401 "error": "server_error",
3402 "error_description": "An error occurred."
3403 })),
3404 )
3405 .into_response();
3406 }
3407 };
3408
3409 if !is_verified {
3410 let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await;
3411 return (
3412 StatusCode::FORBIDDEN,
3413 Json(serde_json::json!({
3414 "error": "account_not_verified",
3415 "error_description": "Please verify your account before continuing.",
3416 "did": did,
3417 "handle": resend_info.as_ref().map(|r| r.handle.to_string()),
3418 "channel": resend_info.as_ref().map(|r| r.channel.as_str())
3419 })),
3420 )
3421 .into_response();
3422 }
3423
3424 if let Err(e) = state
3425 .oauth_repo
3426 .set_authorization_did(&request_id, &did, None)
3427 .await
3428 {
3429 tracing::error!(
3430 request_uri = %form.request_uri,
3431 did = %did,
3432 error = ?e,
3433 "register_complete: failed to set authorization DID"
3434 );
3435 return (
3436 StatusCode::INTERNAL_SERVER_ERROR,
3437 Json(serde_json::json!({
3438 "error": "server_error",
3439 "error_description": "An error occurred."
3440 })),
3441 )
3442 .into_response();
3443 }
3444
3445 let requested_scope_str = request_data
3446 .parameters
3447 .scope
3448 .as_deref()
3449 .unwrap_or("atproto");
3450 let requested_scopes: Vec<String> = requested_scope_str
3451 .split_whitespace()
3452 .map(|s| s.to_string())
3453 .collect();
3454 let client_id_typed = ClientId::from(request_data.parameters.client_id.clone());
3455 let needs_consent = should_show_consent(
3456 state.oauth_repo.as_ref(),
3457 &did,
3458 &client_id_typed,
3459 &requested_scopes,
3460 )
3461 .await
3462 .unwrap_or(true);
3463
3464 if needs_consent {
3465 tracing::info!(
3466 did = %did,
3467 client_id = %request_data.parameters.client_id,
3468 "OAuth registration complete, redirecting to consent"
3469 );
3470 let consent_url = format!(
3471 "/app/oauth/consent?request_uri={}",
3472 url_encode(&form.request_uri)
3473 );
3474 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response();
3475 }
3476
3477 let code = Code::generate();
3478 let auth_code = AuthorizationCode::from(code.0.clone());
3479 if let Err(e) = state
3480 .oauth_repo
3481 .update_authorization_request(&request_id, &did, None, &auth_code)
3482 .await
3483 {
3484 tracing::error!(
3485 request_uri = %form.request_uri,
3486 did = %did,
3487 error = ?e,
3488 "register_complete: failed to update authorization request with code"
3489 );
3490 return (
3491 StatusCode::INTERNAL_SERVER_ERROR,
3492 Json(serde_json::json!({
3493 "error": "server_error",
3494 "error_description": "An error occurred."
3495 })),
3496 )
3497 .into_response();
3498 }
3499
3500 tracing::info!(
3501 did = %did,
3502 client_id = %request_data.parameters.client_id,
3503 "OAuth registration flow completed successfully"
3504 );
3505
3506 let redirect_url = build_intermediate_redirect_url(
3507 &request_data.parameters.redirect_uri,
3508 &code.0,
3509 request_data.parameters.state.as_deref(),
3510 request_data.parameters.response_mode.map(|m| m.as_str()),
3511 );
3512 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response()
3513}
3514
3515pub async fn establish_session(
3516 State(state): State<AppState>,
3517 headers: HeaderMap,
3518 auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
3519) -> Response {
3520 let did = &auth.did;
3521
3522 let existing_device = extract_device_cookie(&headers);
3523
3524 let (device_id, new_cookie) = match existing_device {
3525 Some(id) => {
3526 let _ = state.oauth_repo.upsert_account_device(did, &id).await;
3527 (id, None)
3528 }
3529 None => {
3530 let new_id = DeviceId::generate();
3531 let device_typed = DeviceIdType::new(new_id.0.clone());
3532 let device_data = DeviceData {
3533 session_id: SessionId::generate(),
3534 user_agent: extract_user_agent(&headers),
3535 ip_address: extract_client_ip(&headers, None),
3536 last_seen_at: Utc::now(),
3537 };
3538
3539 if let Err(e) = state
3540 .oauth_repo
3541 .create_device(&device_typed, &device_data)
3542 .await
3543 {
3544 tracing::error!(error = ?e, "Failed to create device");
3545 return (
3546 StatusCode::INTERNAL_SERVER_ERROR,
3547 Json(serde_json::json!({
3548 "error": "server_error",
3549 "error_description": "Failed to establish session"
3550 })),
3551 )
3552 .into_response();
3553 }
3554
3555 if let Err(e) = state
3556 .oauth_repo
3557 .upsert_account_device(did, &device_typed)
3558 .await
3559 {
3560 tracing::error!(error = ?e, "Failed to link device to account");
3561 return (
3562 StatusCode::INTERNAL_SERVER_ERROR,
3563 Json(serde_json::json!({
3564 "error": "server_error",
3565 "error_description": "Failed to establish session"
3566 })),
3567 )
3568 .into_response();
3569 }
3570
3571 let cookie = make_device_cookie(&device_typed);
3572 (device_typed, Some(cookie))
3573 }
3574 };
3575
3576 tracing::info!(did = %did, device_id = %device_id, "Device session established");
3577
3578 match new_cookie {
3579 Some(cookie) => (
3580 StatusCode::OK,
3581 [(SET_COOKIE, cookie)],
3582 Json(serde_json::json!({
3583 "success": true,
3584 "device_id": device_id
3585 })),
3586 )
3587 .into_response(),
3588 None => Json(serde_json::json!({
3589 "success": true,
3590 "device_id": device_id
3591 }))
3592 .into_response(),
3593 }
3594}