mirror of
https://github.com/Nutomic/ibis.git
synced 2025-01-24 06:45: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,
|
search_article,
|
||||||
},
|
},
|
||||||
instance::{follow_instance, get_instance, resolve_instance},
|
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,
|
database::IbisData,
|
||||||
error::MyResult,
|
error::MyResult,
|
||||||
},
|
},
|
||||||
common::{DbEdit, EditView, GetEditList, LocalUserView, SiteView, AUTH_COOKIE},
|
common::{DbEdit, EditView, GetEditList, LocalUserView, SiteView},
|
||||||
};
|
};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use article::{approve_article, delete_conflict};
|
use article::{approve_article, delete_conflict};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
|
||||||
extract::Query,
|
extract::Query,
|
||||||
http::{Request, StatusCode},
|
|
||||||
middleware::{self, Next},
|
|
||||||
response::Response,
|
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Extension,
|
Extension,
|
||||||
Json,
|
Json,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
use http::header::COOKIE;
|
|
||||||
use instance::list_remote_instances;
|
use instance::list_remote_instances;
|
||||||
use std::collections::HashSet;
|
|
||||||
use user::{count_notifications, list_notifications, update_user_profile};
|
use user::{count_notifications, list_notifications, update_user_profile};
|
||||||
|
|
||||||
pub mod article;
|
pub mod article;
|
||||||
|
@ -70,40 +64,6 @@ pub fn api_routes() -> Router<()> {
|
||||||
.route("/account/logout", post(logout_user))
|
.route("/account/logout", post(logout_user))
|
||||||
.route("/account/update", post(update_user_profile))
|
.route("/account/update", post(update_user_profile))
|
||||||
.route("/site", get(site_view))
|
.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<()> {
|
fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
|
||||||
|
|
|
@ -64,7 +64,7 @@ fn generate_login_token(person: &DbPerson, data: &Data<IbisData>) -> MyResult<St
|
||||||
Ok(jwt)
|
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 validation = Validation::default();
|
||||||
let secret = read_jwt_secret(data)?;
|
let secret = read_jwt_secret(data)?;
|
||||||
let key = DecodingKey::from_secret(secret.as_bytes());
|
let key = DecodingKey::from_secret(secret.as_bytes());
|
||||||
|
|
|
@ -159,7 +159,7 @@ impl DbPerson {
|
||||||
Ok(())
|
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 mut conn = data.db_pool.get()?;
|
||||||
let (person, local_user) = person::table
|
let (person, local_user) = person::table
|
||||||
.inner_join(local_user::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};
|
use instance_follow::dsl::{follower_id, instance_id};
|
||||||
let mut conn = data.db_pool.get()?;
|
let mut conn = data.db_pool.get()?;
|
||||||
Ok(instance_follow::table
|
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,
|
DbInstance,
|
||||||
DbPerson,
|
DbPerson,
|
||||||
EditVersion,
|
EditVersion,
|
||||||
AUTH_COOKIE,
|
|
||||||
MAIN_PAGE_NAME,
|
MAIN_PAGE_NAME,
|
||||||
},
|
},
|
||||||
frontend::app::{shell, App},
|
frontend::app::{shell, App},
|
||||||
|
@ -27,15 +26,14 @@ use assets::file_and_error_handler;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{HeaderValue, Request},
|
http::Request,
|
||||||
middleware::Next,
|
middleware::from_fn_with_state,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
|
Extension,
|
||||||
Router,
|
Router,
|
||||||
ServiceExt,
|
ServiceExt,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
|
||||||
use axum_macros::debug_middleware;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
r2d2::{ConnectionManager, Pool},
|
r2d2::{ConnectionManager, Pool},
|
||||||
|
@ -49,7 +47,8 @@ use federation::objects::{
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use log::info;
|
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 tokio::{net::TcpListener, sync::oneshot};
|
||||||
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
|
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
|
||||||
use tower_layer::Layer;
|
use tower_layer::Layer;
|
||||||
|
@ -61,14 +60,13 @@ pub mod config;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod federation;
|
pub mod federation;
|
||||||
|
mod middleware;
|
||||||
mod nodeinfo;
|
mod nodeinfo;
|
||||||
mod scheduled_tasks;
|
mod scheduled_tasks;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||||
|
|
||||||
const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes";
|
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
config: IbisConfig,
|
config: IbisConfig,
|
||||||
override_hostname: Option<SocketAddr>,
|
override_hostname: Option<SocketAddr>,
|
||||||
|
@ -83,11 +81,11 @@ pub async fn start(
|
||||||
.get()?
|
.get()?
|
||||||
.run_pending_migrations(MIGRATIONS)
|
.run_pending_migrations(MIGRATIONS)
|
||||||
.expect("run migrations");
|
.expect("run migrations");
|
||||||
let data = IbisData { db_pool, config };
|
let ibis_data = IbisData { db_pool, config };
|
||||||
let data = FederationConfig::builder()
|
let data = FederationConfig::builder()
|
||||||
.domain(data.config.federation.domain.clone())
|
.domain(ibis_data.config.federation.domain.clone())
|
||||||
.url_verifier(Box::new(VerifyUrlData(data.config.clone())))
|
.url_verifier(Box::new(VerifyUrlData(ibis_data.config.clone())))
|
||||||
.app_data(data)
|
.app_data(ibis_data.clone())
|
||||||
.http_fetch_limit(1000)
|
.http_fetch_limit(1000)
|
||||||
.debug(cfg!(debug_assertions))
|
.debug(cfg!(debug_assertions))
|
||||||
.build()
|
.build()
|
||||||
|
@ -111,6 +109,7 @@ pub async fn start(
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
let config = data.clone();
|
let config = data.clone();
|
||||||
|
let arc_data = Arc::new(ibis_data);
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||||
.fallback(file_and_error_handler)
|
.fallback(file_and_error_handler)
|
||||||
|
@ -120,7 +119,8 @@ pub async fn start(
|
||||||
.nest("", nodeinfo::config())
|
.nest("", nodeinfo::config())
|
||||||
.layer(FederationMiddleware::new(config))
|
.layer(FederationMiddleware::new(config))
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.layer(CompressionLayer::new());
|
.layer(CompressionLayer::new())
|
||||||
|
.route_layer(from_fn_with_state(arc_data.clone(), auth_middleware));
|
||||||
|
|
||||||
// Rewrite federation routes
|
// Rewrite federation routes
|
||||||
// https://docs.rs/axum/0.7.4/axum/middleware/index.html#rewriting-request-uri-in-middleware
|
// 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
|
/// Make auth token available in hydrate mode
|
||||||
async fn leptos_routes_handler(
|
async fn leptos_routes_handler(
|
||||||
jar: CookieJar,
|
auth: Option<Extension<Auth>>,
|
||||||
State(leptos_options): State<LeptosOptions>,
|
State(leptos_options): State<LeptosOptions>,
|
||||||
req: Request<Body>,
|
request: Request<Body>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let handler = leptos_axum::render_app_async_with_context(
|
let handler = leptos_axum::render_app_async_with_context(
|
||||||
move || {
|
move || {
|
||||||
let cookie = jar.get(AUTH_COOKIE).map(|c| c.value().to_string());
|
if let Some(auth) = &auth {
|
||||||
provide_context(Auth(cookie));
|
provide_context(auth.0.clone());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
move || shell(leptos_options.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!
|
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(())
|
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";
|
pub static AUTH_COOKIE: &str = "auth";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Auth(pub Option<String>);
|
pub struct Auth(pub Option<String>);
|
||||||
|
|
||||||
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
|
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
|
||||||
|
|
Loading…
Reference in a new issue