Support for bearer token authentication (#120)

* User manager can create and recognize auth tokens

* Implement HTTP bearer auth

* Use bearer auth in test-harness

* Can receive auth token via query parameter (useful for media endpoints)
This commit is contained in:
Antoine Gersant 2020-12-20 03:25:45 -08:00 committed by GitHub
parent 5e065c5e6a
commit 72c8ed9289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 737 additions and 131 deletions

45
Cargo.lock generated
View file

@ -448,6 +448,17 @@ dependencies = [
"byte-tools", "byte-tools",
] ]
[[package]]
name = "branca"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e85a179a6f9595d798a16a97ce64312beb20cfe30ebc40f94271a5fda89f3f"
dependencies = [
"base-x",
"byteorder",
"orion",
]
[[package]] [[package]]
name = "brotli-sys" name = "brotli-sys"
version = "0.3.2" version = "0.3.2"
@ -1039,6 +1050,17 @@ dependencies = [
"wasi 0.9.0+wasi-snapshot-preview1", "wasi 0.9.0+wasi-snapshot-preview1",
] ]
[[package]]
name = "getrandom"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8025cf36f917e6a52cce185b7c7177689b838b7ec138364e50cc2277a56cf4"
dependencies = [
"cfg-if 0.1.10",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]] [[package]]
name = "gif" name = "gif"
version = "0.11.1" version = "0.11.1"
@ -1644,6 +1666,18 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbb993947f111397c2bc536944f8dac7f54a4e73383d478efe1990b56404b60" checksum = "afbb993947f111397c2bc536944f8dac7f54a4e73383d478efe1990b56404b60"
[[package]]
name = "orion"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1066cd107e316d26fe9f8657525afc01ed74b3288126ee7e58b5b141916aab09"
dependencies = [
"base64 0.13.0",
"getrandom 0.2.0",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.1" version = "0.11.1"
@ -1776,6 +1810,7 @@ dependencies = [
"anyhow", "anyhow",
"ape", "ape",
"base64 0.13.0", "base64 0.13.0",
"branca",
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
"diesel", "diesel",
@ -1902,7 +1937,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.1.15",
"libc", "libc",
"rand_chacha", "rand_chacha",
"rand_core", "rand_core",
@ -1925,7 +1960,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.1.15",
] ]
[[package]] [[package]]
@ -2904,3 +2939,9 @@ dependencies = [
"winapi 0.2.8", "winapi 0.2.8",
"winapi-build", "winapi-build",
] ]
[[package]]
name = "zeroize"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36"

View file

@ -16,6 +16,7 @@ actix-web-httpauth = { version = "0.5.0" }
anyhow = "1.0.35" anyhow = "1.0.35"
ape = "0.3.0" ape = "0.3.0"
base64 = "0.13" base64 = "0.13"
branca = "0.10.0"
cookie = { version = "0.14", features = ["signed", "key-expansion"] } cookie = { version = "0.14", features = ["signed", "key-expansion"] }
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
diesel_migrations = { version = "1.4", features = ["sqlite"] } diesel_migrations = { version = "1.4", features = ["sqlite"] }

View file

@ -93,7 +93,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -123,7 +125,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -151,7 +155,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -180,7 +186,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -207,7 +215,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -235,7 +245,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -265,7 +277,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -295,7 +309,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -335,7 +351,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -363,7 +381,9 @@
}, },
"security": [ "security": [
{ {
"admin_http_header": [], "admin_http_basic": [],
"admin_http_bearer": [],
"admin_query_parameter": [],
"admin_cookie": [] "admin_cookie": []
} }
] ]
@ -390,7 +410,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -418,7 +440,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -436,14 +460,21 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#components/schemas/AuthCredentials" "$ref": "#components/schemas/Credentials"
} }
} }
} }
}, },
"responses": { "responses": {
"200": { "200": {
"description": "Successful operation" "description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Authorization"
}
}
}
}, },
"401": { "401": {
"description": "Invalid credentials" "description": "Invalid credentials"
@ -475,7 +506,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -515,7 +548,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -545,7 +580,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -585,7 +622,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -615,7 +654,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -645,7 +686,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -685,7 +728,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -722,7 +767,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -768,7 +815,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -798,7 +847,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -838,7 +889,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -876,7 +929,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -904,7 +959,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -934,7 +991,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -964,7 +1023,38 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": []
}
]
}
},
"/lastfm/link_token": {
"get": {
"tags": [
"Last.fm"
],
"summary": "Obtain an authentication token to be used when linking a Polaris account to a Last.fm account. The token is only valid for 10 minutes.",
"operationId": "getLastFMLinkToken",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LastFMLinkToken"
}
}
}
}
},
"security": [
{
"auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -982,6 +1072,15 @@
}, },
"operationId": "getLastFMLink", "operationId": "getLastFMLink",
"parameters": [ "parameters": [
{
"name": "auth_token",
"in": "query",
"required": true,
"description": "Polaris authentication token received from the `lastfm/link_token` endpoint",
"schema": {
"type": "string"
}
},
{ {
"name": "token", "name": "token",
"in": "query", "in": "query",
@ -1025,7 +1124,9 @@
}, },
"security": [ "security": [
{ {
"auth_http_header": [], "auth_http_basic": [],
"auth_http_bearer": [],
"auth_query_parameter": [],
"auth_cookie": [] "auth_cookie": []
} }
] ]
@ -1183,7 +1284,7 @@
} }
} }
}, },
"AuthCredentials": { "Credentials": {
"type": "object", "type": "object",
"properties": { "properties": {
"username": { "username": {
@ -1194,6 +1295,28 @@
} }
} }
}, },
"Authorization": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"token": {
"type": "string"
},
"is_admin": {
"type": "bool"
}
}
},
"LastFMLinkToken": {
"type": "object",
"properties": {
"value": {
"type": "string"
}
}
},
"CollectionFile": { "CollectionFile": {
"oneOf": [ "oneOf": [
{ {
@ -1320,26 +1443,49 @@
} }
}, },
"securitySchemes": { "securitySchemes": {
"auth_http_header": { "auth_http_bearer": {
"type": "http", "type": "http",
"scheme": "basic" "scheme": "bearer",
"description": "An authentication token obtained in the output of the `auth` endpoint"
},
"admin_http_bearer": {
"type": "http",
"scheme": "bearer",
"description": "Identical to the auth_http_bearer scheme but only for users recognized as admin by the Polaris server"
},
"auth_query_parameter": {
"type": "apikey",
"in": "query",
"name": "auth_token",
"description": "An authentication token obtained in the output of the `auth` endpoint"
},
"admin_query_parameter": {
"type": "apikey",
"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": { "auth_cookie": {
"type": "apikey", "type": "apikey",
"in": "cookie", "in": "cookie",
"name": "session", "name": "session",
"description": "A session token obtained returned as a server cookie by making a request via the auth_http_header scheme." "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_http_header": {
"type": "http",
"scheme": "basic",
"description": "Identical to the auth_http_header scheme but only for users recognized as admin by the Polaris server"
}, },
"admin_cookie": { "admin_cookie": {
"type": "apikey", "type": "apikey",
"in": "cookie", "in": "cookie",
"name": "session", "name": "session",
"description": "Identical to the auth_cookie scheme but only for users recognized as admin by the Polaris server" "description": "[deprecated] Identical to the auth_cookie scheme but only for users recognized as admin by the Polaris server"
} }
}, },
"links": {}, "links": {},

View file

@ -24,7 +24,8 @@ fn get_test_db(name: &str) -> DB {
fn apply_saves_misc_settings() { fn apply_saves_misc_settings() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = user::Manager::new(db.clone(), auth_secret);
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = Manager::new( let config_manager = Manager::new(
@ -60,7 +61,8 @@ fn apply_saves_misc_settings() {
fn apply_saves_mount_points() { fn apply_saves_mount_points() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = user::Manager::new(db.clone(), auth_secret);
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = Manager::new( let config_manager = Manager::new(
@ -89,7 +91,8 @@ fn apply_saves_ddns_settings() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = user::Manager::new(db.clone(), auth_secret);
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = Manager::new( let config_manager = Manager::new(
@ -117,7 +120,8 @@ fn apply_saves_ddns_settings() {
fn apply_can_toggle_admin() { fn apply_can_toggle_admin() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = user::Manager::new(db.clone(), auth_secret);
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = Manager::new( let config_manager = Manager::new(

View file

@ -2,6 +2,7 @@ use anyhow::*;
use rustfm_scrobble::{Scrobble, Scrobbler}; use rustfm_scrobble::{Scrobble, Scrobbler};
use serde::Deserialize; use serde::Deserialize;
use std::path::Path; use std::path::Path;
use user::AuthToken;
use crate::app::{index::Index, user}; use crate::app::{index::Index, user};
@ -53,12 +54,19 @@ impl Manager {
} }
} }
pub fn link(&self, username: &str, token: &str) -> Result<()> { pub fn generate_link_token(&self, username: &str) -> Result<AuthToken> {
self.user_manager
.generate_lastfm_link_token(username)
.map_err(|e| e.into())
}
pub fn link(&self, username: &str, lastfm_token: &str) -> Result<()> {
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into());
let auth_response = scrobbler.authenticate_with_token(token)?; let auth_response = scrobbler.authenticate_with_token(lastfm_token)?;
self.user_manager self.user_manager
.lastfm_link(username, &auth_response.name, &auth_response.key) .lastfm_link(username, &auth_response.name, &auth_response.key)
.map_err(|e| e.into())
} }
pub fn unlink(&self, username: &str) -> Result<()> { pub fn unlink(&self, username: &str) -> Result<()> {

View file

@ -6,7 +6,7 @@ mod manager;
pub use error::*; pub use error::*;
pub use manager::*; pub use manager::*;
#[derive(Clone)] #[derive(Clone, Default)]
pub struct AuthSecret { pub struct AuthSecret {
pub key: [u8; 32], pub key: [u8; 32],
} }

View file

@ -6,6 +6,12 @@ pub enum Error {
EmptyPassword, EmptyPassword,
#[error("Username does not exist")] #[error("Username does not exist")]
IncorrectUsername, IncorrectUsername,
#[error("Password does not match username")]
IncorrectPassword,
#[error("Invalid auth token")]
InvalidAuthToken,
#[error("Incorrect authorization scope")]
IncorrectAuthorizationScope,
#[error("Unspecified")] #[error("Unspecified")]
Unspecified, Unspecified,
} }

View file

@ -1,20 +1,24 @@
use anyhow::anyhow; use anyhow::anyhow;
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use std::time::{SystemTime, UNIX_EPOCH};
use super::*; use super::*;
use crate::app::settings::AuthSecret;
use crate::db::DB; use crate::db::DB;
const HASH_ITERATIONS: u32 = 10000; const HASH_ITERATIONS: u32 = 10000;
#[derive(Clone)] #[derive(Clone)]
pub struct Manager { pub struct Manager {
// TODO make this private and move preferences methods in this file
pub db: DB, pub db: DB,
auth_secret: AuthSecret,
} }
impl Manager { impl Manager {
pub fn new(db: DB) -> Self { pub fn new(db: DB, auth_secret: AuthSecret) -> Self {
Self { db } Self { db, auth_secret }
} }
pub fn create(&self, new_user: &NewUser) -> Result<(), Error> { pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
@ -67,7 +71,7 @@ impl Manager {
Ok(()) Ok(())
} }
pub fn login(&self, username: &str, password: &str) -> Result<bool, Error> { pub fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
match users match users
@ -78,11 +82,68 @@ impl Manager {
Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername), Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
Ok(hash) => { Ok(hash) => {
let hash: String = hash; let hash: String = hash;
Ok(verify_password(&hash, password)) if verify_password(&hash, password) {
let authorization = Authorization {
username: username.to_owned(),
scope: AuthorizationScope::PolarisAuth,
};
self.generate_auth_token(&authorization)
} else {
Err(Error::IncorrectPassword)
}
} }
Err(_) => Err(Error::Unspecified), Err(_) => Err(Error::Unspecified),
} }
} }
pub fn authenticate(
&self,
auth_token: &AuthToken,
scope: AuthorizationScope,
) -> Result<Authorization, Error> {
let authorization = self.decode_auth_token(auth_token, scope)?;
if self.exists(&authorization.username)? {
Ok(authorization)
} else {
Err(Error::IncorrectUsername)
}
}
fn decode_auth_token(
&self,
auth_token: &AuthToken,
scope: AuthorizationScope,
) -> Result<Authorization, Error> {
let AuthToken(data) = auth_token;
let ttl = match scope {
AuthorizationScope::PolarisAuth => 0, // permanent
AuthorizationScope::LastFMLink => 10 * 60, // 10 minutes
};
let authorization = branca::decode(data, &self.auth_secret.key, ttl)
.map_err(|_| Error::InvalidAuthToken)?;
let authorization: Authorization =
serde_json::from_slice(&authorization[..]).map_err(|_| Error::InvalidAuthToken)?;
if authorization.scope != scope {
return Err(Error::IncorrectAuthorizationScope);
}
Ok(authorization)
}
fn generate_auth_token(&self, authorization: &Authorization) -> Result<AuthToken, Error> {
let serialized_authorization =
serde_json::to_string(&authorization).map_err(|_| Error::Unspecified)?;
branca::encode(
serialized_authorization.as_bytes(),
&self.auth_secret.key,
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::Unspecified)?
.as_secs() as u32,
)
.map_err(|_| Error::Unspecified)
.map(AuthToken)
}
pub fn count(&self) -> anyhow::Result<i64> { pub fn count(&self) -> anyhow::Result<i64> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
@ -110,13 +171,14 @@ impl Manager {
Ok(results.len() > 0) Ok(results.len() > 0)
} }
pub fn is_admin(&self, username: &str) -> anyhow::Result<bool> { pub fn is_admin(&self, username: &str) -> Result<bool, Error> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
let is_admin: i32 = users let is_admin: i32 = users
.filter(name.eq(username)) .filter(name.eq(username))
.select(admin) .select(admin)
.get_result(&connection)?; .get_result(&connection)
.map_err(|_| Error::Unspecified)?;
Ok(is_admin != 0) Ok(is_admin != 0)
} }
@ -125,7 +187,7 @@ impl Manager {
username: &str, username: &str,
lastfm_login: &str, lastfm_login: &str,
session_key: &str, session_key: &str,
) -> anyhow::Result<()> { ) -> Result<(), Error> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
diesel::update(users.filter(name.eq(username))) diesel::update(users.filter(name.eq(username)))
@ -133,10 +195,18 @@ impl Manager {
lastfm_username.eq(lastfm_login), lastfm_username.eq(lastfm_login),
lastfm_session_key.eq(session_key), lastfm_session_key.eq(session_key),
)) ))
.execute(&connection)?; .execute(&connection)
.map_err(|_| Error::Unspecified)?;
Ok(()) Ok(())
} }
pub fn generate_lastfm_link_token(&self, username: &str) -> Result<AuthToken, Error> {
self.generate_auth_token(&Authorization {
username: username.to_owned(),
scope: AuthorizationScope::LastFMLink,
})
}
pub fn get_lastfm_session_key(&self, username: &str) -> anyhow::Result<String> { pub fn get_lastfm_session_key(&self, username: &str) -> anyhow::Result<String> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;

View file

@ -1,4 +1,4 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::db::users; use crate::db::users;
@ -32,3 +32,18 @@ pub struct NewUser {
pub password: String, pub password: String,
pub admin: bool, pub admin: bool,
} }
#[derive(Debug)]
pub struct AuthToken(pub String);
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub enum AuthorizationScope {
PolarisAuth,
LastFMLink,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Authorization {
pub username: String,
pub scope: AuthorizationScope,
}

View file

@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::app::settings;
use crate::db::DB; use crate::db::DB;
use crate::test_name; use crate::test_name;
@ -19,7 +20,9 @@ pub fn get_test_db(name: &str) -> DB {
#[test] #[test]
fn create_delete_user_golden_path() { fn create_delete_user_golden_path() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let user_manager = Manager::new(db); let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let new_user = NewUser { let new_user = NewUser {
name: "Walter".to_owned(), name: "Walter".to_owned(),
@ -37,7 +40,9 @@ fn create_delete_user_golden_path() {
#[test] #[test]
fn cannot_create_user_with_blank_username() { fn cannot_create_user_with_blank_username() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let user_manager = Manager::new(db); let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let new_user = NewUser { let new_user = NewUser {
name: "".to_owned(), name: "".to_owned(),
@ -54,7 +59,9 @@ fn cannot_create_user_with_blank_username() {
#[test] #[test]
fn cannot_create_user_with_blank_password() { fn cannot_create_user_with_blank_password() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let user_manager = Manager::new(db); let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let new_user = NewUser { let new_user = NewUser {
name: "Walter".to_owned(), name: "Walter".to_owned(),
@ -71,7 +78,9 @@ fn cannot_create_user_with_blank_password() {
#[test] #[test]
fn cannot_create_duplicate_user() { fn cannot_create_duplicate_user() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let user_manager = Manager::new(db); let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let new_user = NewUser { let new_user = NewUser {
name: "Walter".to_owned(), name: "Walter".to_owned(),
@ -86,7 +95,9 @@ fn cannot_create_duplicate_user() {
#[test] #[test]
fn can_read_write_preferences() { fn can_read_write_preferences() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let user_manager = Manager::new(db); let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let new_preferences = Preferences { let new_preferences = Preferences {
web_theme_base: Some("very-dark-theme".to_owned()), web_theme_base: Some("very-dark-theme".to_owned()),
@ -108,3 +119,126 @@ fn can_read_write_preferences() {
let read_preferences = user_manager.read_preferences("Walter").unwrap(); let read_preferences = user_manager.read_preferences("Walter").unwrap();
assert_eq!(new_preferences, read_preferences); assert_eq!(new_preferences, read_preferences);
} }
#[test]
fn login_rejects_bad_password() {
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let username = "Walter";
let password = "super_secret!";
let new_user = NewUser {
name: username.to_owned(),
password: password.to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
assert_eq!(
user_manager
.login(username, "not the password")
.unwrap_err(),
Error::IncorrectPassword
)
}
#[test]
fn login_golden_path() {
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let username = "Walter";
let password = "super_secret!";
let new_user = NewUser {
name: username.to_owned(),
password: password.to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
assert!(user_manager.login(username, password).is_ok())
}
#[test]
fn authenticate_rejects_bad_token() {
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let username = "Walter";
let password = "super_secret!";
let new_user = NewUser {
name: username.to_owned(),
password: password.to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
let fake_token = AuthToken("fake token".to_owned());
assert!(user_manager
.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
.is_err())
}
#[test]
fn authenticate_golden_path() {
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let username = "Walter";
let password = "super_secret!";
let new_user = NewUser {
name: username.to_owned(),
password: password.to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
let token = user_manager.login(username, password).unwrap();
let authorization = user_manager
.authenticate(&token, AuthorizationScope::PolarisAuth)
.unwrap();
assert_eq!(
authorization,
Authorization {
username: username.to_owned(),
scope: AuthorizationScope::PolarisAuth,
}
)
}
#[test]
fn authenticate_validates_scope() {
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = Manager::new(db, auth_secret);
let username = "Walter";
let password = "super_secret!";
let new_user = NewUser {
name: username.to_owned(),
password: password.to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
let token = user_manager.generate_lastfm_link_token(username).unwrap();
let authorization = user_manager.authenticate(&token, AuthorizationScope::PolarisAuth);
assert_eq!(
authorization.unwrap_err(),
Error::IncorrectAuthorizationScope
)
}

View file

@ -102,7 +102,8 @@ pub fn get_test_db(name: &str) -> DB {
let db = DB::new(&db_path).unwrap(); let db = DB::new(&db_path).unwrap();
let settings_manager = settings::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let auth_secret = settings_manager.get_auth_secret().unwrap();
let user_manager = user::Manager::new(db.clone(), auth_secret);
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = let config_manager =

View file

@ -10,7 +10,7 @@ use actix_web::{
web::{self, Data, Json, JsonConfig, ServiceConfig}, web::{self, Data, Json, JsonConfig, ServiceConfig},
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
}; };
use actix_web_httpauth::extractors::basic::BasicAuth; use actix_web_httpauth::extractors::{basic::BasicAuth, bearer::BearerAuth};
use cookie::{self, *}; use cookie::{self, *};
use futures_util::future::{err, ok}; use futures_util::future::{err, ok};
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
@ -64,6 +64,7 @@ pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
.service(delete_playlist) .service(delete_playlist)
.service(lastfm_now_playing) .service(lastfm_now_playing)
.service(lastfm_scrobble) .service(lastfm_scrobble)
.service(lastfm_link_token)
.service(lastfm_link) .service(lastfm_link)
.service(lastfm_unlink); .service(lastfm_unlink);
} }
@ -151,8 +152,10 @@ impl FromRequest for Cookies {
#[derive(Debug)] #[derive(Debug)]
enum AuthSource { enum AuthSource {
AuthorizationHeader, AuthorizationBasic,
AuthorizationBearer,
Cookie, Cookie,
QueryParameter,
} }
#[derive(Debug)] #[derive(Debug)]
@ -173,7 +176,10 @@ impl FromRequest for Auth {
}; };
let cookies_future = Cookies::from_request(request, payload); let cookies_future = Cookies::from_request(request, payload);
let http_auth_future = BasicAuth::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 { Box::pin(async move {
// Auth via session cookie // Auth via session cookie
@ -192,20 +198,46 @@ impl FromRequest for Auth {
} }
} }
// Auth via HTTP header // Auth via bearer token in query parameter
if let Ok(query) = query_params_future.await {
let auth_token = user::AuthToken(query.auth_token.clone());
let authorization = block(move || {
user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
})
.await?;
return Ok(Auth {
username: authorization.username.to_owned(),
source: AuthSource::QueryParameter,
});
}
// Auth via bearer token in authorization header
if let Ok(bearer_auth) = bearer_auth_future.await {
let auth_token = user::AuthToken(bearer_auth.token().to_owned());
let authorization = block(move || {
user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
})
.await?;
return Ok(Auth {
username: authorization.username.to_owned(),
source: AuthSource::AuthorizationBearer,
});
}
// Auth via basic authorization header
{ {
let auth = http_auth_future.await?; let basic_auth = basic_auth_future.await?;
let username = auth.user_id().to_string(); let username = basic_auth.user_id().to_string();
let password = auth let password = basic_auth
.password() .password()
.map(|s| s.as_ref()) .map(|s| s.as_ref())
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
let auth_result = block(move || user_manager.login(&username, &password)).await?; let auth_result = block(move || user_manager.login(&username, &password)).await;
if auth_result { if auth_result.is_ok() {
Ok(Auth { Ok(Auth {
username: auth.user_id().to_string(), username: basic_auth.user_id().to_string(),
source: AuthSource::AuthorizationHeader, source: AuthSource::AuthorizationBasic,
}) })
} else { } else {
Err(ErrorUnauthorized(APIError::Unspecified)) Err(ErrorUnauthorized(APIError::Unspecified))
@ -280,8 +312,10 @@ pub fn http_auth_middleware<
let mut response = response_future.await?; let mut response = response_future.await?;
if let Ok(auth) = auth_future.await { if let Ok(auth) = auth_future.await {
let set_cookies = match auth.source { let set_cookies = match auth.source {
AuthSource::AuthorizationHeader => true, AuthSource::AuthorizationBasic => true,
AuthSource::AuthorizationBearer => false,
AuthSource::Cookie => false, AuthSource::Cookie => false,
AuthSource::QueryParameter => false,
}; };
if set_cookies { if set_cookies {
let cookies = cookies_future.await?; let cookies = cookies_future.await?;
@ -547,20 +581,23 @@ async fn trigger_index(
#[post("/auth")] #[post("/auth")]
async fn login( async fn login(
user_manager: Data<user::Manager>, user_manager: Data<user::Manager>,
credentials: Json<dto::AuthCredentials>, credentials: Json<dto::Credentials>,
cookies: Cookies, cookies: Cookies,
) -> Result<HttpResponse, APIError> { ) -> Result<HttpResponse, APIError> {
let username = credentials.username.clone(); let username = credentials.username.clone();
let is_admin = block(move || { let (user::AuthToken(token), is_admin) =
if !user_manager.login(&credentials.username, &credentials.password)? { block(move || -> Result<(user::AuthToken, bool), APIError> {
return Err(APIError::IncorrectCredentials); let auth_token = user_manager.login(&credentials.username, &credentials.password)?;
} let is_admin = user_manager.is_admin(&credentials.username)?;
user_manager Ok((auth_token, is_admin))
.is_admin(&credentials.username) })
.map_err(|_| APIError::Unspecified) .await?;
}) let authorization = dto::Authorization {
.await?; username: username.clone(),
let mut response = HttpResponse::Ok().finish(); token,
is_admin,
};
let mut response = HttpResponse::Ok().json(authorization);
add_auth_cookies(&mut response, &cookies, &username, is_admin) add_auth_cookies(&mut response, &cookies, &username, is_admin)
.map_err(|_| APIError::Unspecified)?; .map_err(|_| APIError::Unspecified)?;
Ok(response) Ok(response)
@ -770,14 +807,29 @@ async fn lastfm_scrobble(
Ok(HttpResponse::new(StatusCode::OK)) Ok(HttpResponse::new(StatusCode::OK))
} }
#[get("/lastfm/link_token")]
async fn lastfm_link_token(
lastfm_manager: Data<lastfm::Manager>,
auth: Auth,
) -> Result<Json<dto::LastFMLinkToken>, APIError> {
let user::AuthToken(value) =
block(move || lastfm_manager.generate_link_token(&auth.username)).await?;
Ok(Json(dto::LastFMLinkToken { value }))
}
#[get("/lastfm/link")] #[get("/lastfm/link")]
async fn lastfm_link( async fn lastfm_link(
lastfm_manager: Data<lastfm::Manager>, lastfm_manager: Data<lastfm::Manager>,
auth: Auth, user_manager: Data<user::Manager>,
payload: web::Query<dto::LastFMLink>, payload: web::Query<dto::LastFMLink>,
) -> Result<HttpResponse, APIError> { ) -> Result<HttpResponse, APIError> {
let popup_content_string = block(move || { let popup_content_string = block(move || {
lastfm_manager.link(&auth.username, &payload.token)?; let auth_token = user::AuthToken(payload.auth_token.clone());
let authorization =
user_manager.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)?;
let lastfm_token = &payload.token;
lastfm_manager.link(&authorization.username, lastfm_token)?;
// Percent decode // Percent decode
let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy(); let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();

View file

@ -1,5 +1,4 @@
use actix_web::{ use actix_web::{
client::ClientResponse,
middleware::{Compress, Logger}, middleware::{Compress, Logger},
rt::{System, SystemRunner}, rt::{System, SystemRunner},
test, test,
@ -7,36 +6,26 @@ use actix_web::{
web::Bytes, web::Bytes,
App, App,
}; };
use cookie::Cookie; use http::{response::Builder, Method, Request, Response};
use http::{header, response::Builder, Method, Request, Response};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap;
use std::fs; use std::fs;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::service::actix::*; use crate::service::actix::*;
use crate::service::dto;
use crate::service::test::TestService; use crate::service::test::TestService;
pub struct ActixTestService { pub struct ActixTestService {
system_runner: SystemRunner, system_runner: SystemRunner,
cookies: HashMap<String, String>, authorization: Option<dto::Authorization>,
server: TestServer, server: TestServer,
} }
pub type ServiceType = ActixTestService; pub type ServiceType = ActixTestService;
impl ActixTestService { impl ActixTestService {
fn update_cookies<T>(&mut self, actix_response: &ClientResponse<T>) {
let cookies = actix_response.headers().get_all(header::SET_COOKIE);
for raw_cookie in cookies {
let cookie = Cookie::parse(raw_cookie.to_str().unwrap()).unwrap();
self.cookies
.insert(cookie.name().to_owned(), cookie.value().to_owned());
}
}
fn process_internal<T: Serialize + Clone + 'static>( fn process_internal<T: Serialize + Clone + 'static>(
&mut self, &mut self,
request: &Request<T>, request: &Request<T>,
@ -57,22 +46,14 @@ impl ActixTestService {
actix_request = actix_request.set_header(name, value.clone()); actix_request = actix_request.set_header(name, value.clone());
} }
actix_request = { if let Some(ref authorization) = self.authorization {
let cookies_value = self actix_request = actix_request.bearer_auth(&authorization.token);
.cookies }
.iter()
.map(|(name, value)| format!("{}={}", name, value))
.collect::<Vec<_>>()
.join("; ");
actix_request.set_header(header::COOKIE, cookies_value)
};
let mut actix_response = self let mut actix_response = self
.system_runner .system_runner
.block_on(async move { actix_request.send_json(&body).await.unwrap() }); .block_on(async move { actix_request.send_json(&body).await.unwrap() });
self.update_cookies(&actix_response);
let mut response_builder = Response::builder().status(actix_response.status()); let mut response_builder = Response::builder().status(actix_response.status());
let headers = response_builder.headers_mut().unwrap(); let headers = response_builder.headers_mut().unwrap();
for (name, value) in actix_response.headers().iter() { for (name, value) in actix_response.headers().iter() {
@ -122,7 +103,7 @@ impl TestService for ActixTestService {
}); });
ActixTestService { ActixTestService {
cookies: HashMap::new(), authorization: None,
system_runner, system_runner,
server, server,
} }
@ -151,4 +132,8 @@ impl TestService for ActixTestService {
let body = serde_json::from_slice(&body.unwrap()).unwrap(); let body = serde_json::from_slice(&body.unwrap()).unwrap();
response_builder.body(body).unwrap() response_builder.body(body).unwrap()
} }
fn set_authorization(&mut self, authorization: Option<dto::Authorization>) {
self.authorization = authorization;
}
} }

View file

@ -20,11 +20,23 @@ pub struct InitialSetup {
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct AuthCredentials { pub struct Credentials {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
#[derive(Clone, Serialize, Deserialize)]
pub struct Authorization {
pub username: String,
pub token: String,
pub is_admin: bool,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AuthQueryParameters {
pub auth_token: String,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct ThumbnailOptions { pub struct ThumbnailOptions {
pub pad: Option<bool>, pub pad: Option<bool>,
@ -42,8 +54,14 @@ pub struct SavePlaylistInput {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LastFMLink { pub struct LastFMLink {
pub token: String, pub auth_token: String, // user::AuthToken emitted by Polaris, valid for LastFMLink scope
pub content: String, pub token: String, // LastFM token for use in scrobble calls
pub content: String, // Payload to send back to client after successful link
}
#[derive(Serialize, Deserialize)]
pub struct LastFMLinkToken {
pub value: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View file

@ -87,6 +87,9 @@ impl From<user::Error> for APIError {
user::Error::EmptyUsername => APIError::EmptyUsername, user::Error::EmptyUsername => APIError::EmptyUsername,
user::Error::EmptyPassword => APIError::EmptyPassword, user::Error::EmptyPassword => APIError::EmptyPassword,
user::Error::IncorrectUsername => APIError::IncorrectCredentials, user::Error::IncorrectUsername => APIError::IncorrectCredentials,
user::Error::IncorrectPassword => APIError::IncorrectCredentials,
user::Error::InvalidAuthToken => APIError::IncorrectCredentials,
user::Error::IncorrectAuthorizationScope => APIError::IncorrectCredentials,
user::Error::Unspecified => APIError::Unspecified, user::Error::Unspecified => APIError::Unspecified,
} }
} }

View file

@ -84,8 +84,9 @@ impl ContextBuilder {
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let settings_manager = settings::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let auth_secret = settings_manager.get_auth_secret()?;
let ddns_manager = ddns::Manager::new(db.clone()); let ddns_manager = ddns::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let user_manager = user::Manager::new(db.clone(), auth_secret);
let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone()); let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
let config_manager = config::Manager::new( let config_manager = config::Manager::new(
settings_manager.clone(), settings_manager.clone(),

View file

@ -77,9 +77,14 @@ fn test_login_golden_path() {
service.complete_initial_setup(); service.complete_initial_setup();
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
let response = service.fetch(&request); let response = service.fetch_json::<_, dto::Authorization>(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let authorization = response.body();
assert_eq!(authorization.username, TEST_USERNAME);
assert_eq!(authorization.is_admin, false);
assert!(!authorization.token.is_empty());
validate_added_cookies(&response); validate_added_cookies(&response);
} }
@ -97,7 +102,7 @@ fn test_requests_without_auth_header_do_not_set_cookies() {
} }
#[test] #[test]
fn test_authentication_via_http_header_rejects_bad_username() { fn test_authentication_via_basic_http_header_rejects_bad_username() {
let mut service = ServiceType::new(&test_name!()); let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup(); service.complete_initial_setup();
@ -110,7 +115,7 @@ fn test_authentication_via_http_header_rejects_bad_username() {
} }
#[test] #[test]
fn test_authentication_via_http_header_rejects_bad_password() { fn test_authentication_via_basic_http_header_rejects_bad_password() {
let mut service = ServiceType::new(&test_name!()); let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup(); service.complete_initial_setup();
@ -123,7 +128,7 @@ fn test_authentication_via_http_header_rejects_bad_password() {
} }
#[test] #[test]
fn test_authentication_via_http_header_golden_path() { fn test_authentication_via_basic_http_header_golden_path() {
let mut service = ServiceType::new(&test_name!()); let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup(); service.complete_initial_setup();
@ -136,3 +141,78 @@ fn test_authentication_via_http_header_golden_path() {
validate_added_cookies(&response); validate_added_cookies(&response);
} }
#[test]
fn test_authentication_via_bearer_http_header_rejects_bad_token() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let mut request = protocol::random();
let bearer = headers::Authorization::bearer("garbage").unwrap();
request.headers_mut().typed_insert(bearer);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_authentication_via_bearer_http_header_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let authorization = {
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
let response = service.fetch_json::<_, dto::Authorization>(&request);
assert_eq!(response.status(), StatusCode::OK);
response.into_body()
};
service.logout();
let mut request = protocol::random();
let bearer = headers::Authorization::bearer(&authorization.token).unwrap();
request.headers_mut().typed_insert(bearer);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_no_cookies(&response);
}
#[test]
fn test_authentication_via_query_param_rejects_bad_token() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let mut request = protocol::random();
*request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token")
.parse()
.unwrap();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_authentication_via_query_param_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let authorization = {
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
let response = service.fetch_json::<_, dto::Authorization>(&request);
assert_eq!(response.status(), StatusCode::OK);
response.into_body()
};
service.logout();
let mut request = protocol::random();
*request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token)
.parse()
.unwrap();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_no_cookies(&response);
}

View file

@ -1,6 +1,7 @@
use http::StatusCode; use http::StatusCode;
use std::path::PathBuf; use std::path::PathBuf;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name; use crate::test_name;
@ -37,3 +38,25 @@ fn test_lastfm_now_playing_ignores_unlinked_user() {
let response = service.fetch(&request); let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NO_CONTENT); assert_eq!(response.status(), StatusCode::NO_CONTENT);
} }
#[test]
fn lastfm_link_token_requires_auth() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::lastfm_link_token();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn lastfm_link_token_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login();
let request = protocol::lastfm_link_token();
let response = service.fetch_json::<_, dto::LastFMLinkToken>(&request);
assert_eq!(response.status(), StatusCode::OK);
let link_token = response.body();
assert!(!link_token.value.is_empty());
}

View file

@ -62,18 +62,28 @@ pub trait TestService {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
fn login_admin(&mut self) { fn login_internal(&mut self, username: &str, password: &str) {
let request = protocol::login(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN); let request = protocol::login(username, password);
let response = self.fetch(&request); let response = self.fetch_json::<_, dto::Authorization>(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let authorization = response.into_body();
self.set_authorization(Some(authorization));
}
fn login_admin(&mut self) {
self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN);
} }
fn login(&mut self) { fn login(&mut self) {
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); self.login_internal(TEST_USERNAME, TEST_PASSWORD);
let response = self.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
} }
fn logout(&mut self) {
self.set_authorization(None);
}
fn set_authorization(&mut self, authorization: Option<dto::Authorization>);
fn index(&mut self) { fn index(&mut self) {
let request = protocol::trigger_index(); let request = protocol::trigger_index();
let response = self.fetch(&request); let response = self.fetch(&request);

View file

@ -37,8 +37,8 @@ pub fn initial_setup() -> Request<()> {
.unwrap() .unwrap()
} }
pub fn login(username: &str, password: &str) -> Request<dto::AuthCredentials> { pub fn login(username: &str, password: &str) -> Request<dto::Credentials> {
let credentials = dto::AuthCredentials { let credentials = dto::Credentials {
username: username.into(), username: username.into(),
password: password.into(), password: password.into(),
}; };
@ -253,6 +253,14 @@ pub fn delete_playlist(name: &str) -> Request<()> {
.unwrap() .unwrap()
} }
pub fn lastfm_link_token() -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/lastfm/link_token")
.body(())
.unwrap()
}
pub fn lastfm_now_playing(path: &Path) -> Request<()> { pub fn lastfm_now_playing(path: &Path) -> Request<()> {
let path = path.to_string_lossy(); let path = path.to_string_lossy();
let endpoint = format!("/api/lastfm/now_playing/{}", url_encode(path.as_ref())); let endpoint = format!("/api/lastfm/now_playing/{}", url_encode(path.as_ref()));