Something went wrong. Try again.
An OIDC provider for AT Protocol OAuth. Rust port of apenwarr/atlogin. forked from quilling.dev/ratlogin
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121use 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<String> { 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<L: AtprotoLogin>( state: &AppState<L>, headers: &HeaderMap, now: Timestamp,) -> Result<Option<BrowserSession>, 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<L>( State(state): State<Arc<AppState<L>>>, 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."), )}