1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2025-01-23 22:55:48 +00:00

Really fix auth handling for duplicate cookies

This commit is contained in:
Felix Ableitner 2025-01-14 14:48:25 +01:00
parent 202e0cc441
commit 301d7945d7
6 changed files with 103 additions and 98 deletions

View file

@ -13,31 +13,25 @@ use crate::{
search_article,
},
instance::{follow_instance, get_instance, resolve_instance},
user::{get_user, login_user, logout_user, register_user, validate},
user::{get_user, login_user, logout_user, register_user},
},
database::IbisData,
error::MyResult,
},
common::{DbEdit, EditView, GetEditList, LocalUserView, SiteView, AUTH_COOKIE},
common::{DbEdit, EditView, GetEditList, LocalUserView, SiteView},
};
use activitypub_federation::config::Data;
use anyhow::anyhow;
use article::{approve_article, delete_conflict};
use axum::{
body::Body,
extract::Query,
http::{Request, StatusCode},
middleware::{self, Next},
response::Response,
routing::{delete, get, post},
Extension,
Json,
Router,
};
use axum_macros::debug_handler;
use http::header::COOKIE;
use instance::list_remote_instances;
use std::collections::HashSet;
use user::{count_notifications, list_notifications, update_user_profile};
pub mod article;
@ -70,40 +64,6 @@ pub fn api_routes() -> Router<()> {
.route("/account/logout", post(logout_user))
.route("/account/update", post(update_user_profile))
.route("/site", get(site_view))
.route_layer(middleware::from_fn(auth))
}
async fn auth(
data: Data<IbisData>,
mut request: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
// Check all duplicate auth headers and cookies for the first valid one.
// We need to extract cookies manually because CookieJar ignores duplicates.
let cookies = request
.headers()
.get(COOKIE)
.and_then(|h| h.to_str().ok())
.unwrap_or_default()
.split(';')
.flat_map(|s| s.split_once('='))
.filter(|s| s.0 == AUTH_COOKIE)
.map(|s| s.1);
let headers = request
.headers()
.get_all(AUTH_COOKIE)
.into_iter()
.filter_map(|h| h.to_str().ok());
let auth: HashSet<_> = headers.chain(cookies).map(|s| s.to_string()).collect();
for a in &auth {
if let Ok(user) = validate(a, &data).await {
request.extensions_mut().insert(user);
break;
}
}
let response = next.run(request).await;
Ok(response)
}
fn check_is_admin(user: &LocalUserView) -> MyResult<()> {

View file

@ -64,7 +64,7 @@ fn generate_login_token(person: &DbPerson, data: &Data<IbisData>) -> MyResult<St
Ok(jwt)
}
pub async fn validate(jwt: &str, data: &Data<IbisData>) -> MyResult<LocalUserView> {
pub async fn validate(jwt: &str, data: &IbisData) -> MyResult<LocalUserView> {
let validation = Validation::default();
let secret = read_jwt_secret(data)?;
let key = DecodingKey::from_secret(secret.as_bytes());

View file

@ -159,7 +159,7 @@ impl DbPerson {
Ok(())
}
pub fn read_local_from_name(username: &str, data: &Data<IbisData>) -> MyResult<LocalUserView> {
pub fn read_local_from_name(username: &str, data: &IbisData) -> MyResult<LocalUserView> {
let mut conn = data.db_pool.get()?;
let (person, local_user) = person::table
.inner_join(local_user::table)
@ -175,7 +175,7 @@ impl DbPerson {
})
}
fn read_following(id_: PersonId, data: &Data<IbisData>) -> MyResult<Vec<DbInstance>> {
fn read_following(id_: PersonId, data: &IbisData) -> MyResult<Vec<DbInstance>> {
use instance_follow::dsl::{follower_id, instance_id};
let mut conn = data.db_pool.get()?;
Ok(instance_follow::table

78
src/backend/middleware.rs Normal file
View file

@ -0,0 +1,78 @@
use super::api::user::validate;
use crate::{
backend::database::IbisData,
common::{Auth, AUTH_COOKIE},
};
use axum::{body::Body, extract::State, http::Request, middleware::Next, response::Response};
use axum_macros::debug_middleware;
use http::{header::COOKIE, HeaderValue};
use std::{collections::HashSet, sync::Arc};
pub(super) const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes";
/// Checks all headers and cookies (including duplicates) for first valid auth token.
/// We need to extract cookies manually because CookieJar ignores duplicates.
/// If user is authenticated sets extensions `Auth` and `LocalUserView`.
#[debug_middleware]
pub(super) async fn auth_middleware(
State(data): State<Arc<IbisData>>,
mut request: Request<Body>,
next: Next,
) -> Response {
let headers = request.headers();
let cookies = headers
.get(COOKIE)
.and_then(|h| h.to_str().ok())
.unwrap_or_default()
.split(';')
.flat_map(|s| s.split_once('='))
.filter(|s| s.0.trim() == AUTH_COOKIE)
.map(|s| s.1);
let headers = headers
.get_all(AUTH_COOKIE)
.into_iter()
.filter_map(|h| h.to_str().ok());
let auth: HashSet<_> = headers.chain(cookies).map(|s| s.to_string()).collect();
for auth in auth {
if let Ok(local_user) = validate(&auth, &data).await {
request.extensions_mut().insert(Auth(Some(auth)));
request.extensions_mut().insert(local_user);
}
}
next.run(request).await
}
/// Rewrite federation routes to use `FEDERATION_ROUTES_PREFIX`, to avoid conflicts
/// with frontend routes. If a request is an Activitypub fetch as indicated by
/// `Accept: application/activity+json` header, use the federation routes. Otherwise
/// leave the path unchanged so it can go to frontend.
#[debug_middleware]
pub(super) async fn federation_routes_middleware(request: Request<Body>, next: Next) -> Response {
let (mut parts, body) = request.into_parts();
// rewrite uri based on accept header
let mut uri = parts.uri.to_string();
const VALUE1: HeaderValue = HeaderValue::from_static("application/activity+json");
const VALUE2: HeaderValue = HeaderValue::from_static(
r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#,
);
let accept = parts.headers.get("Accept");
let content_type = parts.headers.get("Content-Type");
if Some(&VALUE1) == accept
|| Some(&VALUE2) == accept
|| Some(&VALUE1) == content_type
|| Some(&VALUE2) == content_type
{
uri = format!("{FEDERATION_ROUTES_PREFIX}{uri}");
}
// drop trailing slash
if uri.ends_with('/') && uri.len() > 1 {
uri.pop();
}
parts.uri = uri
.parse()
.expect("can parse uri after dropping trailing slash");
let request = Request::from_parts(parts, body);
next.run(request).await
}

View file

@ -13,7 +13,6 @@ use crate::{
DbInstance,
DbPerson,
EditVersion,
AUTH_COOKIE,
MAIN_PAGE_NAME,
},
frontend::app::{shell, App},
@ -27,15 +26,14 @@ use assets::file_and_error_handler;
use axum::{
body::Body,
extract::State,
http::{HeaderValue, Request},
middleware::Next,
http::Request,
middleware::from_fn_with_state,
response::{IntoResponse, Response},
routing::get,
Extension,
Router,
ServiceExt,
};
use axum_extra::extract::CookieJar;
use axum_macros::debug_middleware;
use chrono::Utc;
use diesel::{
r2d2::{ConnectionManager, Pool},
@ -49,7 +47,8 @@ use federation::objects::{
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use log::info;
use std::{net::SocketAddr, thread};
use middleware::{auth_middleware, federation_routes_middleware, FEDERATION_ROUTES_PREFIX};
use std::{net::SocketAddr, sync::Arc, thread};
use tokio::{net::TcpListener, sync::oneshot};
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
use tower_layer::Layer;
@ -61,14 +60,13 @@ pub mod config;
pub mod database;
pub mod error;
pub mod federation;
mod middleware;
mod nodeinfo;
mod scheduled_tasks;
mod utils;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes";
pub async fn start(
config: IbisConfig,
override_hostname: Option<SocketAddr>,
@ -83,11 +81,11 @@ pub async fn start(
.get()?
.run_pending_migrations(MIGRATIONS)
.expect("run migrations");
let data = IbisData { db_pool, config };
let ibis_data = IbisData { db_pool, config };
let data = FederationConfig::builder()
.domain(data.config.federation.domain.clone())
.url_verifier(Box::new(VerifyUrlData(data.config.clone())))
.app_data(data)
.domain(ibis_data.config.federation.domain.clone())
.url_verifier(Box::new(VerifyUrlData(ibis_data.config.clone())))
.app_data(ibis_data.clone())
.http_fetch_limit(1000)
.debug(cfg!(debug_assertions))
.build()
@ -111,6 +109,7 @@ pub async fn start(
let routes = generate_route_list(App);
let config = data.clone();
let arc_data = Arc::new(ibis_data);
let app = Router::new()
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
@ -120,7 +119,8 @@ pub async fn start(
.nest("", nodeinfo::config())
.layer(FederationMiddleware::new(config))
.layer(CorsLayer::permissive())
.layer(CompressionLayer::new());
.layer(CompressionLayer::new())
.route_layer(from_fn_with_state(arc_data.clone(), auth_middleware));
// Rewrite federation routes
// https://docs.rs/axum/0.7.4/axum/middleware/index.html#rewriting-request-uri-in-middleware
@ -139,19 +139,20 @@ pub async fn start(
/// Make auth token available in hydrate mode
async fn leptos_routes_handler(
jar: CookieJar,
auth: Option<Extension<Auth>>,
State(leptos_options): State<LeptosOptions>,
req: Request<Body>,
request: Request<Body>,
) -> Response {
let handler = leptos_axum::render_app_async_with_context(
move || {
let cookie = jar.get(AUTH_COOKIE).map(|c| c.value().to_string());
provide_context(Auth(cookie));
if let Some(auth) = &auth {
provide_context(auth.0.clone());
}
},
move || shell(leptos_options.clone()),
);
handler(req).await.into_response()
handler(request).await.into_response()
}
const MAIN_PAGE_DEFAULT_TEXT: &str = "Welcome to Ibis, the federated Wikipedia alternative!
@ -215,37 +216,3 @@ async fn setup(data: &Data<IbisData>) -> Result<(), Error> {
Ok(())
}
/// Rewrite federation routes to use `FEDERATION_ROUTES_PREFIX`, to avoid conflicts
/// with frontend routes. If a request is an Activitypub fetch as indicated by
/// `Accept: application/activity+json` header, use the federation routes. Otherwise
/// leave the path unchanged so it can go to frontend.
#[debug_middleware]
async fn federation_routes_middleware(request: Request<Body>, next: Next) -> Response {
let (mut parts, body) = request.into_parts();
// rewrite uri based on accept header
let mut uri = parts.uri.to_string();
const VALUE1: HeaderValue = HeaderValue::from_static("application/activity+json");
const VALUE2: HeaderValue = HeaderValue::from_static(
r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#,
);
let accept = parts.headers.get("Accept");
let content_type = parts.headers.get("Content-Type");
if Some(&VALUE1) == accept
|| Some(&VALUE2) == accept
|| Some(&VALUE1) == content_type
|| Some(&VALUE2) == content_type
{
uri = format!("{FEDERATION_ROUTES_PREFIX}{uri}");
}
// drop trailing slash
if uri.ends_with('/') && uri.len() > 1 {
uri.pop();
}
parts.uri = uri
.parse()
.expect("can parse uri after dropping trailing slash");
let request = Request::from_parts(parts, body);
next.run(request).await
}

View file

@ -25,7 +25,7 @@ pub const MAIN_PAGE_NAME: &str = "Main_Page";
pub static AUTH_COOKIE: &str = "auth";
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Auth(pub Option<String>);
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66