Allow passing auth via header or cookie (#3725)
* Allow passing auth via header or cookie * revert submodule * taplo * fix build * working * convert apub api methods * also set cache-control header * opt * clippy * deduplicate code, ignore invalid auth * clippy --------- Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
parent
7fd14b3d2a
commit
b2aee565f3
13 changed files with 184 additions and 20 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2738,10 +2738,12 @@ dependencies = [
|
|||
name = "lemmy_db_views"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"diesel",
|
||||
"diesel-async",
|
||||
"diesel_ltree",
|
||||
"lemmy_db_schema",
|
||||
"lemmy_utils",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"serial_test",
|
||||
|
|
|
@ -159,6 +159,16 @@ pub async fn local_user_view_from_jwt_opt(
|
|||
) -> Option<LocalUserView> {
|
||||
local_user_view_from_jwt(jwt?, context).await.ok()
|
||||
}
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn local_user_view_from_jwt_opt_new(
|
||||
local_user_view: &mut Option<LocalUserView>,
|
||||
jwt: Option<&Sensitive<String>>,
|
||||
context: &LemmyContext,
|
||||
) {
|
||||
if local_user_view.is_none() {
|
||||
*local_user_view = local_user_view_from_jwt_opt(jwt, context).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if user's token was issued before user's password reset.
|
||||
pub fn check_validator_time(
|
||||
|
|
|
@ -8,21 +8,22 @@ use actix_web::web::{Json, Query};
|
|||
use lemmy_api_common::{
|
||||
comment::{GetComments, GetCommentsResponse},
|
||||
context::LemmyContext,
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt_new},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{comment::Comment, community::Community, local_site::LocalSite},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::comment_view::CommentQuery;
|
||||
use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_comments(
|
||||
data: Query<GetComments>,
|
||||
context: Data<LemmyContext>,
|
||||
mut local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<GetCommentsResponse>, LemmyError> {
|
||||
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
|
||||
local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await;
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
||||
|
|
|
@ -8,18 +8,19 @@ use actix_web::web::{Json, Query};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{GetPosts, GetPostsResponse},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt_new},
|
||||
};
|
||||
use lemmy_db_schema::source::{community::Community, local_site::LocalSite};
|
||||
use lemmy_db_views::post_view::PostQuery;
|
||||
use lemmy_db_views::{post_view::PostQuery, structs::LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_posts(
|
||||
data: Query<GetPosts>,
|
||||
context: Data<LemmyContext>,
|
||||
mut local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<GetPostsResponse>, LemmyError> {
|
||||
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
|
||||
local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await;
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
|
|
@ -4,7 +4,7 @@ use actix_web::web::{Json, Query};
|
|||
use lemmy_api_common::{
|
||||
community::{GetCommunity, GetCommunityResponse},
|
||||
context::LemmyContext,
|
||||
utils::{check_private_instance, is_mod_or_admin_opt, local_user_view_from_jwt_opt},
|
||||
utils::{check_private_instance, is_mod_or_admin_opt, local_user_view_from_jwt_opt_new},
|
||||
};
|
||||
use lemmy_db_schema::source::{
|
||||
actor_language::CommunityLanguage,
|
||||
|
@ -12,6 +12,7 @@ use lemmy_db_schema::source::{
|
|||
local_site::LocalSite,
|
||||
site::Site,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType};
|
||||
|
||||
|
@ -19,8 +20,9 @@ use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorTy
|
|||
pub async fn get_community(
|
||||
data: Query<GetCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
mut local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<GetCommunityResponse>, LemmyError> {
|
||||
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
|
||||
local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await;
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
if data.name.is_none() && data.id.is_none() {
|
||||
|
|
|
@ -4,13 +4,13 @@ use actix_web::web::{Json, Query};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{GetPersonDetails, GetPersonDetailsResponse},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt_new},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{local_site::LocalSite, person::Person},
|
||||
utils::post_to_comment_sort_type,
|
||||
};
|
||||
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
|
||||
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery, structs::LocalUserView};
|
||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
|
||||
|
||||
|
@ -18,13 +18,14 @@ use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
|
|||
pub async fn read_person(
|
||||
data: Query<GetPersonDetails>,
|
||||
context: Data<LemmyContext>,
|
||||
mut local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<GetPersonDetailsResponse>, LemmyError> {
|
||||
// Check to make sure a person name or an id is given
|
||||
if data.username.is_none() && data.person_id.is_none() {
|
||||
return Err(LemmyErrorType::NoIdGiven)?;
|
||||
}
|
||||
|
||||
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
|
||||
local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await;
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
|
|
@ -9,10 +9,10 @@ use diesel::NotFound;
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
site::{ResolveObject, ResolveObjectResponse},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt},
|
||||
utils::{check_private_instance, local_user_view_from_jwt_opt_new},
|
||||
};
|
||||
use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool};
|
||||
use lemmy_db_views::structs::{CommentView, PostView};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView, PostView};
|
||||
use lemmy_db_views_actor::structs::{CommunityView, PersonView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
|
||||
|
||||
|
@ -20,8 +20,9 @@ use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
|
|||
pub async fn resolve_object(
|
||||
data: Query<ResolveObject>,
|
||||
context: Data<LemmyContext>,
|
||||
mut local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<ResolveObjectResponse>, LemmyError> {
|
||||
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
|
||||
local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await;
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
let person_id = local_user_view.map(|v| v.person.id);
|
||||
|
|
|
@ -4,14 +4,14 @@ use actix_web::web::{Json, Query};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
site::{Search, SearchResponse},
|
||||
utils::{check_private_instance, is_admin, local_user_view_from_jwt_opt},
|
||||
utils::{check_private_instance, is_admin, local_user_view_from_jwt_opt_new},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{community::Community, local_site::LocalSite},
|
||||
utils::{post_to_comment_sort_type, post_to_person_sort_type},
|
||||
SearchType,
|
||||
};
|
||||
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
|
||||
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery, structs::LocalUserView};
|
||||
use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
|
||||
|
@ -19,8 +19,9 @@ use lemmy_utils::error::LemmyError;
|
|||
pub async fn search(
|
||||
data: Query<Search>,
|
||||
context: Data<LemmyContext>,
|
||||
mut local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<SearchResponse>, LemmyError> {
|
||||
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
|
||||
local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await;
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
|
|
@ -13,16 +13,18 @@ doctest = false
|
|||
|
||||
[features]
|
||||
full = [
|
||||
"lemmy_db_schema/full",
|
||||
"lemmy_utils",
|
||||
"diesel",
|
||||
"diesel-async",
|
||||
"diesel_ltree",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"actix-web",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
lemmy_db_schema = { workspace = true }
|
||||
lemmy_utils = { workspace = true, optional = true }
|
||||
diesel = { workspace = true, optional = true }
|
||||
diesel-async = { workspace = true, optional = true }
|
||||
diesel_ltree = { workspace = true, optional = true }
|
||||
|
@ -30,6 +32,7 @@ serde = { workspace = true }
|
|||
serde_with = { workspace = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
actix-web = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = { workspace = true }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::structs::LocalUserView;
|
||||
use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
|
||||
use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema::{
|
||||
|
@ -9,6 +10,8 @@ use lemmy_db_schema::{
|
|||
traits::JoinView,
|
||||
utils::{functions::lower, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||
};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||
use std::future::{ready, Ready};
|
||||
|
||||
type LocalUserViewTuple = (LocalUser, Person, PersonAggregates);
|
||||
|
||||
|
@ -116,3 +119,15 @@ impl JoinView for LocalUserView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for LocalUserView {
|
||||
type Error = LemmyError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
ready(match req.extensions().get::<LocalUserView>() {
|
||||
Some(c) => Ok(c.clone()),
|
||||
None => Err(LemmyErrorType::IncorrectLogin.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,6 +208,7 @@ pub enum LemmyErrorType {
|
|||
InvalidUrlScheme,
|
||||
CouldntSendWebmention,
|
||||
ContradictingFilters,
|
||||
AuthCookieInsecure,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -4,10 +4,15 @@ pub mod code_migrations;
|
|||
pub mod prometheus_metrics;
|
||||
pub mod root_span_builder;
|
||||
pub mod scheduled_tasks;
|
||||
pub mod session_middleware;
|
||||
#[cfg(feature = "console")]
|
||||
pub mod telemetry;
|
||||
|
||||
use crate::{code_migrations::run_advanced_migrations, root_span_builder::QuieterRootSpanBuilder};
|
||||
use crate::{
|
||||
code_migrations::run_advanced_migrations,
|
||||
root_span_builder::QuieterRootSpanBuilder,
|
||||
session_middleware::SessionMiddleware,
|
||||
};
|
||||
use activitypub_federation::config::{FederationConfig, FederationMiddleware};
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{
|
||||
|
@ -204,7 +209,8 @@ pub async fn start_lemmy_server() -> Result<(), LemmyError> {
|
|||
.wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
|
||||
.app_data(Data::new(context.clone()))
|
||||
.app_data(Data::new(rate_limit_cell.clone()))
|
||||
.wrap(FederationMiddleware::new(federation_config.clone()));
|
||||
.wrap(FederationMiddleware::new(federation_config.clone()))
|
||||
.wrap(SessionMiddleware::new(context.clone()));
|
||||
|
||||
#[cfg(feature = "prometheus-metrics")]
|
||||
let app = app.wrap(prom_api_metrics.clone());
|
||||
|
|
120
src/session_middleware.rs
Normal file
120
src/session_middleware.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use actix_web::{
|
||||
body::MessageBody,
|
||||
cookie::SameSite,
|
||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
http::header::CACHE_CONTROL,
|
||||
Error,
|
||||
HttpMessage,
|
||||
};
|
||||
use core::future::Ready;
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use lemmy_api_common::{context::LemmyContext, utils::local_user_view_from_jwt};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||
use reqwest::header::HeaderValue;
|
||||
use std::{future::ready, rc::Rc};
|
||||
|
||||
static AUTH_COOKIE_NAME: &str = "auth";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionMiddleware {
|
||||
context: LemmyContext,
|
||||
}
|
||||
|
||||
impl SessionMiddleware {
|
||||
pub fn new(context: LemmyContext) -> Self {
|
||||
SessionMiddleware { context }
|
||||
}
|
||||
}
|
||||
impl<S, B> Transform<S, ServiceRequest> for SessionMiddleware
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Transform = SessionService<S>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(SessionService {
|
||||
service: Rc::new(service),
|
||||
context: self.context.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionService<S> {
|
||||
service: Rc<S>,
|
||||
context: LemmyContext,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for SessionService<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let svc = self.service.clone();
|
||||
let context = self.context.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
// Try reading jwt from auth header
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get(AUTH_COOKIE_NAME)
|
||||
.and_then(|h| h.to_str().ok());
|
||||
let jwt = if let Some(a) = auth_header {
|
||||
Some(a.to_string())
|
||||
}
|
||||
// If that fails, try auth cookie. Dont use the `jwt` cookie from lemmy-ui because
|
||||
// its not http-only.
|
||||
else {
|
||||
let auth_cookie = req.cookie(AUTH_COOKIE_NAME);
|
||||
if let Some(a) = &auth_cookie {
|
||||
// ensure that its marked as httponly and secure
|
||||
let secure = a.secure().unwrap_or_default();
|
||||
let http_only = a.http_only().unwrap_or_default();
|
||||
let same_site = a.same_site();
|
||||
if !secure || !http_only || same_site != Some(SameSite::Strict) {
|
||||
return Err(LemmyError::from(LemmyErrorType::AuthCookieInsecure).into());
|
||||
}
|
||||
}
|
||||
auth_cookie.map(|c| c.value().to_string())
|
||||
};
|
||||
|
||||
if let Some(jwt) = &jwt {
|
||||
// Ignore any invalid auth so the site can still be used
|
||||
// TODO: this means it will be impossible to get any error message for invalid jwt. Need
|
||||
// to add a separate endpoint for that.
|
||||
// https://github.com/LemmyNet/lemmy/issues/3702
|
||||
let local_user_view = local_user_view_from_jwt(jwt, &context).await.ok();
|
||||
if let Some(local_user_view) = local_user_view {
|
||||
req.extensions_mut().insert(local_user_view);
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = svc.call(req).await?;
|
||||
|
||||
// Add cache-control header. If user is authenticated, mark as private. Otherwise cache
|
||||
// up to one minute.
|
||||
let cache_value = if jwt.is_some() {
|
||||
"private"
|
||||
} else {
|
||||
"public, max-age=60"
|
||||
};
|
||||
res
|
||||
.headers_mut()
|
||||
.insert(CACHE_CONTROL, HeaderValue::from_static(cache_value));
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue