use super::{AppState, SESSION_COOKIE, response::*}; use std::sync::Arc; use axum::body::Body; use axum::extract::State; use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE, COOKIE, SET_COOKIE}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use jiff::Timestamp; use crate::atproto::AtprotoLogin; use crate::database::DatabaseError; use crate::flows::BrowserSession; use crate::identity::normalize_login; use crate::oidc::AuthorizationRequest; pub fn flow_cookie_name(state: &str) -> String { format!("__Host-ratlogin-{state}") } pub(super) fn session_cookie(value: &str, max_age: i64) -> String { format!("{SESSION_COOKIE}={value}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age={max_age}") } pub(super) fn cookie_value(headers: &HeaderMap, name: &str) -> Option { headers .get_all(COOKIE) .iter() .filter_map(|value| value.to_str().ok()) .flat_map(|header| header.split(';')) .filter_map(|pair| pair.trim().split_once('=')) .find(|(key, _)| *key == name) .map(|(_, value)| value.to_owned()) } pub(super) fn flow_cookie(state: &str, value: &str, max_age: i64) -> String { format!( "{}={value}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age={max_age}", flow_cookie_name(state) ) } pub(super) async fn remembered( state: &AppState, headers: &HeaderMap, now: Timestamp, ) -> Result, DatabaseError> { if state.config.session_days == 0 { return Ok(None); } let Some(value) = cookie_value(headers, SESSION_COOKIE) else { return Ok(None); }; let Some(session) = state.flows.session(&value, now)? else { return Ok(None); }; match state.login.resolve(&session.handle).await { Ok(did) if did == session.did => Ok(Some(session)), Ok(did) => { tracing::info!( handle = session.handle.as_str(), did = session.did.as_str(), resolved = did.as_str(), "remembered sign-in revoked: the handle belongs to another account" ); state.flows.revoke_session(&value, now)?; Ok(None) } Err(error) => { tracing::debug!( handle = session.handle.as_str(), ?error, "remembered sign-in not verified, asking the PDS instead" ); Ok(None) } } } pub(super) fn session_satisfies( session: &BrowserSession, request: &AuthorizationRequest, now: Timestamp, ) -> bool { if request.prompt.login { return false; } let fresh = request.max_age.is_none_or(|max_age| { now.duration_since(session.auth_time).as_secs() <= i64::try_from(max_age).unwrap_or(i64::MAX) }); let hinted = request .login_hint .as_deref() .is_none_or(|hint| normalize_login(hint).is_ok_and(|handle| handle == session.handle)); fresh && hinted } pub(super) async fn forget_session( State(state): State>>, headers: HeaderMap, ) -> Response { if let Err(error) = require_same_origin(&state, &headers) { return error.page(state.config.extra_css.as_deref()); } if let Some(value) = cookie_value(&headers, SESSION_COOKIE) && let Err(error) = state.flows.revoke_session(&value, Timestamp::now()) { return HttpError::from(error).page(state.config.extra_css.as_deref()); } respond( StatusCode::OK, vec![ (CONTENT_TYPE, "text/plain; charset=utf-8".to_owned()), (CACHE_CONTROL, "no-store".to_owned()), (SET_COOKIE, session_cookie("", 0)), ], Body::from("This device is no longer remembered."), ) }