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:
parent
202e0cc441
commit
301d7945d7
6 changed files with 103 additions and 98 deletions
|
@ -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<()> {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
78
src/backend/middleware.rs
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue