Removed deprecated authentication methods

This commit is contained in:
Antoine Gersant 2022-11-08 02:01:20 -08:00
parent d41e837561
commit 63e971059a
8 changed files with 42 additions and 450 deletions

16
Cargo.lock generated
View file

@ -578,13 +578,7 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
dependencies = [
"base64",
"hkdf",
"hmac",
"percent-encoding",
"rand",
"sha2",
"subtle",
"time 0.3.14",
"version_check",
]
@ -981,15 +975,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
@ -1524,7 +1509,6 @@ dependencies = [
"ape",
"base64",
"branca",
"cookie 0.16.0",
"crossbeam-channel",
"daemonize",
"diesel",

View file

@ -19,7 +19,6 @@ anyhow = "1.0.56"
ape = "0.4.0"
base64 = "0.13"
branca = "0.10.1"
cookie = { version = "0.16", features = ["signed", "key-expansion"] }
crossbeam-channel = "0.5"
diesel_migrations = { version = "2.0", features = ["sqlite"] }
futures-util = { version = "0.3" }

View file

@ -93,10 +93,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -125,10 +123,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -155,10 +151,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
},
@ -186,10 +180,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -215,10 +207,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
},
@ -245,10 +235,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -277,10 +265,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -309,10 +295,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -351,10 +335,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
},
@ -381,10 +363,8 @@
},
"security": [
{
"admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": []
"admin_query_parameter": []
}
]
}
@ -410,10 +390,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
},
@ -440,10 +418,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -453,7 +429,7 @@
"tags": [
"Users"
],
"summary": "Signs in a user. Response has Set-Cookie headers for the session, username and admin permission of the user.",
"summary": "Signs in a user.",
"operationId": "postAuth",
"requestBody": {
"required": true,
@ -506,10 +482,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -548,10 +522,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -580,10 +552,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -622,10 +592,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -654,10 +622,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -686,10 +652,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -728,10 +692,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -767,10 +729,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -825,10 +785,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -857,10 +815,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -899,10 +855,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
},
@ -939,10 +893,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
},
@ -969,10 +921,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -1001,10 +951,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -1033,10 +981,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -1062,10 +1008,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -1134,10 +1078,8 @@
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
"auth_query_parameter": []
}
]
}
@ -1490,28 +1432,6 @@
"in": "query",
"name": "auth_token",
"description": "Identical to the auth_query_parameter scheme but only for users recognized as admin by the Polaris server"
},
"auth_http_basic": {
"type": "http",
"scheme": "basic",
"description": "[deprecated]"
},
"admin_http_basic": {
"type": "http",
"scheme": "basic",
"description": "[deprecated] Identical to the auth_http_basic scheme but only for users recognized as admin by the Polaris server"
},
"auth_cookie": {
"type": "apikey",
"in": "cookie",
"name": "session",
"description": "[deprecated] A token obtained via the SET-COOKIE header in a response to a request via the auth_http_basic scheme, or a request to the `auth` endpoint."
},
"admin_cookie": {
"type": "apikey",
"in": "cookie",
"name": "session",
"description": "[deprecated] Identical to the auth_cookie scheme but only for users recognized as admin by the Polaris server"
}
},
"links": {},

View file

@ -1,9 +1,9 @@
use actix_files::NamedFile;
use actix_web::body::{BoxBody, MessageBody};
use actix_web::body::BoxBody;
use actix_web::http::header::ContentEncoding;
use actix_web::{
delete,
dev::{Payload, Service, ServiceRequest, ServiceResponse},
dev::Payload,
error::{ErrorForbidden, ErrorInternalServerError, ErrorUnauthorized},
get,
http::StatusCode,
@ -11,12 +11,10 @@ use actix_web::{
web::{self, Data, Json, JsonConfig, ServiceConfig},
FromRequest, HttpRequest, HttpResponse, Responder, ResponseError,
};
use actix_web_httpauth::extractors::{basic::BasicAuth, bearer::BearerAuth};
use cookie::{self, *};
use futures_util::future::{err, ok};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use futures_util::future::err;
use percent_encoding::percent_decode_str;
use std::future::Future;
use std::ops::Deref;
use std::path::Path;
use std::pin::Pin;
use std::str;
@ -75,6 +73,7 @@ pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
impl ResponseError for APIError {
fn status_code(&self) -> StatusCode {
match self {
APIError::AuthenticationRequired => StatusCode::UNAUTHORIZED,
APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED,
APIError::EmptyUsername => StatusCode::BAD_REQUEST,
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
@ -93,76 +92,9 @@ impl ResponseError for APIError {
}
}
#[derive(Clone)]
struct Cookies {
jar: CookieJar,
key: Key,
}
impl Cookies {
fn new(key: Key) -> Self {
let jar = CookieJar::new();
Self { jar, key }
}
fn add_original(&mut self, cookie: Cookie<'static>) {
self.jar.add_original(cookie);
}
fn add(&mut self, cookie: Cookie<'static>) {
self.jar.add(cookie);
}
fn add_signed(&mut self, cookie: Cookie<'static>) {
self.jar.signed_mut(&self.key).add(cookie);
}
#[allow(dead_code)]
fn get(&self, name: &str) -> Option<&Cookie> {
self.jar.get(name)
}
fn get_signed(&mut self, name: &str) -> Option<Cookie> {
self.jar.signed(&self.key).get(name)
}
}
impl FromRequest for Cookies {
type Error = actix_web::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let request_cookies = match request.cookies() {
Ok(c) => c,
Err(_) => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))),
};
let key = match request.app_data::<Data<Key>>() {
Some(k) => k.as_ref(),
None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))),
};
let mut cookies = Cookies::new(key.clone());
for cookie in request_cookies.deref() {
cookies.add_original(cookie.clone());
}
Box::pin(ok(cookies))
}
}
#[derive(Debug)]
enum AuthSource {
AuthorizationBasic,
AuthorizationBearer,
Cookie,
QueryParameter,
}
#[derive(Debug)]
struct Auth {
username: String,
source: AuthSource,
}
impl FromRequest for Auth {
@ -175,29 +107,11 @@ impl FromRequest for Auth {
None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))),
};
let cookies_future = Cookies::from_request(request, payload);
let basic_auth_future = BasicAuth::from_request(request, payload);
let bearer_auth_future = BearerAuth::from_request(request, payload);
let query_params_future =
web::Query::<dto::AuthQueryParameters>::from_request(request, payload);
Box::pin(async move {
// Auth via session cookie
{
let mut cookies = cookies_future.await?;
if let Some(session_cookie) = cookies.get_signed(dto::COOKIE_SESSION) {
let username = session_cookie.value().to_string();
let exists = block(move || user_manager.exists(&username)).await?;
if !exists {
return Err(ErrorUnauthorized(APIError::Unspecified));
}
return Ok(Auth {
username: session_cookie.value().to_string(),
source: AuthSource::Cookie,
});
}
}
// Auth via bearer token in query parameter
if let Ok(query) = query_params_future.await {
let auth_token = user::AuthToken(query.auth_token.clone());
@ -207,7 +121,6 @@ impl FromRequest for Auth {
.await?;
return Ok(Auth {
username: authorization.username,
source: AuthSource::QueryParameter,
});
}
@ -220,29 +133,10 @@ impl FromRequest for Auth {
.await?;
return Ok(Auth {
username: authorization.username,
source: AuthSource::AuthorizationBearer,
});
}
// Auth via basic authorization header
{
let basic_auth = basic_auth_future.await?;
let username = basic_auth.user_id().to_string();
let password = basic_auth
.password()
.map(|s| s.as_ref())
.unwrap_or("")
.to_string();
let auth_result = block(move || user_manager.login(&username, &password)).await;
if auth_result.is_ok() {
Ok(Auth {
username: basic_auth.user_id().to_string(),
source: AuthSource::AuthorizationBasic,
})
} else {
Err(ErrorUnauthorized(APIError::Unspecified))
}
}
Err(ErrorUnauthorized(APIError::AuthenticationRequired))
})
}
}
@ -285,93 +179,6 @@ impl FromRequest for AdminRights {
}
}
pub fn http_auth_middleware<
B: MessageBody + 'static,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
>(
request: ServiceRequest,
service: &S,
) -> Pin<Box<dyn Future<Output = Result<ServiceResponse<B>, actix_web::Error>>>> {
let user_manager = match request.app_data::<Data<user::Manager>>() {
Some(m) => m.clone(),
None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))),
};
let (request, mut payload) = request.into_parts();
let auth_future = Auth::from_request(&request, &mut payload);
let cookies_future = Cookies::from_request(&request, &mut payload);
let request = ServiceRequest::from_parts(request, payload);
let response_future = service.call(request);
Box::pin(async move {
let mut response = response_future.await?;
if let Ok(auth) = auth_future.await {
let set_cookies = match auth.source {
AuthSource::AuthorizationBasic => true,
AuthSource::AuthorizationBearer => false,
AuthSource::Cookie => false,
AuthSource::QueryParameter => false,
};
if set_cookies {
let cookies = cookies_future.await?;
let username = auth.username.clone();
let is_admin = block(move || {
user_manager
.is_admin(&auth.username)
.map_err(|_| APIError::Unspecified)
})
.await?;
add_auth_cookies(response.response_mut(), &cookies, &username, is_admin)?;
}
}
Ok(response)
})
}
fn add_auth_cookies<T>(
response: &mut HttpResponse<T>,
cookies: &Cookies,
username: &str,
is_admin: bool,
) -> Result<(), http::Error> {
let mut cookies = cookies.clone();
cookies.add_signed(
Cookie::build(dto::COOKIE_SESSION, username.to_owned())
.same_site(cookie::SameSite::Lax)
.http_only(true)
.permanent()
.finish(),
);
cookies.add(
Cookie::build(dto::COOKIE_USERNAME, username.to_owned())
.same_site(cookie::SameSite::Lax)
.http_only(false)
.permanent()
.path("/")
.finish(),
);
cookies.add(
Cookie::build(dto::COOKIE_ADMIN, format!("{}", is_admin))
.same_site(cookie::SameSite::Lax)
.http_only(false)
.permanent()
.path("/")
.finish(),
);
let headers = response.headers_mut();
for cookie in cookies.jar.delta() {
http::HeaderValue::from_str(&cookie.to_string()).map(|c| {
headers.append(http::header::SET_COOKIE, c);
})?;
}
Ok(())
}
struct MediaFile {
named_file: NamedFile,
}
@ -476,11 +283,7 @@ async fn put_mount_dirs(
vfs_manager: Data<vfs::Manager>,
new_mount_dirs: Json<Vec<dto::MountDir>>,
) -> Result<HttpResponse, APIError> {
let new_mount_dirs: Vec<MountDir> = new_mount_dirs
.iter()
.cloned()
.map(|m| m.into())
.collect();
let new_mount_dirs: Vec<MountDir> = new_mount_dirs.iter().cloned().map(|m| m.into()).collect();
block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?;
Ok(HttpResponse::new(StatusCode::OK))
}
@ -598,7 +401,6 @@ async fn trigger_index(
async fn login(
user_manager: Data<user::Manager>,
credentials: Json<dto::Credentials>,
cookies: Cookies,
) -> Result<HttpResponse, APIError> {
let username = credentials.username.clone();
let (user::AuthToken(token), is_admin) =
@ -613,9 +415,7 @@ async fn login(
token,
is_admin,
};
let mut response = HttpResponse::Ok().json(authorization);
add_auth_cookies(&mut response, &cookies, &username, is_admin)
.map_err(|_| APIError::Unspecified)?;
let response = HttpResponse::Ok().json(authorization);
Ok(response)
}

View file

@ -15,7 +15,6 @@ pub mod test;
pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
move |cfg: &mut ServiceConfig| {
let encryption_key = cookie::Key::derive_from(&app.auth_secret.key[..]);
cfg.app_data(web::Data::new(app.index))
.app_data(web::Data::new(app.config_manager))
.app_data(web::Data::new(app.ddns_manager))
@ -25,11 +24,9 @@ pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
.app_data(web::Data::new(app.thumbnail_manager))
.app_data(web::Data::new(app.user_manager))
.app_data(web::Data::new(app.vfs_manager))
.app_data(web::Data::new(encryption_key))
.service(
web::scope("/api")
.configure(api::make_config())
.wrap_fn(api::http_auth_middleware)
.wrap(NormalizePath::trim()),
)
.service(
@ -60,7 +57,7 @@ pub fn run(app: App) -> anyhow::Result<()> {
error!("Error starting HTTP server: {:?}", e);
e
})?
.run()
.run(),
)?;
Ok(())
}

View file

@ -5,9 +5,6 @@ use std::convert::From;
pub const API_MAJOR_VERSION: i32 = 6;
pub const API_MINOR_VERSION: i32 = 1;
pub const COOKIE_SESSION: &str = "session";
pub const COOKIE_USERNAME: &str = "username";
pub const COOKIE_ADMIN: &str = "admin";
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Version {

View file

@ -5,6 +5,8 @@ use crate::app::{config, playlist, settings, user};
#[derive(Error, Debug)]
pub enum APIError {
#[error("Authentication is required")]
AuthenticationRequired,
#[error("Incorrect Credentials")]
IncorrectCredentials,
#[error("EmptyUsername")]

View file

@ -1,57 +1,10 @@
use std::time::Duration;
use cookie::Cookie;
use headers::{self, HeaderMapExt};
use http::{Response, StatusCode};
use http::StatusCode;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
fn validate_added_cookies<T>(response: &Response<T>) {
let twenty_years = Duration::from_secs(20 * 365 * 24 * 60 * 60);
let cookies: Vec<Cookie> = response
.headers()
.get_all(http::header::SET_COOKIE)
.iter()
.map(|c| Cookie::parse(c.to_str().unwrap()).unwrap())
.collect();
let session = cookies
.iter()
.find(|c| c.name() == dto::COOKIE_SESSION)
.unwrap();
assert_ne!(session.value(), TEST_USERNAME);
assert!(session.max_age().unwrap() >= twenty_years);
let username = cookies
.iter()
.find(|c| c.name() == dto::COOKIE_USERNAME)
.unwrap();
assert_eq!(username.value(), TEST_USERNAME);
assert!(session.max_age().unwrap() >= twenty_years);
let is_admin = cookies
.iter()
.find(|c| c.name() == dto::COOKIE_ADMIN)
.unwrap();
assert_eq!(is_admin.value(), false.to_string());
assert!(session.max_age().unwrap() >= twenty_years);
}
fn validate_no_cookies<T>(response: &Response<T>) {
let cookies: Vec<Cookie> = response
.headers()
.get_all(http::header::SET_COOKIE)
.iter()
.map(|c| Cookie::parse(c.to_str().unwrap()).unwrap())
.collect();
assert!(!cookies.iter().any(|c| c.name() == dto::COOKIE_SESSION));
assert!(!cookies.iter().any(|c| c.name() == dto::COOKIE_USERNAME));
assert!(!cookies.iter().any(|c| c.name() == dto::COOKIE_ADMIN));
}
#[test]
fn login_rejects_bad_username() {
let mut service = ServiceType::new(&test_name!());
@ -85,62 +38,6 @@ fn login_golden_path() {
assert_eq!(authorization.username, TEST_USERNAME);
assert!(!authorization.is_admin);
assert!(!authorization.token.is_empty());
validate_added_cookies(&response);
}
#[test]
fn requests_without_auth_header_do_not_set_cookies() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login();
let request = protocol::random();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_no_cookies(&response);
}
#[test]
fn authentication_via_basic_http_header_rejects_bad_username() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let mut request = protocol::random();
let basic = headers::Authorization::basic("garbage", TEST_PASSWORD);
request.headers_mut().typed_insert(basic);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn authentication_via_basic_http_header_rejects_bad_password() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let mut request = protocol::random();
let basic = headers::Authorization::basic(TEST_PASSWORD, "garbage");
request.headers_mut().typed_insert(basic);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn authentication_via_basic_http_header_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let mut request = protocol::random();
let basic = headers::Authorization::basic(TEST_USERNAME, TEST_PASSWORD);
request.headers_mut().typed_insert(basic);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_added_cookies(&response);
}
#[test]
@ -175,8 +72,6 @@ fn authentication_via_bearer_http_header_golden_path() {
request.headers_mut().typed_insert(bearer);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_no_cookies(&response);
}
#[test]
@ -214,6 +109,4 @@ fn authentication_via_query_param_golden_path() {
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_no_cookies(&response);
}