-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change session to be based off of UserID instead of Username (#437)
* 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
Showing
39 changed files
with
791 additions
and
649 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)?, | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.