Merge factors type for authentication

This commit is contained in:
2022-11-11 12:26:02 +01:00
parent 8d231c0b45
commit af383720b7
44 changed files with 1177 additions and 674 deletions

View File

@ -27,8 +27,8 @@ impl AccessToken {
jwt_id: None,
nonce: self.nonce,
custom: CustomAccessTokenClaims {
rand_val: self.rand_val
rand_val: self.rand_val,
},
}
}
}
}

View File

@ -42,4 +42,4 @@ impl EntityManager<Client> {
c.redirect_uri = apply_env_vars(&c.redirect_uri);
}
}
}
}

View File

@ -16,10 +16,8 @@ impl CodeChallenge {
match self.code_challenge_method.as_str() {
"plain" => code_verifer.eq(&self.code_challenge),
"S256" => {
let encoded = base64::encode_config(
sha256(code_verifer.as_bytes()),
URL_SAFE_NO_PAD,
);
let encoded =
base64::encode_config(sha256(code_verifer.as_bytes()), URL_SAFE_NO_PAD);
encoded.eq(&self.code_challenge)
}
@ -64,7 +62,10 @@ mod test {
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
};
assert_eq!(true, chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"));
assert_eq!(
true,
chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
);
assert_eq!(false, chal.verify_code("text1"));
}
}
}

View File

@ -1,7 +1,7 @@
use std::io::ErrorKind;
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
use aes_gcm::aead::{Aead, OsRng};
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
use rand::Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
@ -17,7 +17,9 @@ pub struct CryptoWrapper {
impl CryptoWrapper {
/// Generate a new memory wrapper
pub fn new_random() -> Self {
Self { key: Aes256Gcm::generate_key(&mut OsRng) }
Self {
key: Aes256Gcm::generate_key(&mut OsRng),
}
}
/// Encrypt some data
@ -27,11 +29,11 @@ impl CryptoWrapper {
let serialized_data = bincode::serialize(data)?;
let mut enc = aes_key.encrypt(Nonce::from_slice(&nonce_bytes),
serialized_data.as_slice()).unwrap();
let mut enc = aes_key
.encrypt(Nonce::from_slice(&nonce_bytes), serialized_data.as_slice())
.unwrap();
enc.extend_from_slice(&nonce_bytes);
Ok(base64::encode(enc))
}
@ -40,8 +42,10 @@ impl CryptoWrapper {
let bytes = base64::decode(input)?;
if bytes.len() < NONCE_LEN {
return Err(Box::new(std::io::Error::new(ErrorKind::Other,
"Input string is smaller than nonce!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Input string is smaller than nonce!",
)));
}
let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN);
@ -53,8 +57,10 @@ impl CryptoWrapper {
Ok(d) => d,
Err(e) => {
log::error!("Failed to decrypt wrapped data! {:#?}", e);
return Err(Box::new(std::io::Error::new(ErrorKind::Other,
"Failed to decrypt wrapped data!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Failed to decrypt wrapped data!",
)));
}
};
@ -87,4 +93,4 @@ mod test {
let enc = wrapper_1.encrypt(&msg).unwrap();
wrapper_2.decrypt::<Message>(&enc).unwrap_err();
}
}
}

View File

@ -4,9 +4,9 @@ use std::pin::Pin;
use actix::Addr;
use actix_identity::Identity;
use actix_web::{Error, FromRequest, HttpRequest, web};
use actix_web::dev::Payload;
use actix_web::error::ErrorInternalServerError;
use actix_web::{web, Error, FromRequest, HttpRequest};
use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor;
@ -31,27 +31,33 @@ impl Deref for CurrentUser {
impl FromRequest for CurrentUser {
type Error = Error;
type Future = Pin<Box<dyn Future<Output=Result<Self, Self::Error>>>>;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let user_actor: &web::Data<Addr<UsersActor>> = req.app_data().expect("UserActor undefined!");
let user_actor: &web::Data<Addr<UsersActor>> =
req.app_data().expect("UserActor undefined!");
let user_actor: Addr<UsersActor> = user_actor.as_ref().clone();
let identity: Identity = Identity::from_request(req, payload).into_inner()
let identity: Identity = Identity::from_request(req, payload)
.into_inner()
.expect("Failed to get identity!");
let user_id = SessionIdentity(Some(&identity)).user_id();
Box::pin(async move {
let user = match user_actor.send(
users_actor::GetUserRequest(user_id)
).await.unwrap().0 {
let user = match user_actor
.send(users_actor::GetUserRequest(user_id))
.await
.unwrap()
.0
{
Some(u) => u,
None => {
return Err(ErrorInternalServerError("Could not extract user information!"));
return Err(ErrorInternalServerError(
"Could not extract user information!",
));
}
};
Ok(CurrentUser(user))
})
}
}
}

View File

@ -3,7 +3,10 @@ use std::slice::{Iter, IterMut};
use crate::utils::err::Res;
enum FileFormat { Json, Yaml }
enum FileFormat {
Json,
Yaml,
}
pub struct EntityManager<E> {
file_path: PathBuf,
@ -11,8 +14,8 @@ pub struct EntityManager<E> {
}
impl<E> EntityManager<E>
where
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
where
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
{
/// Open entity
pub fn open_or_create<A: AsRef<Path>>(path: A) -> Res<Self> {
@ -30,7 +33,7 @@ impl<E> EntityManager<E>
file_path: path.as_ref().to_path_buf(),
list: match Self::file_format(path.as_ref()) {
FileFormat::Json => serde_json::from_str(&file_content)?,
FileFormat::Yaml => serde_yaml::from_str(&file_content)?
FileFormat::Yaml => serde_yaml::from_str(&file_content)?,
},
})
}
@ -49,7 +52,7 @@ impl<E> EntityManager<E>
fn file_format(p: &Path) -> FileFormat {
match p.to_string_lossy().ends_with(".json") {
true => FileFormat::Json,
false => FileFormat::Yaml
false => FileFormat::Yaml,
}
}
@ -70,8 +73,8 @@ impl<E> EntityManager<E>
/// Replace entries in the list that matches a criteria
pub fn replace_entries<F>(&mut self, filter: F, el: &E) -> Res
where
F: Fn(&E) -> bool,
where
F: Fn(&E) -> bool,
{
for i in 0..self.list.len() {
if filter(&self.list[i]) {

View File

@ -49,4 +49,4 @@ impl IdToken {
},
}
}
}
}

View File

@ -27,8 +27,9 @@ pub struct JWTSigner(RS256KeyPair);
impl JWTSigner {
pub fn gen_from_memory() -> Res<Self> {
Ok(Self(RS256KeyPair::generate(2048)?
.with_key_id(&format!("key-{}", rand_str(15)))))
Ok(Self(
RS256KeyPair::generate(2048)?.with_key_id(&format!("key-{}", rand_str(15))),
))
}
pub fn get_json_web_key(&self) -> JsonWebKey {
@ -45,4 +46,4 @@ impl JWTSigner {
pub fn sign_token<E: Serialize + DeserializeOwned>(&self, c: JWTClaims<E>) -> Res<String> {
Ok(self.0.sign(c)?)
}
}
}

View File

@ -18,4 +18,4 @@ impl Default for LoginRedirect {
fn default() -> Self {
Self("/".to_string())
}
}
}

View File

@ -1,17 +1,17 @@
pub mod app_config;
pub mod entity_manager;
pub mod session_identity;
pub mod user;
pub mod client;
pub mod remote_ip;
pub mod current_user;
pub mod openid_config;
pub mod jwt_signer;
pub mod id_token;
pub mod code_challenge;
pub mod open_id_user_info;
pub mod access_token;
pub mod totp_key;
pub mod app_config;
pub mod client;
pub mod code_challenge;
pub mod crypto_wrapper;
pub mod current_user;
pub mod entity_manager;
pub mod id_token;
pub mod jwt_signer;
pub mod login_redirect;
pub mod open_id_user_info;
pub mod openid_config;
pub mod remote_ip;
pub mod session_identity;
pub mod totp_key;
pub mod user;
pub mod webauthn_manager;
pub mod crypto_wrapper;

View File

@ -21,4 +21,4 @@ pub struct OpenIDUserInfo {
/// True if the End-User's e-mail address has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this e-mail address was controlled by the End-User at the time the verification was performed. The means by which an e-mail address is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating.
pub email_verified: bool,
}
}

View File

@ -34,4 +34,4 @@ pub struct OpenIDConfig {
pub claims_supported: Vec<&'static str>,
pub code_challenge_methods_supported: Vec<&'static str>,
}
}

View File

@ -1,8 +1,8 @@
use std::net::IpAddr;
use actix_web::{Error, FromRequest, HttpRequest, web};
use actix_web::dev::Payload;
use futures_util::future::{Ready, ready};
use actix_web::{web, Error, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::data::app_config::AppConfig;
use crate::utils::network_utils::get_remote_ip;
@ -25,4 +25,4 @@ impl FromRequest for RemoteIP {
let config: &web::Data<AppConfig> = req.app_data().expect("AppData undefined!");
ready(Ok(RemoteIP(get_remote_ip(req, config.proxy_ip.as_deref()))))
}
}
}

View File

@ -33,8 +33,7 @@ impl<'a> SessionIdentity<'a> {
fn get_session_data(&self) -> Option<SessionIdentityData> {
if let Some(id) = self.0 {
Self::deserialize_session_data(id.id().ok())
}
else {
} else {
None
}
}
@ -71,12 +70,15 @@ impl<'a> SessionIdentity<'a> {
}
pub fn set_user(&self, req: &HttpRequest, user: &User, status: SessionStatus) {
self.set_session_data(req, &SessionIdentityData {
id: Some(user.uid.clone()),
is_admin: user.admin,
auth_time: time(),
status,
});
self.set_session_data(
req,
&SessionIdentityData {
id: Some(user.uid.clone()),
is_admin: user.admin,
auth_time: time(),
status,
},
);
}
pub fn set_status(&self, req: &HttpRequest, status: SessionStatus) {
@ -108,7 +110,9 @@ impl<'a> SessionIdentity<'a> {
}
pub fn user_id(&self) -> UserID {
self.get_session_data().unwrap_or_default().id
self.get_session_data()
.unwrap_or_default()
.id
.expect("UserID should never be null here!")
}

View File

@ -23,13 +23,15 @@ impl TotpKey {
pub fn new_random() -> Self {
let random_bytes = rand::thread_rng().gen::<[u8; 10]>();
Self {
encoded: base32::encode(BASE32_ALPHABET, &random_bytes)
encoded: base32::encode(BASE32_ALPHABET, &random_bytes),
}
}
/// Get a key from an encoded secret
pub fn from_encoded_secret(s: &str) -> Self {
Self { encoded: s.to_string() }
Self {
encoded: s.to_string(),
}
}
/// Get QrCode URL for user
@ -74,15 +76,19 @@ impl TotpKey {
/// Get the code at a specific time
fn get_code_at<F: Fn() -> u64>(&self, get_time: F) -> Res<String> {
let gen = TotpGenerator::new()
.set_digit(NUM_DIGITS).unwrap()
.set_step(PERIOD).unwrap()
.set_digit(NUM_DIGITS)
.unwrap()
.set_step(PERIOD)
.unwrap()
.set_hash_algorithm(HashAlgorithm::SHA1)
.build();
let key = match base32::decode(BASE32_ALPHABET, &self.encoded) {
None => {
return Err(Box::new(
std::io::Error::new(ErrorKind::Other, "Failed to decode base32 secret!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Failed to decode base32 secret!",
)));
}
Some(k) => k,
};
@ -113,4 +119,4 @@ mod test {
let key = TotpKey::from_encoded_secret("JBSWY3DPEHPK3PXP");
assert_eq!("124851", key.get_code_at(|| 1650470683).unwrap());
}
}
}

View File

@ -32,14 +32,32 @@ impl TwoFactor {
}
}
pub fn description_str(&self) -> &'static str {
match self.kind {
TwoFactorType::TOTP(_) => "Login by entering an OTP code",
TwoFactorType::WEBAUTHN(_) => "Login using a security key",
}
}
pub fn type_image(&self) -> &'static str {
match self.kind {
TwoFactorType::TOTP(_) => "/assets/img/pin.svg",
TwoFactorType::WEBAUTHN(_) => "/assets/img/key.svg",
}
}
pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String {
match self.kind {
TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}",
self.id.0, redirect_uri.get_encoded()),
TwoFactorType::WEBAUTHN(_) => format!("/2fa_webauthn?id={}&redirect={}",
self.id.0, redirect_uri.get_encoded()),
TwoFactorType::TOTP(_) => format!("/2fa_otp?redirect={}", redirect_uri.get_encoded()),
TwoFactorType::WEBAUTHN(_) => {
format!("/2fa_webauthn?redirect={}", redirect_uri.get_encoded())
}
}
}
pub fn is_webauthn(&self) -> bool {
matches!(self.kind, TwoFactorType::WEBAUTHN(_))
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -71,7 +89,7 @@ impl User {
pub fn can_access_app(&self, id: &ClientID) -> bool {
match &self.authorized_clients {
None => true,
Some(c) => c.contains(id)
Some(c) => c.contains(id),
}
}
@ -94,6 +112,49 @@ impl User {
pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> {
self.two_factor.iter().find(|f| f.id.eq(factor_id))
}
pub fn has_webauthn_factor(&self) -> bool {
self.two_factor.iter().any(TwoFactor::is_webauthn)
}
/// Get all registered OTP registered passwords
pub fn get_otp_factors(&self) -> Vec<TotpKey> {
self.two_factor.iter().fold(vec![], |mut acc, factor| {
if let TwoFactorType::TOTP(key) = &factor.kind {
acc.push(key.clone())
}
acc
})
}
/// Get all registered 2FA webauthn public keys
pub fn get_webauthn_pub_keys(&self) -> Vec<WebauthnPubKey> {
self.two_factor.iter().fold(vec![], |mut acc, factor| {
if let TwoFactorType::WEBAUTHN(key) = &factor.kind {
acc.push(*key.clone())
}
acc
})
}
/// Get the first factor of each kind of factors
pub fn get_distinct_factors_types(&self) -> Vec<&TwoFactor> {
let mut urls = vec![];
self.two_factor
.iter()
.filter(|f| {
if urls.contains(&f.type_str()) {
false
} else {
urls.push(f.type_str());
true
}
})
.collect::<Vec<_>>()
}
}
impl PartialEq for User {
@ -157,8 +218,8 @@ impl EntityManager<User> {
/// Update user information
fn update_user<F>(&mut self, id: &UserID, update: F) -> bool
where
F: FnOnce(User) -> User,
where
F: FnOnce(User) -> User,
{
let user = match self.find_by_user_id(id) {
None => return false,

View File

@ -3,10 +3,15 @@ use std::sync::Arc;
use actix_web::web;
use uuid::Uuid;
use webauthn_rs::prelude::{
CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential,
RequestChallengeResponse,
};
use webauthn_rs::{Webauthn, WebauthnBuilder};
use webauthn_rs::prelude::{CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse};
use crate::constants::{APP_NAME, WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE};
use crate::constants::{
APP_NAME, WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE,
};
use crate::data::app_config::AppConfig;
use crate::data::crypto_wrapper::CryptoWrapper;
use crate::data::user::{User, UserID};
@ -42,7 +47,6 @@ struct AuthStateOpaqueData {
expire: u64,
}
pub type WebAuthManagerReq = web::Data<Arc<WebAuthManager>>;
pub struct WebAuthManager {
@ -54,24 +58,23 @@ impl WebAuthManager {
pub fn init(conf: &AppConfig) -> Self {
Self {
core: WebauthnBuilder::new(
conf.domain_name().split_once(':')
conf.domain_name()
.split_once(':')
.map(|s| s.0)
.unwrap_or_else(|| conf.domain_name()),
&url::Url::parse(&conf.website_origin)
.expect("Failed to parse configuration origin!"))
.expect("Invalid Webauthn configuration")
.rp_name(APP_NAME)
.build()
.expect("Failed to build webauthn")
,
.expect("Failed to parse configuration origin!"),
)
.expect("Invalid Webauthn configuration")
.rp_name(APP_NAME)
.build()
.expect("Failed to build webauthn"),
crypto_wrapper: CryptoWrapper::new_random(),
}
}
pub fn start_register(&self, user: &User) -> Res<RegisterKeyRequest> {
let (creation_challenge, registration_state)
= self.core.start_passkey_registration(
let (creation_challenge, registration_state) = self.core.start_passkey_registration(
Uuid::parse_str(&user.uid.0).expect("Failed to parse user id"),
&user.username,
&user.full_name(),
@ -88,29 +91,43 @@ impl WebAuthManager {
})
}
pub fn finish_registration(&self, user: &User, opaque_state: &str,
pub_cred: RegisterPublicKeyCredential) -> Res<WebauthnPubKey> {
pub fn finish_registration(
&self,
user: &User,
opaque_state: &str,
pub_cred: RegisterPublicKeyCredential,
) -> Res<WebauthnPubKey> {
let state: RegisterKeyOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
if state.user_id != user.uid {
return Err(Box::new(
std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Invalid user for pubkey!",
)));
}
if state.expire < time() {
return Err(Box::new(
std::io::Error::new(ErrorKind::Other, "Challenge has expired!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Challenge has expired!",
)));
}
let res = self.core
.finish_passkey_registration(&pub_cred, &serde_json::from_str(&state.registration_state)?)?;
let res = self.core.finish_passkey_registration(
&pub_cred,
&serde_json::from_str(&state.registration_state)?,
)?;
Ok(WebauthnPubKey { creds: res })
}
pub fn start_authentication(&self, user_id: &UserID, key: &WebauthnPubKey) -> Res<AuthRequest> {
let (login_challenge, authentication_state) = self.core.start_passkey_authentication(&vec![
key.creds.clone()
])?;
pub fn start_authentication(
&self,
user_id: &UserID,
keys: &[WebauthnPubKey],
) -> Res<AuthRequest> {
let (login_challenge, authentication_state) = self.core.start_passkey_authentication(
&keys.iter().map(|k| k.creds.clone()).collect::<Vec<_>>(),
)?;
Ok(AuthRequest {
opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData {
@ -122,22 +139,32 @@ impl WebAuthManager {
})
}
pub fn finish_authentication(&self, user_id: &UserID, opaque_state: &str,
pub_cred: &PublicKeyCredential) -> Res {
pub fn finish_authentication(
&self,
user_id: &UserID,
opaque_state: &str,
pub_cred: &PublicKeyCredential,
) -> Res {
let state: AuthStateOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
if &state.user_id != user_id {
return Err(Box::new(
std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Invalid user for pubkey!",
)));
}
if state.expire < time() {
return Err(Box::new(
std::io::Error::new(ErrorKind::Other, "Challenge has expired!")));
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Challenge has expired!",
)));
}
self.core.finish_passkey_authentication(pub_cred,
&serde_json::from_str(&state.authentication_state)?)?;
self.core.finish_passkey_authentication(
pub_cred,
&serde_json::from_str(&state.authentication_state)?,
)?;
Ok(())
}
}
}