Skip to content

Commit

Permalink
Change session to be based off of UserID instead of Username (#437)
Browse files Browse the repository at this point in the history
* Moved auth to Database crate

* Updated sqlx (Needed for RETURNING statements)

* Updated User to hold an ID, and changed almost all functions to use ID instead of username.  Now pass User struct to authenticated functions

* Removed old sqlx patch from Cargo.toml

* Fixed username re-rendering

* Removed no-auth feature, added cookie tests

* Fix roles to be transparent, fixed imports

* Fixed issue with once_cell and parallel tests

* Refactored authentication back into it's original crate

* chore: fix proxy, stringify error when dispatching CHANGE_USERNAME_ERR to avoid redux from complaining the error is not serializiable

* Added migration for userid and progress, added fix for previous migrations to become compatible with RC-6

* Updated migration to rename tables instead of recreate

* Fixed readability of migration

* cleanup Cargo.toml

* Fix formatting

Co-authored-by: Valerian G <valerian@dusklabs.io>
  • Loading branch information
cobyge and vgarleanu committed May 14, 2022
1 parent 9196a68 commit fd3602f
Show file tree
Hide file tree
Showing 39 changed files with 791 additions and 649 deletions.
7 changes: 0 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,3 @@ members = ["dim", "database", "auth", "events"]

[profile.dev]
codegen-units = 16

[profile.release]
lto = true
opt-level = 'z'

[patch.crates-io]
sqlx = { git = "https://github.com/launchbadge/sqlx", tag = "v0.5.5" }
12 changes: 5 additions & 7 deletions auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
null_auth = []

[dependencies]
serde = { version = "^1.0.105", features = ["derive"], default-features = false }
jsonwebtoken = "8.0.0-beta.2"
time = "0.1.42"
uuid = { version = "0.8.2", features = ["v4"] }
rand = "0.8.3"
warp = "0.3.1"
serde = { version = "^1", features = ["derive"] }
once_cell = "1.8.0"
rand = "0.8.3"
aes-gcm = "0.9.4"
base64 = "0.13.0"
err-derive = "^0.3.0"
268 changes: 75 additions & 193 deletions auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,216 +1,98 @@
use jsonwebtoken::decode;
use jsonwebtoken::encode;
use jsonwebtoken::Algorithm;
use jsonwebtoken::DecodingKey;
use jsonwebtoken::EncodingKey;
use jsonwebtoken::Header;
use jsonwebtoken::TokenData;
use jsonwebtoken::Validation;
use aes_gcm::aead::generic_array::GenericArray;
use aes_gcm::aead::Aead;
use aes_gcm::AeadInPlace;
use aes_gcm::Aes256Gcm;
use aes_gcm::NewAead;

use err_derive::Error;
use once_cell::sync::OnceCell;
use rand::Rng;
use serde::Deserialize;
use rand::RngCore;
use serde::Serialize;
use time::get_time;
use std::convert::TryInto;

use warp::filters::header::headers_cloned;
use warp::http::header::HeaderMap;
use warp::http::header::AUTHORIZATION;
use warp::reject;
use warp::Filter;
use warp::Rejection;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;

#[cfg(all(not(debug_assertions), feature = "null_auth"))]
std::compile_error!("Cannot disable authentication for non-devel environments.");

/// This is the secret key with which we sign the JWT tokens.
/// This is the secret key with which we sign the cookies.
// TODO: Generate this at first run to ensure security
static KEY: OnceCell<[u8; 16]> = OnceCell::new();
static ONE_WEEK: i64 = 60 * 60 * 24 * 7;
static KEY: OnceCell<[u8; 32]> = OnceCell::new();

pub fn generate_key() -> [u8; 16] {
pub fn generate_key() -> [u8; 32] {
rand::thread_rng().gen()
}

pub fn set_jwt_key(k: [u8; 16]) {
KEY.set(k).expect("Failed to set JWT secret_key")
pub fn set_key(k: [u8; 32]) {
KEY.set(k).expect("Failed to set secret_key")
}

fn get_key() -> &'static [u8; 16] {
KEY.get().expect("JWT key must be initialized")
/// This function should only be called from tests
pub fn set_key_fallible(k: [u8; 32]) {
let _ = KEY.set(k);
}

/// Struct holds info needed for JWT to function correctly
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct UserRolesToken {
/// Unique, per jwt identifier
pub id: u128,
/// Timestamp when the token was issued.
iat: i64,
/// Timestamp when the token expires.
exp: i64,
/// Username of the user to whom this token belongs to
user: String,
/// The roles of the user, usually owner or user
// TODO: Use a enum here maybe considering theres like two possibilities lol?
roles: Vec<String>,
fn get_key() -> &'static [u8; 32] {
KEY.get().expect("key must be initialized")
}

#[derive(Debug)]
pub struct Wrapper(pub TokenData<UserRolesToken>);

impl Wrapper {
pub fn get_user(&self) -> String {
self.0.claims.get_user()
}

pub fn user_ref(&self) -> &str {
self.0.claims.user_ref()
}
#[derive(Clone, Debug, Error, Serialize)]
pub enum AuthError {
#[error(display = "InvalidBase64")]
BadBase64,
#[error(display = "Data too short")]
ShortData,
#[error(display = "Failed Decrypting")]
DecryptError,
#[error(display = "Plaintext is not a UserID")]
PlainTextNoti64,
}

#[derive(Clone, Debug)]
pub enum JWTError {
Missing,
Invalid,
InvalidKey,
BadCount,
/// Function encrypts a UserID with a nonce and returns it as a base64 string to be used as a cookie/token.
pub fn user_cookie_generate(user: i64) -> String {
// Create a vec to hold the [nonce | cookie value].
let cookie_val = &user.to_be_bytes();
let mut data = vec![0; NONCE_LEN + cookie_val.len() + TAG_LEN];

// Split data into three: nonce, input/output, tag. Copy input.
let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
let (in_out, tag) = in_out.split_at_mut(cookie_val.len());
in_out.copy_from_slice(cookie_val);

// Fill nonce piece with random data.
let mut rng = rand::thread_rng();
rng.try_fill_bytes(nonce)
.expect("couldn't random fill nonce");
let nonce = GenericArray::clone_from_slice(nonce);

// Perform the actual sealing operation, using the cookie's name as
// associated data to prevent value swapping.
let aead = Aes256Gcm::new(GenericArray::from_slice(get_key()));
let aad_tag = aead
.encrypt_in_place_detached(&nonce, b"", in_out)
.expect("encryption failure!");

// Copy the tag into the tag piece.
tag.copy_from_slice(&aad_tag);

// Base64 encode [nonce | encrypted value | tag].
base64::encode(&data)
}

impl warp::reject::Reject for JWTError {}

impl UserRolesToken {
/// Method returns whether the token is expired or not.
pub fn is_expired(&self) -> bool {
let now = get_time().sec;
now >= self.exp
}

/// Method used to make sure that tokens are generated for different users to avoid collisions
pub fn is_claimed_user(&self, claimed_user: String) -> bool {
self.user == claimed_user
}

/// Method checks if the user holding this token has a specific role.
pub fn has_role(&self, role: &str) -> bool {
self.roles.contains(&role.to_string())
}

/// Method returns the username from the token
pub fn get_user(&self) -> String {
self.user.clone()
}

pub fn get_user_ref(&self) -> &str {
&self.user
/// Function decrypts a UserID which was encrypted with `user_cookie_generate`
pub fn user_cookie_decode(cookie: String) -> Result<i64, AuthError> {
let data = base64::decode(cookie).map_err(|_| AuthError::BadBase64)?;
if data.len() <= NONCE_LEN {
return Err(AuthError::ShortData);
}

/// Method returns the id of this token
pub fn get_id(&self) -> u128 {
self.id
}

/// Method returns a clone of all roles.
pub fn clone_roles(&self) -> Vec<String> {
self.roles
.iter()
.map(|x| x.to_ascii_lowercase())
.collect::<Vec<_>>()
}

pub fn user_ref(&self) -> &str {
self.user.as_ref()
}
}

/// Function generates a new JWT token and signs it with our KEY
/// # Arguments
/// * `user` - Username for whom we want to generate a token
/// * `roles` - vector of roles we want to give to this user.
///
/// # Example
/// ```
/// use auth::{jwt_generate, jwt_check};
///
/// auth::set_jwt_key(auth::generate_key());
///
/// let token_1 = jwt_generate("test".into(), vec!["owner".into()]);
/// let check_token = jwt_check(token_1).unwrap();
/// ```
pub fn jwt_generate(user: String, roles: Vec<String>) -> String {
let now = get_time().sec;
let payload = UserRolesToken {
id: uuid::Uuid::new_v4().to_u128_le(),
iat: now,
exp: now + ONE_WEEK,
user,
roles,
};

encode(
&Header::new(Algorithm::HS512),
&payload,
&EncodingKey::from_secret(get_key()),
)
.unwrap()
}

/// Function checks the token supplied and validates it
/// # Arguments
/// * `token` - JWT token we want to validate
///
/// # Example
/// ```
/// use auth::{jwt_generate, jwt_check};
///
/// auth::set_jwt_key(auth::generate_key());
///
/// let token_1 = jwt_generate("test".into(), vec!["owner".into()]);
/// let check_token = jwt_check(token_1).unwrap();
///
/// let check_token_2 = jwt_check("testtesttest".into());
/// assert!(check_token_2.is_err());
/// ```
#[cfg(not(feature = "null_auth"))]
pub fn jwt_check(token: String) -> Result<TokenData<UserRolesToken>, jsonwebtoken::errors::Error> {
decode::<UserRolesToken>(
&token,
&DecodingKey::from_secret(get_key()),
&Validation::new(Algorithm::HS512),
)
}

#[cfg(all(debug_assertions, feature = "null_auth"))]
pub fn jwt_check(_: String) -> Result<TokenData<UserRolesToken>, jsonwebtoken::errors::Error> {
Ok(TokenData {
header: jsonwebtoken::Header {
alg: jsonwebtoken::Algorithm::HS512,
..Default::default()
},
claims: UserRolesToken {
id: uuid::Uuid::new_v4().to_u128_le(),
iat: 0,
exp: i64::MAX,
user: "Admin".into(),
roles: vec!["owner".into()],
},
})
}

pub fn with_auth() -> impl Filter<Extract = (Wrapper,), Error = Rejection> + Clone {
headers_cloned().and_then(|x: HeaderMap| async move {
match x.get(AUTHORIZATION) {
Some(k) => match k.to_str().ok().and_then(|x| jwt_check(x.into()).ok()) {
Some(k) => Ok(Wrapper(k)),
None => Err(reject::custom(JWTError::InvalidKey)),
},
None => {
if cfg!(not(feature = "null_auth")) {
Err(reject::custom(JWTError::Missing))
} else {
Ok(Wrapper(jwt_check(String::new()).unwrap()))
}
}
}
})
let (nonce, cipher) = data.split_at(NONCE_LEN);
let aead = Aes256Gcm::new(GenericArray::from_slice(get_key()));
let plaintext = aead
.decrypt(GenericArray::from_slice(nonce), cipher)
.map_err(|_| AuthError::DecryptError)?;

Ok(i64::from_be_bytes(
plaintext
.try_into()
.map_err(|_| AuthError::PlainTextNoti64)?,
))
}
11 changes: 6 additions & 5 deletions database/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,20 @@ tracing-log = "0.1.2"
tracing = "0.1.29"

ring = "^0.16.11"
base64 = "0.13.0"
uuid = { version = "0.8.1", features = ["v4"] }
cfg-if = "1.0.0"
err-derive = "0.3.0"
sqlx = { version = "=0.5.5", features = ["runtime-tokio-rustls"] }
err-derive = "^0.3.0"
sqlx = { version = "=0.5.13", features = ["runtime-tokio-rustls"] }
once_cell = "1.8.0"
tokio = "1.14.0"
base64 = "0.13.0"

auth = { path = "../auth" }

[dev-dependencies]
tokio = { version = "1", default-features = false, features = ["rt", "macros"] }

[build-dependencies]
fs_extra = "1.1.0"
sqlx = { version = "=0.5.5" }
sqlx = { version = "=0.5.13" }
tokio = "1.12.0"
dotenv = "0.15.0"
7 changes: 4 additions & 3 deletions database/migrations/20210619211347_dim.sql
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ CREATE TABLE _tblmedia (
CREATE VIEW media AS
SELECT _tblmedia.*, pp.local_path as poster_path, bp.local_path as backdrop_path
FROM _tblmedia
LEFT OUTER JOIN assets pp ON _tblmedia.poster = pp.id
LEFT OUTER JOIN assets bp ON _tblmedia.backdrop = bp.id;
LEFT JOIN assets pp ON _tblmedia.poster = pp.id
LEFT JOIN assets bp ON _tblmedia.backdrop = bp.id;

CREATE TRIGGER media_delete
INSTEAD OF DELETE ON media
Expand All @@ -53,6 +53,7 @@ BEGIN
END;

CREATE UNIQUE INDEX media_idx ON _tblmedia(library_id, name, media_type) WHERE NOT _tblmedia.media_type = "episode";
CREATE INDEX media_excl_ep_idx ON _tblmedia(name) WHERE NOT _tblmedia.media_type = "episode";

CREATE TABLE movie (
id INTEGER,
Expand Down Expand Up @@ -85,7 +86,7 @@ CREATE VIEW season AS
SELECT _tblseason.id, _tblseason.season_number,
_tblseason.tvshowid, _tblseason.added, assets.local_path as poster
FROM _tblseason
LEFT OUTER JOIN assets ON _tblseason.poster = assets.id;
JOIN assets ON _tblseason.poster = assets.id;

CREATE TRIGGER season_delete
INSTEAD OF DELETE ON season
Expand Down
Loading

0 comments on commit fd3602f

Please sign in to comment.