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:
Nutomic 2023-08-29 16:47:57 +02:00 committed by GitHub
parent 7fd14b3d2a
commit b2aee565f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 20 deletions

2
Cargo.lock generated
View file

@ -2738,10 +2738,12 @@ dependencies = [
name = "lemmy_db_views" name = "lemmy_db_views"
version = "0.18.1" version = "0.18.1"
dependencies = [ dependencies = [
"actix-web",
"diesel", "diesel",
"diesel-async", "diesel-async",
"diesel_ltree", "diesel_ltree",
"lemmy_db_schema", "lemmy_db_schema",
"lemmy_utils",
"serde", "serde",
"serde_with", "serde_with",
"serial_test", "serial_test",

View file

@ -159,6 +159,16 @@ pub async fn local_user_view_from_jwt_opt(
) -> Option<LocalUserView> { ) -> Option<LocalUserView> {
local_user_view_from_jwt(jwt?, context).await.ok() 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. /// Checks if user's token was issued before user's password reset.
pub fn check_validator_time( pub fn check_validator_time(

View file

@ -8,21 +8,22 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
comment::{GetComments, GetCommentsResponse}, comment::{GetComments, GetCommentsResponse},
context::LemmyContext, 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::{ use lemmy_db_schema::{
source::{comment::Comment, community::Community, local_site::LocalSite}, source::{comment::Comment, community::Community, local_site::LocalSite},
traits::Crud, 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}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_comments( pub async fn list_comments(
data: Query<GetComments>, data: Query<GetComments>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
mut local_user_view: Option<LocalUserView>,
) -> Result<Json<GetCommentsResponse>, LemmyError> { ) -> 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?; let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;

View file

@ -8,18 +8,19 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{GetPosts, GetPostsResponse}, 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_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}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_posts( pub async fn list_posts(
data: Query<GetPosts>, data: Query<GetPosts>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
mut local_user_view: Option<LocalUserView>,
) -> Result<Json<GetPostsResponse>, LemmyError> { ) -> 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?; let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;

View file

@ -4,7 +4,7 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
community::{GetCommunity, GetCommunityResponse}, community::{GetCommunity, GetCommunityResponse},
context::LemmyContext, 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::{ use lemmy_db_schema::source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
@ -12,6 +12,7 @@ use lemmy_db_schema::source::{
local_site::LocalSite, local_site::LocalSite,
site::Site, site::Site,
}; };
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType}; 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( pub async fn get_community(
data: Query<GetCommunity>, data: Query<GetCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
mut local_user_view: Option<LocalUserView>,
) -> Result<Json<GetCommunityResponse>, LemmyError> { ) -> 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?; let local_site = LocalSite::read(&mut context.pool()).await?;
if data.name.is_none() && data.id.is_none() { if data.name.is_none() && data.id.is_none() {

View file

@ -4,13 +4,13 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{GetPersonDetails, GetPersonDetailsResponse}, 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::{ use lemmy_db_schema::{
source::{local_site::LocalSite, person::Person}, source::{local_site::LocalSite, person::Person},
utils::post_to_comment_sort_type, 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_db_views_actor::structs::{CommunityModeratorView, PersonView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
@ -18,13 +18,14 @@ use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
pub async fn read_person( pub async fn read_person(
data: Query<GetPersonDetails>, data: Query<GetPersonDetails>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
mut local_user_view: Option<LocalUserView>,
) -> Result<Json<GetPersonDetailsResponse>, LemmyError> { ) -> Result<Json<GetPersonDetailsResponse>, LemmyError> {
// Check to make sure a person name or an id is given // Check to make sure a person name or an id is given
if data.username.is_none() && data.person_id.is_none() { if data.username.is_none() && data.person_id.is_none() {
return Err(LemmyErrorType::NoIdGiven)?; 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?; let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;

View file

@ -9,10 +9,10 @@ use diesel::NotFound;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{ResolveObject, ResolveObjectResponse}, 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_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_db_views_actor::structs::{CommunityView, PersonView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
@ -20,8 +20,9 @@ use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
pub async fn resolve_object( pub async fn resolve_object(
data: Query<ResolveObject>, data: Query<ResolveObject>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
mut local_user_view: Option<LocalUserView>,
) -> Result<Json<ResolveObjectResponse>, LemmyError> { ) -> 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?; let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;
let person_id = local_user_view.map(|v| v.person.id); let person_id = local_user_view.map(|v| v.person.id);

View file

@ -4,14 +4,14 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{Search, SearchResponse}, 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::{ use lemmy_db_schema::{
source::{community::Community, local_site::LocalSite}, source::{community::Community, local_site::LocalSite},
utils::{post_to_comment_sort_type, post_to_person_sort_type}, utils::{post_to_comment_sort_type, post_to_person_sort_type},
SearchType, 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_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery};
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
@ -19,8 +19,9 @@ use lemmy_utils::error::LemmyError;
pub async fn search( pub async fn search(
data: Query<Search>, data: Query<Search>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
mut local_user_view: Option<LocalUserView>,
) -> Result<Json<SearchResponse>, LemmyError> { ) -> 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?; let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;

View file

@ -13,16 +13,18 @@ doctest = false
[features] [features]
full = [ full = [
"lemmy_db_schema/full", "lemmy_utils",
"diesel", "diesel",
"diesel-async", "diesel-async",
"diesel_ltree", "diesel_ltree",
"tracing", "tracing",
"ts-rs", "ts-rs",
"actix-web",
] ]
[dependencies] [dependencies]
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_utils = { workspace = true, optional = true }
diesel = { workspace = true, optional = true } diesel = { workspace = true, optional = true }
diesel-async = { workspace = true, optional = true } diesel-async = { workspace = true, optional = true }
diesel_ltree = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true }
@ -30,6 +32,7 @@ serde = { workspace = true }
serde_with = { workspace = true } serde_with = { workspace = true }
tracing = { workspace = true, optional = true } tracing = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true }
actix-web = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
serial_test = { workspace = true } serial_test = { workspace = true }

View file

@ -1,4 +1,5 @@
use crate::structs::LocalUserView; use crate::structs::LocalUserView;
use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -9,6 +10,8 @@ use lemmy_db_schema::{
traits::JoinView, traits::JoinView,
utils::{functions::lower, DbConn, DbPool, ListFn, Queries, ReadFn}, utils::{functions::lower, DbConn, DbPool, ListFn, Queries, ReadFn},
}; };
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use std::future::{ready, Ready};
type LocalUserViewTuple = (LocalUser, Person, PersonAggregates); 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()),
})
}
}

View file

@ -208,6 +208,7 @@ pub enum LemmyErrorType {
InvalidUrlScheme, InvalidUrlScheme,
CouldntSendWebmention, CouldntSendWebmention,
ContradictingFilters, ContradictingFilters,
AuthCookieInsecure,
Unknown(String), Unknown(String),
} }

View file

@ -4,10 +4,15 @@ pub mod code_migrations;
pub mod prometheus_metrics; pub mod prometheus_metrics;
pub mod root_span_builder; pub mod root_span_builder;
pub mod scheduled_tasks; pub mod scheduled_tasks;
pub mod session_middleware;
#[cfg(feature = "console")] #[cfg(feature = "console")]
pub mod telemetry; 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 activitypub_federation::config::{FederationConfig, FederationMiddleware};
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{ use actix_web::{
@ -204,7 +209,8 @@ pub async fn start_lemmy_server() -> Result<(), LemmyError> {
.wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors)) .wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
.app_data(Data::new(context.clone())) .app_data(Data::new(context.clone()))
.app_data(Data::new(rate_limit_cell.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")] #[cfg(feature = "prometheus-metrics")]
let app = app.wrap(prom_api_metrics.clone()); let app = app.wrap(prom_api_metrics.clone());

120
src/session_middleware.rs Normal file
View 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)
})
}
}