use std::collections::HashMap;
use std::io::{Read, Write};
use csv;
use ring::constant_time::verify_slices_are_equal;
use ring::test;
use super::util::{generate_salt, hash_password_digest, hex_dump};
use super::{AuthenticationResult, Basic};
use crate::{Error, JsonMap, JsonValue};
pub type Users = HashMap<String, (Vec<u8>, Vec<u8>)>;
pub struct SimpleAuthenticator {
users: Users,
}
impl SimpleAuthenticator {
pub fn new<R: Read>(csv: csv::Reader<R>) -> Result<Self, Error> {
warn_!("Do not use the Simple authenticator in production");
Ok(SimpleAuthenticator {
users: Self::users_from_csv(csv)?,
})
}
pub fn with_csv_file(path: &str, has_headers: bool, delimiter: u8) -> Result<Self, Error> {
let reader = csv::ReaderBuilder::new()
.has_headers(has_headers)
.delimiter(delimiter)
.from_path(path)
.map_err(|e| e.to_string())?;
Self::new(reader)
}
fn users_from_csv<R: Read>(mut csv: csv::Reader<R>) -> Result<Users, Error> {
let records: Vec<csv::Result<(String, String, String)>> = csv.deserialize().collect();
let (records, errors): (Vec<_>, Vec<_>) = records.into_iter().partition(Result::is_ok);
if !errors.is_empty() {
let errors: Vec<String> = errors
.into_iter()
.map(|r| r.unwrap_err().to_string())
.collect();
Err(errors.join("; "))?;
}
type ParsedRecordBytes = Vec<Result<(String, Vec<u8>, Vec<u8>), String>>;
let (users, errors): (ParsedRecordBytes, ParsedRecordBytes) = records
.into_iter()
.map(|r| {
let (username, hash, salt) = r.unwrap(); let hash = test::from_hex(&hash)?;
let salt = test::from_hex(&salt)?;
Ok((username, hash, salt))
})
.partition(Result::is_ok);
if !errors.is_empty() {
let errors: Vec<String> = errors.into_iter().map(|r| r.unwrap_err()).collect();
Err(errors.join("; "))?;
}
let users: Users = users
.into_iter()
.map(|r| {
let (username, hash, salt) = r.unwrap(); (username, (hash, salt))
})
.collect();
Ok(users)
}
pub fn hash_password(password: &str, salt: &[u8]) -> Result<String, Error> {
Ok(hex_dump(hash_password_digest(password, salt).as_ref()))
}
pub fn verify(
&self,
username: &str,
password: &str,
include_refresh_payload: bool,
) -> Result<AuthenticationResult, Error> {
match self.users.get(username) {
None => Err(Error::Auth(super::Error::AuthenticationFailure)),
Some(&(ref hash, ref salt)) => {
let actual_password_digest = hash_password_digest(password, salt);
if !verify_slices_are_equal(actual_password_digest.as_ref(), &*hash).is_ok() {
Err(Error::Auth(super::Error::AuthenticationFailure))
} else {
let refresh_payload = if include_refresh_payload {
let mut map = JsonMap::with_capacity(2);
let _ = map.insert("user".to_string(), From::from(username));
let _ = map.insert("password".to_string(), From::from(password));
Some(JsonValue::Object(map))
} else {
None
};
Ok(AuthenticationResult {
subject: username.to_string(),
private_claims: JsonValue::Object(JsonMap::new()),
refresh_payload,
})
}
}
}
}
}
impl super::Authenticator<Basic> for SimpleAuthenticator {
fn authenticate(
&self,
authorization: &super::Authorization<Basic>,
include_refresh_payload: bool,
) -> Result<AuthenticationResult, Error> {
warn_!("Do not use the Simple authenticator in production");
let username = authorization.username();
let password = authorization.password().unwrap_or_else(|| "".to_string());
self.verify(&username, &password, include_refresh_payload)
}
fn authenticate_refresh_token(
&self,
refresh_payload: &JsonValue,
) -> Result<AuthenticationResult, Error> {
warn_!("Do not use the Simple authenticator in production");
match *refresh_payload {
JsonValue::Object(ref map) => {
let user = map
.get("user")
.ok_or_else(|| super::Error::AuthenticationFailure)?
.as_str()
.ok_or_else(|| super::Error::AuthenticationFailure)?;
let password = map
.get("password")
.ok_or_else(|| super::Error::AuthenticationFailure)?
.as_str()
.ok_or_else(|| super::Error::AuthenticationFailure)?;
self.verify(user, password, false)
}
_ => Err(super::Error::AuthenticationFailure)?,
}
}
}
#[derive(Eq, PartialEq, Serialize, Deserialize, Debug)]
pub struct SimpleAuthenticatorConfiguration {
pub csv_path: String,
#[serde(default)]
pub has_headers: bool,
#[serde(default = "default_delimiter")]
pub delimiter: char,
}
fn default_delimiter() -> char {
','
}
impl super::AuthenticatorConfiguration<Basic> for SimpleAuthenticatorConfiguration {
type Authenticator = SimpleAuthenticator;
fn make_authenticator(&self) -> Result<Self::Authenticator, Error> {
Ok(SimpleAuthenticator::with_csv_file(
&self.csv_path,
self.has_headers,
self.delimiter as u8,
)?)
}
}
pub fn hash_passwords(users: &HashMap<String, String>, salt_len: usize) -> Result<Users, Error> {
let mut hashed: Users = HashMap::new();
for (user, password) in users {
let salt = generate_salt(salt_len).map_err(|()| "Unspecified error".to_string())?;
let hash = hash_password_digest(password, &salt);
let _ = hashed.insert(user.to_string(), (hash, salt));
}
Ok(hashed)
}
pub fn write_csv<W: Write>(users: &Users, mut writer: W) -> Result<(), Error> {
for (username, &(ref hash, ref salt)) in users {
let record = vec![username.to_string(), hex_dump(hash), hex_dump(salt)].join(",");
writer.write_all(record.as_bytes())?;
writer.write_all(b"\n")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::Authenticator;
fn make_authenticator() -> SimpleAuthenticator {
not_err!(SimpleAuthenticator::with_csv_file(
"test/fixtures/users.csv",
false,
b',',
))
}
#[test]
fn test_hex_dump() {
assert_eq!(hex_dump(b"foobar"), "666f6f626172");
}
#[test]
fn test_hex_dump_all_bytes() {
for i in 0..256 {
assert_eq!(hex_dump(&[i as u8]), format!("{:02x}", i));
}
}
#[test]
fn hashing_is_done_correctly() {
let hashed_password = not_err!(SimpleAuthenticator::hash_password("password", &[0; 32]));
assert_eq!(
"e6e1111452a5574d8d64f6f4ba6fabc86af5c45c341df1eb23026373c41d24b8",
hashed_password
);
}
#[test]
fn hashing_is_done_correctly_for_unicode() {
let hashed_password = not_err!(SimpleAuthenticator::hash_password(
"冻住,不许走!",
&[0; 32],
));
assert_eq!(
"b400a5eea452afcc67a81602f28012e5634404ddf1e043d6ff1df67022c88cd2",
hashed_password
);
}
#[test]
fn csv_generation_round_trip() {
use std::io::Cursor;
let users: HashMap<String, String> =
[("foobar", "password"), ("mei", "冻住,不许走!")]
.into_iter()
.map(|&(u, p)| (u.to_string(), p.to_string()))
.collect();
let users = not_err!(hash_passwords(&users, 32));
let mut cursor: Cursor<Vec<u8>> = Cursor::new(vec![]);
not_err!(write_csv(&users, &mut cursor));
cursor.set_position(0);
let authenticator = not_err!(SimpleAuthenticator::new(
csv::ReaderBuilder::new()
.has_headers(false,)
.from_reader(&mut cursor,),
));
let expected_keys = vec!["foobar".to_string(), "mei".to_string()];
let mut actual_keys: Vec<String> = authenticator.users.keys().cloned().collect();
actual_keys.sort();
assert_eq!(expected_keys, actual_keys);
let _ = not_err!(authenticator.verify("foobar", "password", false));
let result = not_err!(authenticator.verify("mei", "冻住,不许走!", false));
assert!(result.refresh_payload.is_none());
}
#[test]
fn authentication_with_username_and_password() {
let authenticator = make_authenticator();
let expected_keys = vec!["foobar".to_string(), "mei".to_string()];
let mut actual_keys: Vec<String> = authenticator.users.keys().cloned().collect();
actual_keys.sort();
assert_eq!(expected_keys, actual_keys);
let _ = not_err!(authenticator.verify("foobar", "password", false));
let result = not_err!(authenticator.verify("mei", "冻住,不许走!", false));
assert!(result.refresh_payload.is_none());
}
#[test]
fn authentication_with_refresh_payload() {
let authenticator = make_authenticator();
let result = not_err!(authenticator.verify("foobar", "password", true));
assert!(result.refresh_payload.is_some());
let result = not_err!(
authenticator.authenticate_refresh_token(result.refresh_payload.as_ref().unwrap(),)
);
assert!(result.refresh_payload.is_none());
}
#[test]
fn simple_authenticator_configuration_deserialization() {
use crate::auth::AuthenticatorConfiguration;
use serde_json;
let json = r#"{
"csv_path": "test/fixtures/users.csv",
"delimiter": ","
}"#;
let deserialized: SimpleAuthenticatorConfiguration = not_err!(serde_json::from_str(json));
let expected_config = SimpleAuthenticatorConfiguration {
csv_path: "test/fixtures/users.csv".to_string(),
has_headers: false,
delimiter: ',',
};
assert_eq!(deserialized, expected_config);
let _ = not_err!(expected_config.make_authenticator());
}
}