Files
archy/core/archipelago/src/api/rpc/mod.rs
T
archipelagoandClaude Opus 5 0de67ca6ae
Demo images / Build & push demo images (push) Successful in 4m18s
feat(security): app gate — authenticate every app port
Reproduced again on this node today: with no session cookie, six app
ports answered HTTP 200 with their real UIs (18083 LND, 8334, 8175
Fedimint Guardian, 8336 FIPS Mesh, 8090, 7777), all bound 0.0.0.0 and so
served on every host address. Same bug class as the /lnd-connect-info
and /bitcoin-rpc/ leaks closed in v1.7.120, but across every app.

LAN, Tailscale, Tor and the FIPS mesh all converge on 127.0.0.1:<port>,
so this is one gate rather than four. It lives in the daemon rather than
a per-app sidecar (umbrel's app_proxy model): rootless, no extra
container per app, and it can reuse machinery that already exists.

It invents no authentication policy. verify_password, TOTP secret
decryption, verify_code with used-step replay protection, the session
store, and — importantly — the SAME LoginRateLimiter instance as the
JSON-RPC path, so an attacker cannot get a fresh budget of password
guesses by moving to an app port. Only the transport differs, an HTML
form instead of JSON-RPC, because a browser being sent to an app cannot
speak JSON-RPC.

2FA comes for free: a session still pending its TOTP step fails
validate(), so the gate rejects it without knowing what a second factor
is.

Details worth keeping:
- 401, not a redirect. A redirect to a login page is indistinguishable
  from the app itself redirecting, and machine clients would follow it
  and parse HTML as their API response.
- Cookie and Authorization are stripped before proxying. The app has no
  use for the node session and must never be able to log or forward it.
- The challenge page names and pictures the app being opened, so the
  visitor can confirm what they are authenticating to.
- device_tokens grew `apps: Option<Vec<String>>` and verify_for_app for
  machine clients. None = node-wide, which every existing companion
  token is; migrating them by guessing a scope would silently revoke
  access nobody asked to revoke. An empty list is rejected rather than
  minted, since it reads as unrestricted while authorising nothing.

The rollout is necessarily per-app and the gate is built to say so. A
container publishing 0.0.0.0:<port> claims every host address, so the
gate cannot bind that port until the app is pinned to bind: 127.0.0.1
and recreated — gate-first is impossible, and all-at-once would recreate
every container on a node simultaneously. Every port it cannot claim is
logged at warn each sweep and recorded in GateStatus::unprotected,
surfaced by security.app-gate-status. The failure mode being designed
against is a gate that binds nothing, logs at debug, and reports success
while every app stays exactly as open as before — worse than no gate,
because it stops anyone looking. Same reasoning that ruled out an
nft drop-in, whose absence is a silent no-op.

Not yet done: pinning the 39 gated ports to loopback, repointing
HiddenServicePort at the gate, and on-node verification.

Tests: 21/21 appgate, workspace builds clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 16:46:23 -04:00

721 lines
28 KiB
Rust

mod analytics;
mod appgate;
mod ark;
mod auth;
mod backup_rpc;
mod bitcoin;
pub(crate) mod bitcoin_relay;
mod container;
mod content;
mod credentials;
mod dispatcher;
mod dwn;
mod federation;
mod fedimint;
mod fips;
mod handshake;
mod identity;
mod interfaces;
pub(crate) mod lnd;
mod marketplace;
mod mesh;
mod middleware;
mod monitoring;
mod names;
mod network;
mod node;
mod nostr;
mod onboarding_gate;
mod openwrt;
mod package;
pub(crate) use package::wyoming_satellite_keeper;
mod peers;
mod pine_status;
mod response;
mod router;
mod security;
mod seed_rpc;
mod streaming;
mod system;
mod tor;
mod totp;
mod transitional;
mod transport;
mod update;
mod vpn;
mod wallet;
mod webhooks;
use crate::auth::AuthManager;
use crate::config::Config;
use crate::container::{ContainerOrchestrator, DevContainerOrchestrator};
use crate::monitoring::MetricsStore;
use crate::port_allocator::PortAllocator;
use crate::rate_limit::{EndpointRateLimiter, LoginRateLimiter};
use crate::session::{self, SessionStore, REMEMBER_TTL};
use crate::state::StateManager;
use anyhow::{Context, Result};
use hyper::{Request, Response, StatusCode};
use std::sync::Arc;
use tracing::{debug, error};
pub use middleware::PeerAddr;
use middleware::{
derive_csrf_token, extract_client_ip, extract_cookie, sanitize_error_message,
CACHEABLE_METHODS, UNAUTHENTICATED_METHODS,
};
use response::{cookie_header, json_response, ResponseCache, RpcError, RpcRequest, RpcResponse};
/// Default dev password when no user is set up (matches mock-backend).
/// Dev builds only — the pre-setup login bypass that reads this is
/// cfg-gated out of release binaries.
#[cfg(debug_assertions)]
pub(crate) const DEV_DEFAULT_PASSWORD: &str = "password123";
pub struct RpcHandler {
config: Config,
auth_manager: AuthManager,
/// Shared lifecycle orchestrator (Dev or Prod). Always `Some` in a normal
/// build — the only reason it is `Option` is so tests that don't exercise
/// container RPCs can skip constructing one.
orchestrator: Option<Arc<dyn ContainerOrchestrator>>,
/// Concrete handle to the dev orchestrator, when we're in dev mode. Used by
/// `container-install { manifest_path }` which takes an ad-hoc manifest
/// path and is not part of the shared trait.
dev_orchestrator: Option<Arc<DevContainerOrchestrator>>,
state_manager: Arc<StateManager>,
pub(crate) metrics_store: Arc<MetricsStore>,
port_allocator: Arc<tokio::sync::Mutex<PortAllocator>>,
pub session_store: SessionStore,
login_rate_limiter: LoginRateLimiter,
/// Authentication in front of every app port. Built here rather than in
/// `server.rs` so it shares this handler's session store and login rate
/// limiter — an attacker must not get a fresh budget of password guesses
/// by moving from the dashboard to an app port.
pub(crate) app_gate: Arc<crate::appgate::AppGate>,
endpoint_rate_limiter: EndpointRateLimiter,
response_cache: ResponseCache,
mesh_service: Arc<tokio::sync::RwLock<Option<crate::mesh::MeshService>>>,
/// LoRa radio firmware-flash job state, sibling to `mesh_service` — one
/// job at a time, since flashing needs exclusive access to the port.
flash_job: crate::mesh::flash::FlashJobHandle,
transport_router: Arc<tokio::sync::RwLock<Option<Arc<crate::transport::TransportRouter>>>>,
/// Shared content-addressed blob store. Set by ApiHandler after construction
/// so mesh.send-content / mesh.fetch-content RPCs can reach it without a
/// second instance and duplicated cap_key.
pub(crate) blob_store: Arc<tokio::sync::RwLock<Option<Arc<crate::blobs::BlobStore>>>>,
/// Our own Ed25519 pubkey hex — needed by ContentRef senders for cap scoping
/// and by ContentRef receivers to request caps scoped to themselves.
pub(crate) self_pubkey_hex: Arc<tokio::sync::RwLock<Option<String>>>,
/// Kick the package scanner to run immediately (bypassing the 60s interval).
/// Used by install/update success paths so the fresh manifest (with populated
/// `interfaces.main.ui`) lands before we flip state to Running — closes the
/// "Launch button is missing for up to 60s after install" UX gap.
pub(crate) scan_kick: Arc<tokio::sync::Notify>,
/// Monotonic counter incremented by the scan loop after each completed scan.
/// Install/update success paths subscribe to this to know when a kicked scan
/// has actually finished before flipping to the terminal state.
pub(crate) scan_tick: Arc<tokio::sync::watch::Sender<u64>>,
}
impl RpcHandler {
pub async fn new(
config: Config,
state_manager: Arc<StateManager>,
metrics_store: Arc<MetricsStore>,
session_store: SessionStore,
orchestrator: Option<Arc<dyn ContainerOrchestrator>>,
dev_orchestrator: Option<Arc<DevContainerOrchestrator>>,
) -> Result<Self> {
let auth_manager = AuthManager::new(config.data_dir.clone());
let port_allocator = Arc::new(tokio::sync::Mutex::new(
PortAllocator::new(&config.data_dir).await?,
));
let login_rate_limiter = LoginRateLimiter::new();
let endpoint_rate_limiter = EndpointRateLimiter::new();
// Spawn periodic rate limiter cleanup (every 5 minutes)
{
let limiter = endpoint_rate_limiter.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
loop {
interval.tick().await;
limiter.cleanup().await;
}
});
}
{
let limiter = login_rate_limiter.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
loop {
interval.tick().await;
limiter.cleanup().await;
}
});
}
let app_gate = Arc::new(crate::appgate::AppGate::new(
session_store.clone(),
auth_manager.clone(),
login_rate_limiter.clone(),
config.data_dir.clone(),
));
Ok(Self {
config,
auth_manager,
orchestrator,
dev_orchestrator,
state_manager,
metrics_store,
port_allocator,
session_store,
login_rate_limiter,
app_gate,
endpoint_rate_limiter,
response_cache: ResponseCache::new(5),
mesh_service: Arc::new(tokio::sync::RwLock::new(None)),
flash_job: crate::mesh::flash::new_job_handle(),
transport_router: Arc::new(tokio::sync::RwLock::new(None)),
blob_store: Arc::new(tokio::sync::RwLock::new(None)),
self_pubkey_hex: Arc::new(tokio::sync::RwLock::new(None)),
scan_kick: Arc::new(tokio::sync::Notify::new()),
scan_tick: Arc::new(tokio::sync::watch::channel(0u64).0),
})
}
/// Set the mesh service (called after identity is loaded).
pub async fn set_mesh_service(&self, service: crate::mesh::MeshService) {
// If the blob store is already initialised, propagate it into the
// freshly-started mesh state so the listener can persist inline
// attachments. Mirrors `set_blob_store`'s forward-propagation.
if let Some(store) = self.blob_store.read().await.as_ref().cloned() {
*service.shared_state().blob_store.write().await = Some(store);
}
*self.mesh_service.write().await = Some(service);
}
/// Set the transport router (called after all transports are initialized).
pub async fn set_transport_router(&self, router: Arc<crate::transport::TransportRouter>) {
*self.transport_router.write().await = Some(router);
}
/// Share the blob store + our pubkey so mesh.send-content / fetch-content
/// can reach them. Called once from ApiHandler::new.
pub async fn set_blob_store(
&self,
store: Arc<crate::blobs::BlobStore>,
self_pubkey_hex: String,
) {
*self.blob_store.write().await = Some(store.clone());
*self.self_pubkey_hex.write().await = Some(self_pubkey_hex);
// Propagate into a running mesh service if one is already up — keeps
// `set_blob_store` and `set_mesh_service` order-independent.
if let Some(svc) = self.mesh_service.read().await.as_ref() {
*svc.shared_state().blob_store.write().await = Some(store);
}
}
/// Get reference to the mesh service Arc (for MeshTransport wrapper).
pub fn mesh_service_arc(&self) -> Arc<tokio::sync::RwLock<Option<crate::mesh::MeshService>>> {
Arc::clone(&self.mesh_service)
}
/// Shared Notify handle the package-scanner loop waits on (in addition to
/// its periodic tick). Install/update success paths call `notify_one()` to
/// trigger an immediate scan so the fresh manifest lands before we flip to
/// the terminal Running state.
pub fn scan_kick(&self) -> Arc<tokio::sync::Notify> {
Arc::clone(&self.scan_kick)
}
/// Sender half of the scan-completion watch channel. The scanner bumps this
/// counter after every finished scan; install/update wait for an advance
/// after kicking so they know the fresh manifest has landed.
pub fn scan_tick(&self) -> Arc<tokio::sync::watch::Sender<u64>> {
Arc::clone(&self.scan_tick)
}
fn cookie_suffix_for_request(&self, headers: &hyper::header::HeaderMap) -> &'static str {
// Only set Secure flag when the original request was over HTTPS.
// Nginx sends X-Forwarded-Proto: https for HTTPS connections.
// On LAN HTTP, Secure flag prevents browsers from sending cookies back.
if self.config.dev_mode {
return "";
}
if let Some(proto) = headers.get("x-forwarded-proto") {
if proto.as_bytes() == b"https" {
tracing::debug!("[onboarding] cookie: Secure (X-Forwarded-Proto: https)");
return "; Secure";
}
}
tracing::debug!("[onboarding] cookie: no Secure flag (HTTP or no X-Forwarded-Proto)");
""
}
pub async fn handle(
self: Arc<Self>,
req: Request<hyper::Body>,
) -> Result<Response<hyper::Body>> {
// Extract session cookie before consuming the request
let (parts, body) = req.into_parts();
let session_token = session::extract_session_cookie(&parts.headers);
let secure_suffix = self.cookie_suffix_for_request(&parts.headers);
let body_bytes = hyper::body::to_bytes(body)
.await
.context("Failed to read body")?;
let rpc_req: RpcRequest =
serde_json::from_slice(&body_bytes).context("Invalid RPC request")?;
debug!("RPC method: {}", rpc_req.method);
// Enforce authentication for non-allowlisted methods
let is_unauthenticated = UNAUTHENTICATED_METHODS.contains(&rpc_req.method.as_str());
let mut new_session_cookies: Option<(String, String)> = None;
if !is_unauthenticated {
let mut authenticated = match &session_token {
Some(token) => self.session_store.validate(token).await,
None => false,
};
// If session invalid, try remember-me token to auto-restore session
if !authenticated {
if let Some(remember) = extract_cookie(&parts.headers, "remember") {
if crate::session::SessionStore::validate_remember_token(&remember).await {
let new_token = self.session_store.create().await;
let new_csrf = derive_csrf_token(&new_token).await;
tracing::info!("Auto-restored session from remember-me token");
new_session_cookies = Some((new_token, new_csrf));
authenticated = true;
}
}
}
if !authenticated {
let reason = if session_token.is_none() {
"no session cookie"
} else {
"invalid/expired token"
};
tracing::warn!(method = %rpc_req.method, reason, "401 Unauthorized — rejecting RPC call");
return Ok(self.error_response(401, "Unauthorized", StatusCode::UNAUTHORIZED));
}
}
// RBAC: check if the user's role allows this method
if !is_unauthenticated {
if let Ok(Some(user)) = self.auth_manager.get_user().await {
if !user.role.can_access(&rpc_req.method) {
return Ok(self.error_response(
403,
"Forbidden: insufficient permissions",
StatusCode::FORBIDDEN,
));
}
}
}
// CSRF protection: validate X-CSRF-Token header via HMAC derivation from session token.
// Skip CSRF for read-only methods (polling, status) — CSRF prevents state-changing forgery.
// Skip when session was just auto-restored from remember-me (browser has stale CSRF cookie).
let csrf_exempt = matches!(
rpc_req.method.as_str(),
"node-messages-received"
| "server.echo"
| "server.get-state"
| "system.stats"
| "tor.status"
| "tor.onion-addresses"
| "bitcoin.relay-status"
| "federation.list-nodes"
| "system.get-settings"
| "system.get-node-key"
| "system.get-metrics"
| "system.get-version"
);
if !is_unauthenticated && new_session_cookies.is_none() && !csrf_exempt {
let csrf_header = parts
.headers
.get("x-csrf-token")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let csrf_valid = match (&session_token, &csrf_header) {
(Some(token), Some(header)) => {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let secret = SessionStore::load_or_create_remember_secret().await;
let mut mac = match HmacSha256::new_from_slice(&secret) {
Ok(m) => m,
Err(_) => {
return Ok(json_response(StatusCode::INTERNAL_SERVER_ERROR, b"{}"));
}
};
mac.update(format!("csrf:{}", token).as_bytes());
match hex::decode(header) {
Ok(header_bytes) => mac.verify_slice(&header_bytes).is_ok(),
Err(_) => false,
}
}
_ => false,
};
if !csrf_valid {
// Debug: log expected vs received for diagnosis
if let (Some(token), Some(header)) = (&session_token, &csrf_header) {
let expected = derive_csrf_token(token).await;
tracing::warn!(
method = %rpc_req.method,
session_prefix = %&token[..8.min(token.len())],
csrf_prefix = %&header[..8.min(header.len())],
expected_prefix = %&expected[..8.min(expected.len())],
"403 CSRF mismatch — session/csrf/expected prefixes shown"
);
} else {
tracing::warn!(
method = %rpc_req.method,
has_session = session_token.is_some(),
has_header = csrf_header.is_some(),
"403 CSRF validation failed — rejecting RPC call"
);
}
return Ok(self.error_response(
403,
"CSRF token missing or invalid",
StatusCode::FORBIDDEN,
));
}
}
// Rate limit login attempts
if rpc_req.method == "auth.login" {
let client_ip = extract_client_ip(&parts);
if !self.login_rate_limiter.check(client_ip).await {
return Ok(self.rate_limit_response());
}
}
// Rate limit sensitive endpoints
{
let client_ip = extract_client_ip(&parts);
if !self
.endpoint_rate_limiter
.check(&rpc_req.method, client_ip)
.await
{
return Ok(self.rate_limit_response());
}
self.endpoint_rate_limiter
.record(&rpc_req.method, client_ip)
.await;
}
// Extract params; clone for post-routing use (login 2FA check needs password)
let params = rpc_req.params;
let login_params: Option<serde_json::Value> = if rpc_req.method == "auth.login" {
params.clone()
} else {
None
};
// Check cache for cacheable methods
let is_cacheable = CACHEABLE_METHODS.contains(&rpc_req.method.as_str());
if is_cacheable {
if let Some(cached) = self.response_cache.get(&rpc_req.method).await {
let rpc_resp = RpcResponse {
result: Some(cached),
error: None,
};
let body = serde_json::to_vec(&rpc_resp)?;
return Ok(json_response(StatusCode::OK, &body));
}
}
// Route to handler (track latency for metrics)
let rpc_start = std::time::Instant::now();
let result = Self::dispatch(&self, &rpc_req.method, params, &session_token).await;
// Record RPC latency for monitoring
let elapsed_ms = rpc_start.elapsed().as_secs_f64() * 1000.0;
self.metrics_store.record_rpc_latency(elapsed_ms).await;
// Build response (cache successful results for cacheable methods)
let mut rpc_resp = match result {
Ok(data) => {
if is_cacheable {
self.response_cache
.set(rpc_req.method.clone(), data.clone())
.await;
}
RpcResponse {
result: Some(data),
error: None,
}
}
Err(e) => {
// `{:#}` renders the whole anyhow context chain. Logging only the
// outermost context threw away the actual cause: a peer-files
// failure logged just "Failed to connect to peer", with the real
// error (Tor SOCKS failure, FIPS resolve, timeout) discarded — so
// the logs couldn't distinguish a dead peer from a slow circuit.
// The client-facing message below stays `{}` so internals aren't leaked.
error!("RPC error on {}: {:#}", rpc_req.method, e);
let user_message = sanitize_error_message(&e.to_string());
RpcResponse {
result: None,
error: Some(RpcError {
code: -1,
message: user_message,
data: None,
}),
}
}
};
let resp_body = serde_json::to_vec(&rpc_resp).context("Failed to serialize response")?;
let mut response = json_response(StatusCode::OK, &resp_body);
// Post-dispatch: set cookies for auth-related methods
let client_ip = extract_client_ip(&parts);
self.apply_auth_cookies(
&rpc_req.method,
&mut rpc_resp,
&mut response,
&session_token,
&login_params,
&new_session_cookies,
client_ip,
secure_suffix,
)
.await;
Ok(response)
}
/// Build a JSON error response with the given RPC error code and HTTP status.
fn error_response(
&self,
code: i32,
message: &str,
status: StatusCode,
) -> Response<hyper::Body> {
let rpc_resp = RpcResponse {
result: None,
error: Some(RpcError {
code,
message: message.to_string(),
data: None,
}),
};
let resp_body = serde_json::to_vec(&rpc_resp).unwrap_or_default();
json_response(status, &resp_body)
}
/// Build a 429 Too Many Requests response.
fn rate_limit_response(&self) -> Response<hyper::Body> {
let rpc_resp = RpcResponse {
result: None,
error: Some(RpcError {
code: 429,
message: "Rate limit exceeded. Try again later.".to_string(),
data: None,
}),
};
let resp_body = serde_json::to_vec(&rpc_resp).unwrap_or_default();
let mut resp = json_response(StatusCode::TOO_MANY_REQUESTS, &resp_body);
resp.headers_mut()
.insert("Retry-After", cookie_header("60"));
resp
}
/// Apply session/CSRF/remember-me cookies after dispatch for auth-related methods.
async fn apply_auth_cookies(
&self,
method: &str,
rpc_resp: &mut RpcResponse,
response: &mut Response<hyper::Body>,
session_token: &Option<String>,
login_params: &Option<serde_json::Value>,
new_session_cookies: &Option<(String, String)>,
client_ip: std::net::IpAddr,
secure_suffix: &str,
) {
// Track failed login attempts for rate limiting
if method == "auth.login" && rpc_resp.error.is_some() {
self.login_rate_limiter.record_failure(client_ip).await;
}
// On successful login, check if 2FA is required. Device-token logins
// (companion pairing QR) skip the TOTP challenge like remember-me does:
// the token was minted from an already-authenticated session, and there
// is no password with which to decrypt the TOTP secret anyway.
if method == "auth.login" && rpc_resp.error.is_none() {
let password = login_params
.as_ref()
.and_then(|p| p.get("password"))
.and_then(|v| v.as_str());
let is_token_login = login_params
.as_ref()
.and_then(|p| p.get("token"))
.and_then(|v| v.as_str())
.is_some()
|| match password {
// Companion device tokens also arrive through the password
// field (see handle_auth_login) — those logins get a full
// session too; there's no password to decrypt TOTP with.
Some(pw) => crate::device_tokens::verify(&self.config.data_dir, pw)
.await
.is_some(),
None => false,
};
let totp_enabled =
!is_token_login && self.auth_manager.is_totp_enabled().await.unwrap_or(false);
if totp_enabled {
let password = login_params
.as_ref()
.and_then(|p| p.get("password"))
.and_then(|v| v.as_str())
.unwrap_or("");
if let Ok(Some(totp_data)) = self.auth_manager.get_totp_data().await {
if let Ok(secret) = crate::totp::decrypt_secret(&totp_data, password) {
let token = self.session_store.create_pending(secret).await;
let csrf_token = derive_csrf_token(&token).await;
self.set_session_cookie(response, &token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
let totp_body = serde_json::json!({
"result": { "requires_totp": true },
"error": null
});
*response.body_mut() =
hyper::Body::from(serde_json::to_vec(&totp_body).unwrap_or_default());
}
}
} else {
let token = self.session_store.create().await;
let csrf_token = derive_csrf_token(&token).await;
let remember_token = self.session_store.create_remember_token().await;
self.set_session_cookie(response, &token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
self.set_remember_cookie(response, &remember_token, secure_suffix);
}
}
// On successful TOTP verification, set the rotated session cookie
if (method == "auth.login.totp" || method == "auth.login.backup")
&& rpc_resp.error.is_none()
{
let new_token_opt = rpc_resp
.result
.as_ref()
.and_then(|r| r.get("new_session_token"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(new_token) = new_token_opt {
let csrf_token = derive_csrf_token(&new_token).await;
let remember_token = self.session_store.create_remember_token().await;
self.set_session_cookie(response, &new_token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
self.set_remember_cookie(response, &remember_token, secure_suffix);
// Strip the token from the response body
if let Some(result) = rpc_resp.result.as_mut() {
if let Some(obj) = result.as_object_mut() {
obj.remove("new_session_token");
}
}
let body_bytes = serde_json::to_vec(&rpc_resp).unwrap_or_default();
*response.body_mut() = hyper::Body::from(body_bytes);
}
}
// On password change, rotate the session token for the caller
if method == "auth.changePassword" && rpc_resp.error.is_none() {
if let Some(token) = session_token {
let new_token = self.session_store.rotate(token).await;
let csrf_token = derive_csrf_token(&new_token).await;
self.set_session_cookie(response, &new_token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
}
}
// On logout, invalidate session and expire cookies
if method == "auth.logout" {
if let Some(token) = session_token {
self.session_store.remove(token).await;
}
response.headers_mut().append(
"Set-Cookie",
cookie_header(&format!(
"session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0{}",
secure_suffix
)),
);
response.headers_mut().append(
"Set-Cookie",
cookie_header(&format!(
"csrf_token=; SameSite=Lax; Path=/; Max-Age=0{}",
secure_suffix
)),
);
}
// If session was auto-restored from remember-me, set new cookies
if let Some((new_session, new_csrf)) = new_session_cookies {
self.set_session_cookie(response, new_session, secure_suffix);
self.set_csrf_cookie(response, new_csrf, secure_suffix);
}
}
fn set_session_cookie(
&self,
response: &mut Response<hyper::Body>,
token: &str,
secure_suffix: &str,
) {
response.headers_mut().append(
"Set-Cookie",
cookie_header(&format!(
"session={}; HttpOnly; SameSite=Lax; Path=/{}",
token, secure_suffix
)),
);
}
fn set_csrf_cookie(
&self,
response: &mut Response<hyper::Body>,
csrf_token: &str,
secure_suffix: &str,
) {
response.headers_mut().append(
"Set-Cookie",
cookie_header(&format!(
"csrf_token={}; SameSite=Lax; Path=/{}",
csrf_token, secure_suffix
)),
);
}
fn set_remember_cookie(
&self,
response: &mut Response<hyper::Body>,
remember_token: &str,
secure_suffix: &str,
) {
response.headers_mut().append(
"Set-Cookie",
cookie_header(&format!(
"remember={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}",
remember_token, REMEMBER_TTL, secure_suffix
)),
);
}
}