Merge factors type for authentication

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

3
assets/img/key.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
<path fill="#FFFFFF" d="M14 27.4q-1.4 0-2.4-1t-1-2.4q0-1.4 1-2.4t2.4-1q1.4 0 2.4 1t1 2.4q0 1.4-1 2.4t-2.4 1Zm0 8.6q-5 0-8.5-3.5T2 24q0-5 3.5-8.5T14 12q3.6 0 6.3 1.7 2.7 1.7 4.25 5.15h17.8L48 24.5l-8.35 7.65-4.4-3.2-4.4 3.2-3.75-3h-2.55q-1.25 3-3.925 4.925Q17.95 36 14 36Zm0-3q2.9 0 5.35-1.925 2.45-1.925 3.15-4.925h5.7l2.7 2.25 4.4-3.15 4.1 3.1 4.25-3.95-2.55-2.55H22.5q-.6-2.8-3-4.825Q17.1 15 14 15q-3.75 0-6.375 2.625T5 24q0 3.75 2.625 6.375T14 33Z"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

3
assets/img/pin.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
<path fill="#FFFFFF" d="M7 40q-1.2 0-2.1-.9Q4 38.2 4 37V11q0-1.2.9-2.1Q5.8 8 7 8h34q1.2 0 2.1.9.9.9.9 2.1v26q0 1.2-.9 2.1-.9.9-2.1.9Zm5.35-9.7h2.1V17.7h-1.6L9.2 20.15l1.05 1.65 2.1-1.4Zm6.65 0h8.15v-1.9H21.7v-.1q.7-.6 1.525-1.325T24.7 25.6q1.05-1 1.65-1.925.6-.925.6-2.225 0-1.6-1.1-2.675Q24.75 17.7 23 17.7q-1.5 0-2.525.7t-1.525 1.85l1.9.9q.25-.55.85-.95.6-.4 1.3-.4.9 0 1.4.5.5.5.5 1.2 0 1-.5 1.675T23.05 24.6l-2.025 1.85q-.875.8-2.025 1.9Zm15.7 0q1.8 0 2.975-.875T38.85 26.7q0-1.25-.65-2.05-.65-.8-1.7-.9v-.05q.95-.3 1.425-.975.475-.675.475-1.775 0-1.5-1.075-2.375T34.65 17.7q-1.1 0-2.1.6t-1.65 1.75l1.7.9q.4-.55.925-.875.525-.325 1.125-.325.75 0 1.225.425.475.425.475 1.175 0 .85-.575 1.15-.575.3-1.525.3h-.7v1.95h.75q.95 0 1.725.5.775.5.775 1.45 0 .9-.575 1.375t-1.525.475q-.75 0-1.45-.5t-1-1.35l-1.75.65q.4 1.35 1.425 2.15 1.025.8 2.775.8ZM7 37h34V11H7v26Zm0 0V11v26Z"/>
</svg>

After

Width:  |  Height:  |  Size: 951 B

View File

@ -19,7 +19,6 @@ pub struct CountFailedAttempt {
pub ip: IpAddr, pub ip: IpAddr,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct BruteForceActor { pub struct BruteForceActor {
failed_attempts: HashMap<IpAddr, Vec<u64>>, failed_attempts: HashMap<IpAddr, Vec<u64>>,
@ -28,10 +27,7 @@ pub struct BruteForceActor {
impl BruteForceActor { impl BruteForceActor {
pub fn clean_attempts(&mut self) { pub fn clean_attempts(&mut self) {
#[allow(clippy::map_clone)] #[allow(clippy::map_clone)]
let keys = self.failed_attempts let keys = self.failed_attempts.keys().map(|i| *i).collect::<Vec<_>>();
.keys()
.map(|i| *i)
.collect::<Vec<_>>();
for ip in keys { for ip in keys {
// Remove old attempts // Remove old attempts
@ -102,7 +98,9 @@ mod test {
let mut actor = BruteForceActor::default(); let mut actor = BruteForceActor::default();
actor.failed_attempts.insert(IP_1, vec![1, 10]); actor.failed_attempts.insert(IP_1, vec![1, 10]);
actor.failed_attempts.insert(IP_2, vec![1, 10, time() + 10]); actor.failed_attempts.insert(IP_2, vec![1, 10, time() + 10]);
actor.failed_attempts.insert(IP_3, vec![time() + 10, time() + 20]); actor
.failed_attempts
.insert(IP_3, vec![time() + 10, time() + 20]);
actor.clean_attempts(); actor.clean_attempts();

View File

@ -1,3 +1,3 @@
pub mod users_actor;
pub mod bruteforce_actor; pub mod bruteforce_actor;
pub mod openid_sessions_actor; pub mod openid_sessions_actor;
pub mod users_actor;

View File

@ -1,5 +1,5 @@
use actix::{Actor, AsyncContext, Context, Handler};
use actix::Message; use actix::Message;
use actix::{Actor, AsyncContext, Context, Handler};
use crate::constants::*; use crate::constants::*;
use crate::data::access_token::AccessToken; use crate::data::access_token::AccessToken;
@ -37,13 +37,16 @@ pub struct Session {
impl Session { impl Session {
pub fn is_expired(&self) -> bool { pub fn is_expired(&self) -> bool {
self.authorization_code_expire_at < time() && self.access_token_expire_at < time() self.authorization_code_expire_at < time()
&& self.access_token_expire_at < time()
&& self.refresh_token_expire_at < time() && self.refresh_token_expire_at < time()
} }
pub fn regenerate_access_and_refresh_tokens(&mut self, pub fn regenerate_access_and_refresh_tokens(
app_config: &AppConfig, &mut self,
jwt_signer: &JWTSigner) -> Res { app_config: &AppConfig,
jwt_signer: &JWTSigner,
) -> Res {
let access_token = AccessToken { let access_token = AccessToken {
issuer: app_config.website_origin.to_string(), issuer: app_config.website_origin.to_string(),
subject_identifier: self.user.clone().0, subject_identifier: self.user.clone().0,
@ -116,7 +119,11 @@ impl Handler<PushNewSession> for OpenIDSessionsActor {
impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor { impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
type Result = Option<Session>; type Result = Option<Session>;
fn handle(&mut self, msg: FindSessionByAuthorizationCode, _ctx: &mut Self::Context) -> Self::Result { fn handle(
&mut self,
msg: FindSessionByAuthorizationCode,
_ctx: &mut Self::Context,
) -> Self::Result {
self.session self.session
.iter() .iter()
.find(|f| f.authorization_code.eq(&msg.0)) .find(|f| f.authorization_code.eq(&msg.0))
@ -141,7 +148,12 @@ impl Handler<FindSessionByAccessToken> for OpenIDSessionsActor {
fn handle(&mut self, msg: FindSessionByAccessToken, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: FindSessionByAccessToken, _ctx: &mut Self::Context) -> Self::Result {
self.session self.session
.iter() .iter()
.find(|f| f.access_token.as_ref().map(|t| t.eq(&msg.0)).unwrap_or(false)) .find(|f| {
f.access_token
.as_ref()
.map(|t| t.eq(&msg.0))
.unwrap_or(false)
})
.cloned() .cloned()
} }
} }
@ -150,8 +162,13 @@ impl Handler<UpdateSession> for OpenIDSessionsActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result {
if let Some(r) = self.session.iter().enumerate() if let Some(r) = self
.find(|f| f.1.session_id.eq(&msg.0.session_id)).map(|f| f.0) { .session
.iter()
.enumerate()
.find(|f| f.1.session_id.eq(&msg.0.session_id))
.map(|f| f.0)
{
self.session[r] = msg.0; self.session[r] = msg.0;
} }
} }

View File

@ -123,7 +123,9 @@ impl Handler<FindUserByUsername> for UsersActor {
type Result = MessageResult<FindUserByUsername>; type Result = MessageResult<FindUserByUsername>;
fn handle(&mut self, msg: FindUserByUsername, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: FindUserByUsername, _ctx: &mut Self::Context) -> Self::Result {
MessageResult(FindUserByUsernameResult(self.manager.find_by_username_or_email(&msg.0))) MessageResult(FindUserByUsernameResult(
self.manager.find_by_username_or_email(&msg.0),
))
} }
} }
@ -155,10 +157,13 @@ impl Handler<DeleteUserRequest> for UsersActor {
fn handle(&mut self, msg: DeleteUserRequest, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteUserRequest, _ctx: &mut Self::Context) -> Self::Result {
let user = match self.manager.find_by_user_id(&msg.0) { let user = match self.manager.find_by_user_id(&msg.0) {
None => { None => {
log::warn!("Could not delete account {:?} because it was not found!", msg.0); log::warn!(
"Could not delete account {:?} because it was not found!",
msg.0
);
return MessageResult(DeleteUserResult(false)); return MessageResult(DeleteUserResult(false));
} }
Some(s) => s Some(s) => s,
}; };
MessageResult(DeleteUserResult(match self.manager.remove(&user) { MessageResult(DeleteUserResult(match self.manager.remove(&user) {

View File

@ -1,5 +1,5 @@
use actix::Addr; use actix::Addr;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor}; use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor};
use crate::data::current_user::CurrentUser; use crate::data::current_user::CurrentUser;
@ -15,10 +15,16 @@ struct FindUserResult {
user_id: Option<String>, user_id: Option<String>,
} }
pub async fn find_username(req: web::Form<FindUserNameReq>, users: web::Data<Addr<UsersActor>>) -> impl Responder { pub async fn find_username(
let res = users.send(FindUserByUsername(req.0.username)).await.unwrap(); req: web::Form<FindUserNameReq>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let res = users
.send(FindUserByUsername(req.0.username))
.await
.unwrap();
HttpResponse::Ok().json(FindUserResult { HttpResponse::Ok().json(FindUserResult {
user_id: res.0.map(|r| r.uid.0) user_id: res.0.map(|r| r.uid.0),
}) })
} }
@ -27,9 +33,11 @@ pub struct DeleteUserReq {
user_id: UserID, user_id: UserID,
} }
pub async fn delete_user(
pub async fn delete_user(user: CurrentUser, req: web::Form<DeleteUserReq>, user: CurrentUser,
users: web::Data<Addr<UsersActor>>) -> impl Responder { req: web::Form<DeleteUserReq>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
if user.uid == req.user_id { if user.uid == req.user_id {
return HttpResponse::BadRequest().body("You can not remove your own account!"); return HttpResponse::BadRequest().body("You can not remove your own account!");
} }
@ -41,4 +49,3 @@ pub async fn delete_user(user: CurrentUser, req: web::Form<DeleteUserReq>,
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -1,7 +1,7 @@
use std::ops::Deref; use std::ops::Deref;
use actix::Addr; use actix::Addr;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use askama::Template; use askama::Template;
use crate::actors::users_actor; use crate::actors::users_actor;
@ -35,17 +35,15 @@ struct EditUserTemplate {
clients: Vec<Client>, clients: Vec<Client>,
} }
pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder { pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
HttpResponse::Ok().body(ClientsListTemplate { HttpResponse::Ok().body(
_p: BaseSettingsPage::get( ClientsListTemplate {
"Clients list", _p: BaseSettingsPage::get("Clients list", &user, None, None),
&user, clients: clients.cloned(),
None, }
None, .render()
), .unwrap(),
clients: clients.cloned(), )
}.render().unwrap())
} }
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
@ -63,13 +61,20 @@ pub struct UpdateUserQuery {
two_factor: String, two_factor: String,
} }
pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>, update_query: Option<web::Form<UpdateUserQuery>>) -> impl Responder { pub async fn users_route(
user: CurrentUser,
users: web::Data<Addr<UsersActor>>,
update_query: Option<web::Form<UpdateUserQuery>>,
) -> impl Responder {
let mut danger = None; let mut danger = None;
let mut success = None; let mut success = None;
if let Some(update) = update_query { if let Some(update) = update_query {
let current_user: Option<User> = users.send(users_actor::FindUserByUsername(update.username.to_string())) let current_user: Option<User> = users
.await.unwrap().0; .send(users_actor::FindUserByUsername(update.username.to_string()))
.await
.unwrap()
.0;
let is_creating = current_user.is_none(); let is_creating = current_user.is_none();
let mut user = current_user.unwrap_or_default(); let mut user = current_user.unwrap_or_default();
@ -82,67 +87,84 @@ pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>,
user.admin = update.0.admin.is_some(); user.admin = update.0.admin.is_some();
let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>(); let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>();
user.two_factor.retain(|f| factors_to_keep.contains(&f.id.0.as_str())); user.two_factor
.retain(|f| factors_to_keep.contains(&f.id.0.as_str()));
user.authorized_clients = match update.0.grant_type.as_str() { user.authorized_clients = match update.0.grant_type.as_str() {
"all_clients" => None, "all_clients" => None,
"custom_clients" => Some(update.0.granted_clients.split(',') "custom_clients" => Some(
.map(|c| ClientID(c.to_string())) update
.collect::<Vec<_>>()), .0
_ => Some(Vec::new()) .granted_clients
.split(',')
.map(|c| ClientID(c.to_string()))
.collect::<Vec<_>>(),
),
_ => Some(Vec::new()),
}; };
let new_password = match update.0.gen_new_password.is_some() { let new_password = match update.0.gen_new_password.is_some() {
false => None, false => None,
true => { true => {
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN); let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
user.password = hash_password(&temp_pass) user.password = hash_password(&temp_pass).expect("Failed to hash password");
.expect("Failed to hash password");
user.need_reset_password = true; user.need_reset_password = true;
Some(temp_pass) Some(temp_pass)
} }
}; };
let res = users.send(users_actor::UpdateUserRequest(user.clone())).await.unwrap().0; let res = users
.send(users_actor::UpdateUserRequest(user.clone()))
.await
.unwrap()
.0;
if !res { if !res {
danger = Some(match is_creating { danger = Some(
true => "Failed to create user!", match is_creating {
false => "Failed to update user!" true => "Failed to create user!",
}.to_string()) false => "Failed to update user!",
}
.to_string(),
)
} else { } else {
success = Some(match is_creating { success = Some(match is_creating {
true => format!("User {} was successfully created!", user.full_name()), true => format!("User {} was successfully created!", user.full_name()),
false => format!("User {} was successfully updated!", user.full_name()) false => format!("User {} was successfully updated!", user.full_name()),
}); });
if let Some(pass) = new_password { if let Some(pass) = new_password {
danger = Some(format!("{}'s temporary password is {}", user.full_name(), pass)); danger = Some(format!(
"{}'s temporary password is {}",
user.full_name(),
pass
));
} }
} }
} }
let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0; let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0;
HttpResponse::Ok().body(UsersListTemplate { HttpResponse::Ok().body(
_p: BaseSettingsPage::get( UsersListTemplate {
"Users list", _p: BaseSettingsPage::get("Users list", &user, danger, success),
&user, users,
danger, }
success, .render()
), .unwrap(),
users, )
}.render().unwrap())
} }
pub async fn create_user(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder { pub async fn create_user(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
HttpResponse::Ok().body(EditUserTemplate { HttpResponse::Ok().body(
_p: BaseSettingsPage::get("Create a new user", user.deref(), None, None), EditUserTemplate {
u: Default::default(), _p: BaseSettingsPage::get("Create a new user", user.deref(), None, None),
clients: clients.cloned(), u: Default::default(),
}.render().unwrap()) clients: clients.cloned(),
}
.render()
.unwrap(),
)
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -150,26 +172,33 @@ pub struct EditUserQuery {
id: UserID, id: UserID,
} }
pub async fn edit_user(user: CurrentUser, pub async fn edit_user(
clients: web::Data<ClientManager>, user: CurrentUser,
users: web::Data<Addr<UsersActor>>, clients: web::Data<ClientManager>,
query: web::Query<EditUserQuery>, users: web::Data<Addr<UsersActor>>,
query: web::Query<EditUserQuery>,
) -> impl Responder { ) -> impl Responder {
let edited_account = users.send(users_actor::GetUserRequest(query.0.id)) let edited_account = users
.await.unwrap().0; .send(users_actor::GetUserRequest(query.0.id))
.await
.unwrap()
.0;
HttpResponse::Ok().body(
HttpResponse::Ok().body(EditUserTemplate { EditUserTemplate {
_p: BaseSettingsPage::get( _p: BaseSettingsPage::get(
"Edit user account", "Edit user account",
user.deref(), user.deref(),
match edited_account.is_none() { match edited_account.is_none() {
true => Some("Could not find requested user!".to_string()), true => Some("Could not find requested user!".to_string()),
false => None false => None,
}, },
None, None,
), ),
u: edited_account.unwrap_or_default(), u: edited_account.unwrap_or_default(),
clients: clients.cloned(), clients: clients.cloned(),
}.render().unwrap()) }
.render()
.unwrap(),
)
} }

View File

@ -1,7 +1,7 @@
use std::path::Path; use std::path::Path;
use actix_web::{HttpResponse, web}; use actix_web::{web, HttpResponse};
use include_dir::{Dir, include_dir}; use include_dir::{include_dir, Dir};
/// Assets directory /// Assets directory
static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets"); static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets");

View File

@ -1,5 +1,5 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::{web, HttpRequest, HttpResponse, Responder};
use webauthn_rs::prelude::PublicKeyCredential; use webauthn_rs::prelude::PublicKeyCredential;
use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::session_identity::{SessionIdentity, SessionStatus};
@ -11,10 +11,12 @@ pub struct AuthWebauthnRequest {
credential: PublicKeyCredential, credential: PublicKeyCredential,
} }
pub async fn auth_webauthn(id: Identity, pub async fn auth_webauthn(
req: web::Json<AuthWebauthnRequest>, id: Identity,
manager: WebAuthManagerReq, req: web::Json<AuthWebauthnRequest>,
http_req: HttpRequest) -> impl Responder { manager: WebAuthManagerReq,
http_req: HttpRequest,
) -> impl Responder {
if !SessionIdentity(Some(&id)).need_2fa_auth() { if !SessionIdentity(Some(&id)).need_2fa_auth() {
return HttpResponse::Unauthorized().json("No 2FA required!"); return HttpResponse::Unauthorized().json("No 2FA required!");
} }

View File

@ -1,17 +1,19 @@
use actix::Addr; use actix::Addr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::{web, HttpRequest, HttpResponse, Responder};
use askama::Template; use askama::Template;
use crate::actors::{bruteforce_actor, users_actor};
use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::bruteforce_actor::BruteForceActor;
use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor}; use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor};
use crate::actors::{bruteforce_actor, users_actor};
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user, redirect_user_for_login}; use crate::controllers::base_controller::{
build_fatal_error_page, redirect_user, redirect_user_for_login,
};
use crate::data::login_redirect::LoginRedirect; use crate::data::login_redirect::LoginRedirect;
use crate::data::remote_ip::RemoteIP; use crate::data::remote_ip::RemoteIP;
use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::session_identity::{SessionIdentity, SessionStatus};
use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; use crate::data::user::User;
use crate::data::webauthn_manager::WebAuthManagerReq; use crate::data::webauthn_manager::WebAuthManagerReq;
struct BaseLoginPage<'a> { struct BaseLoginPage<'a> {
@ -40,26 +42,23 @@ struct PasswordResetTemplate<'a> {
#[template(path = "login/choose_second_factor.html")] #[template(path = "login/choose_second_factor.html")]
struct ChooseSecondFactorTemplate<'a> { struct ChooseSecondFactorTemplate<'a> {
_p: BaseLoginPage<'a>, _p: BaseLoginPage<'a>,
factors: &'a [TwoFactor], user: &'a User,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "login/opt_input.html")] #[template(path = "login/otp_input.html")]
struct LoginWithOTPTemplate<'a> { struct LoginWithOTPTemplate<'a> {
_p: BaseLoginPage<'a>, _p: BaseLoginPage<'a>,
factor: &'a TwoFactor,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "login/webauthn_input.html")] #[template(path = "login/webauthn_input.html")]
struct LoginWithWebauthnTemplate<'a> { struct LoginWithWebauthnTemplate<'a> {
_p: BaseLoginPage<'a>, _p: BaseLoginPage<'a>,
factor: &'a TwoFactor,
opaque_state: String, opaque_state: String,
challenge_json: String, challenge_json: String,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct LoginRequestBody { pub struct LoginRequestBody {
login: String, login: String,
@ -87,13 +86,17 @@ pub async fn login_route(
let mut success = None; let mut success = None;
let mut login = String::new(); let mut login = String::new();
let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() }) let failed_attempts = bruteforce
.await.unwrap(); .send(bruteforce_actor::CountFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS { if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
return HttpResponse::TooManyRequests().body( return HttpResponse::TooManyRequests().body(build_fatal_error_page(
build_fatal_error_page("Too many failed login attempts, please try again later!") "Too many failed login attempts, please try again later!",
); ));
} }
// Check if user session must be closed // Check if user session must be closed
@ -103,22 +106,24 @@ pub async fn login_route(
} }
success = Some("Goodbye!".to_string()); success = Some("Goodbye!".to_string());
} }
// Check if user is already authenticated // Check if user is already authenticated
else if SessionIdentity(id.as_ref()).is_authenticated() { else if SessionIdentity(id.as_ref()).is_authenticated() {
return redirect_user(query.redirect.get()); return redirect_user(query.redirect.get());
} }
// Check if the password of the user has to be changed // Check if the password of the user has to be changed
else if SessionIdentity(id.as_ref()).need_new_password() { else if SessionIdentity(id.as_ref()).need_new_password() {
return redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded())); return redirect_user(&format!(
"/reset_password?redirect={}",
query.redirect.get_encoded()
));
} }
// Check if the user has to valide a second factor // Check if the user has to valide a second factor
else if SessionIdentity(id.as_ref()).need_2fa_auth() { else if SessionIdentity(id.as_ref()).need_2fa_auth() {
return redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded())); return redirect_user(&format!(
"/2fa_auth?redirect={}",
query.redirect.get_encoded()
));
} }
// Try to authenticate user // Try to authenticate user
else if let Some(req) = &req { else if let Some(req) = &req {
login = req.login.clone(); login = req.login.clone();
@ -150,10 +155,20 @@ pub async fn login_route(
} }
c => { c => {
log::warn!("Failed login for ip {:?} / username {}: {:?}", remote_ip, login, c); log::warn!(
"Failed login for ip {:?} / username {}: {:?}",
remote_ip,
login,
c
);
danger = Some("Login failed.".to_string()); danger = Some("Login failed.".to_string());
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap(); bruteforce
.send(bruteforce_actor::RecordFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
} }
} }
} }
@ -169,8 +184,8 @@ pub async fn login_route(
}, },
login, login,
} }
.render() .render()
.unwrap(), .unwrap(),
) )
} }
@ -191,10 +206,13 @@ pub struct PasswordResetQuery {
} }
/// Reset user password route /// Reset user password route
pub async fn reset_password_route(id: Option<Identity>, query: web::Query<PasswordResetQuery>, pub async fn reset_password_route(
req: Option<web::Form<ChangePasswordRequestBody>>, id: Option<Identity>,
users: web::Data<Addr<UsersActor>>, query: web::Query<PasswordResetQuery>,
http_req: HttpRequest) -> impl Responder { req: Option<web::Form<ChangePasswordRequestBody>>,
users: web::Data<Addr<UsersActor>>,
http_req: HttpRequest,
) -> impl Responder {
let mut danger = None; let mut danger = None;
if !SessionIdentity(id.as_ref()).need_new_password() { if !SessionIdentity(id.as_ref()).need_new_password() {
@ -235,8 +253,8 @@ pub async fn reset_password_route(id: Option<Identity>, query: web::Query<Passwo
}, },
min_pass_len: MIN_PASS_LEN, min_pass_len: MIN_PASS_LEN,
} }
.render() .render()
.unwrap(), .unwrap(),
) )
} }
@ -249,18 +267,27 @@ pub struct ChooseSecondFactorQuery {
} }
/// Let the user select the factor to use to authenticate /// Let the user select the factor to use to authenticate
pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSecondFactorQuery>, pub async fn choose_2fa_method(
users: web::Data<Addr<UsersActor>>) -> impl Responder { id: Option<Identity>,
query: web::Query<ChooseSecondFactorQuery>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
if !SessionIdentity(id.as_ref()).need_2fa_auth() { if !SessionIdentity(id.as_ref()).need_2fa_auth() {
log::trace!("User does not require 2fa auth, redirecting"); log::trace!("User does not require 2fa auth, redirecting");
return redirect_user_for_login(query.redirect.get()); return redirect_user_for_login(query.redirect.get());
} }
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id())) let user: User = users
.await.unwrap().0.expect("Could not find user!"); .send(users_actor::GetUserRequest(
SessionIdentity(id.as_ref()).user_id(),
))
.await
.unwrap()
.0
.expect("Could not find user!");
// Automatically choose factor if there is only one factor // Automatically choose factor if there is only one factor
if user.two_factor.len() == 1 && !query.force_display { if user.get_distinct_factors_types().len() == 1 && !query.force_display {
log::trace!("User has only one factor, using it by default"); log::trace!("User has only one factor, using it by default");
return redirect_user(&user.two_factor[0].login_url(&query.redirect)); return redirect_user(&user.two_factor[0].login_url(&query.redirect));
} }
@ -274,10 +301,10 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
app_name: APP_NAME, app_name: APP_NAME,
redirect_uri: &query.redirect, redirect_uri: &query.redirect,
}, },
factors: &user.two_factor, user: &user,
} }
.render() .render()
.unwrap(), .unwrap(),
) )
} }
@ -285,7 +312,6 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
pub struct LoginWithOTPQuery { pub struct LoginWithOTPQuery {
#[serde(default)] #[serde(default)]
redirect: LoginRedirect, redirect: LoginRedirect,
id: FactorID,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -293,35 +319,39 @@ pub struct LoginWithOTPForm {
code: String, code: String,
} }
/// Login with OTP /// Login with OTP
pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTPQuery>, pub async fn login_with_otp(
form: Option<web::Form<LoginWithOTPForm>>, id: Option<Identity>,
users: web::Data<Addr<UsersActor>>, query: web::Query<LoginWithOTPQuery>,
http_req: HttpRequest) -> impl Responder { form: Option<web::Form<LoginWithOTPForm>>,
users: web::Data<Addr<UsersActor>>,
http_req: HttpRequest,
) -> impl Responder {
let mut danger = None; let mut danger = None;
if !SessionIdentity(id.as_ref()).need_2fa_auth() { if !SessionIdentity(id.as_ref()).need_2fa_auth() {
return redirect_user_for_login(query.redirect.get()); return redirect_user_for_login(query.redirect.get());
} }
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id())) let user: User = users
.await.unwrap().0.expect("Could not find user!"); .send(users_actor::GetUserRequest(
SessionIdentity(id.as_ref()).user_id(),
))
.await
.unwrap()
.0
.expect("Could not find user!");
let factor = match user.find_factor(&query.id) { let keys = user.get_otp_factors();
Some(f) => f, if keys.is_empty() {
None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!")) return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"));
}; }
let key = match &factor.kind {
TwoFactorType::TOTP(key) => key,
_ => {
return HttpResponse::Ok().body(build_fatal_error_page("Factor is not a TOTP key!"));
}
};
if let Some(form) = form { if let Some(form) = form {
if !key.check_code(&form.code).unwrap_or(false) { if !keys
.iter()
.any(|k| k.check_code(&form.code).unwrap_or(false))
{
danger = Some("Specified code is invalid!".to_string()); danger = Some("Specified code is invalid!".to_string());
} else { } else {
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn); SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
@ -329,56 +359,60 @@ pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTP
} }
} }
HttpResponse::Ok().body(LoginWithOTPTemplate { HttpResponse::Ok().body(
_p: BaseLoginPage { LoginWithOTPTemplate {
danger, _p: BaseLoginPage {
success: None, danger,
page_title: "Two-Factor Auth", success: None,
app_name: APP_NAME, page_title: "Two-Factor Auth",
redirect_uri: &query.redirect, app_name: APP_NAME,
}, redirect_uri: &query.redirect,
factor, },
}.render().unwrap()) }
.render()
.unwrap(),
)
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct LoginWithWebauthnQuery { pub struct LoginWithWebauthnQuery {
#[serde(default)] #[serde(default)]
redirect: LoginRedirect, redirect: LoginRedirect,
id: FactorID,
} }
/// Login with Webauthn /// Login with Webauthn
pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWithWebauthnQuery>, pub async fn login_with_webauthn(
manager: WebAuthManagerReq, id: Option<Identity>,
users: web::Data<Addr<UsersActor>>) -> impl Responder { query: web::Query<LoginWithWebauthnQuery>,
manager: WebAuthManagerReq,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
if !SessionIdentity(id.as_ref()).need_2fa_auth() { if !SessionIdentity(id.as_ref()).need_2fa_auth() {
return redirect_user_for_login(query.redirect.get()); return redirect_user_for_login(query.redirect.get());
} }
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id())) let user: User = users
.await.unwrap().0.expect("Could not find user!"); .send(users_actor::GetUserRequest(
SessionIdentity(id.as_ref()).user_id(),
))
.await
.unwrap()
.0
.expect("Could not find user!");
let factor = match user.find_factor(&query.id) { let pub_keys = user.get_webauthn_pub_keys();
Some(f) => f, if pub_keys.is_empty() {
None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!")) return HttpResponse::Ok()
}; .body(build_fatal_error_page("No Webauthn public key registered!"));
}
let key = match &factor.kind { let challenge = match manager.start_authentication(&user.uid, &pub_keys) {
TwoFactorType::WEBAUTHN(key) => key,
_ => {
return HttpResponse::Ok()
.body(build_fatal_error_page("Factor is not a Webauthn key!"));
}
};
let challenge = match manager.start_authentication(&user.uid, key) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
log::error!("Failed to generate webauthn challenge! {:?}", e); log::error!("Failed to generate webauthn challenge! {:?}", e);
return HttpResponse::InternalServerError() return HttpResponse::InternalServerError().body(build_fatal_error_page(
.body(build_fatal_error_page("Failed to generate webauthn challenge")); "Failed to generate webauthn challenge",
));
} }
}; };
@ -390,16 +424,19 @@ pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWi
} }
}; };
HttpResponse::Ok().body(LoginWithWebauthnTemplate { HttpResponse::Ok().body(
_p: BaseLoginPage { LoginWithWebauthnTemplate {
danger: None, _p: BaseLoginPage {
success: None, danger: None,
page_title: "Two-Factor Auth", success: None,
app_name: APP_NAME, page_title: "Two-Factor Auth",
redirect_uri: &query.redirect, app_name: APP_NAME,
}, redirect_uri: &query.redirect,
factor, },
opaque_state: challenge.opaque_state, opaque_state: challenge.opaque_state,
challenge_json: urlencoding::encode(&challenge_json).to_string(), challenge_json: urlencoding::encode(&challenge_json).to_string(),
}.render().unwrap()) }
.render()
.unwrap(),
)
} }

View File

@ -1,10 +1,10 @@
pub mod admin_api;
pub mod admin_controller;
pub mod assets_controller; pub mod assets_controller;
pub mod base_controller; pub mod base_controller;
pub mod login_controller;
pub mod login_api; pub mod login_api;
pub mod settings_controller; pub mod login_controller;
pub mod admin_controller;
pub mod admin_api;
pub mod openid_controller; pub mod openid_controller;
pub mod two_factors_controller; pub mod settings_controller;
pub mod two_factor_api; pub mod two_factor_api;
pub mod two_factors_controller;

View File

@ -2,12 +2,12 @@ use std::fmt::Debug;
use actix::Addr; use actix::Addr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use actix_web::error::ErrorUnauthorized; use actix_web::error::ErrorUnauthorized;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use crate::actors::{openid_sessions_actor, users_actor};
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID}; use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::UsersActor;
use crate::actors::{openid_sessions_actor, users_actor};
use crate::constants::*; use crate::constants::*;
use crate::controllers::base_controller::build_fatal_error_page; use crate::controllers::base_controller::build_fatal_error_page;
use crate::data::app_config::AppConfig; use crate::data::app_config::AppConfig;
@ -15,7 +15,7 @@ use crate::data::client::{ClientID, ClientManager};
use crate::data::code_challenge::CodeChallenge; use crate::data::code_challenge::CodeChallenge;
use crate::data::current_user::CurrentUser; use crate::data::current_user::CurrentUser;
use crate::data::id_token::IdToken; use crate::data::id_token::IdToken;
use crate::data::jwt_signer::{JsonWebKey, JWTSigner}; use crate::data::jwt_signer::{JWTSigner, JsonWebKey};
use crate::data::open_id_user_info::OpenIDUserInfo; use crate::data::open_id_user_info::OpenIDUserInfo;
use crate::data::openid_config::OpenIDConfig; use crate::data::openid_config::OpenIDConfig;
use crate::data::session_identity::SessionIdentity; use crate::data::session_identity::SessionIdentity;
@ -24,7 +24,9 @@ use crate::utils::string_utils::rand_str;
use crate::utils::time::time; use crate::utils::time::time;
pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>) -> impl Responder { pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>) -> impl Responder {
let is_secure_request = req.headers().get("HTTP_X_FORWARDED_PROTO") let is_secure_request = req
.headers()
.get("HTTP_X_FORWARDED_PROTO")
.map(|v| v.to_str().unwrap_or_default().to_lowercase().eq("https")) .map(|v| v.to_str().unwrap_or_default().to_lowercase().eq("https"))
.unwrap_or(false); .unwrap_or(false);
@ -33,10 +35,14 @@ pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>)
Some(s) => s.to_str().unwrap_or_default(), Some(s) => s.to_str().unwrap_or_default(),
}; };
let curr_origin = format!("{}://{}", match is_secure_request { let curr_origin = format!(
true => "https", "{}://{}",
false => "http" match is_secure_request {
}, host); true => "https",
false => "http",
},
host
);
HttpResponse::Ok().json(OpenIDConfig { HttpResponse::Ok().json(OpenIDConfig {
issuer: app_conf.website_origin.clone(), issuer: app_conf.website_origin.clone(),
@ -80,35 +86,43 @@ pub struct AuthorizeQuery {
} }
fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse { fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse {
log::warn!("Failed to process sign in request ({} => {}): {:?}", error, description, query); log::warn!(
"Failed to process sign in request ({} => {}): {:?}",
error,
description,
query
);
HttpResponse::Found() HttpResponse::Found()
.append_header( .append_header((
("Location", format!( "Location",
format!(
"{}?error={}?error_description={}&state={}", "{}?error={}?error_description={}&state={}",
query.redirect_uri, query.redirect_uri,
urlencoding::encode(error), urlencoding::encode(error),
urlencoding::encode(description), urlencoding::encode(description),
urlencoding::encode(&query.state) urlencoding::encode(&query.state)
)) ),
) ))
.finish() .finish()
} }
pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<AuthorizeQuery>, pub async fn authorize(
clients: web::Data<ClientManager>, user: CurrentUser,
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> impl Responder { id: Identity,
query: web::Query<AuthorizeQuery>,
clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
) -> impl Responder {
let client = match clients.find_by_id(&query.client_id) { let client = match clients.find_by_id(&query.client_id) {
None => { None => {
return HttpResponse::BadRequest() return HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!"));
.body(build_fatal_error_page("Client is invalid!"));
} }
Some(c) => c Some(c) => c,
}; };
let redirect_uri = query.redirect_uri.trim().to_string(); let redirect_uri = query.redirect_uri.trim().to_string();
if !redirect_uri.starts_with(&client.redirect_uri) { if !redirect_uri.starts_with(&client.redirect_uri) {
return HttpResponse::BadRequest() return HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!"));
.body(build_fatal_error_page("Redirect URI is invalid!"));
} }
if !query.scope.split(' ').any(|x| x == "openid") { if !query.scope.split(' ').any(|x| x == "openid") {
@ -116,7 +130,11 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
} }
if !query.response_type.eq("code") { if !query.response_type.eq("code") {
return error_redirect(&query, "invalid_request", "Only code response type is supported!"); return error_redirect(
&query,
"invalid_request",
"Only code response type is supported!",
);
} }
if query.state.is_empty() { if query.state.is_empty() {
@ -127,18 +145,27 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
Some(chal) => { Some(chal) => {
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain"); let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
if !meth.eq("S256") && !meth.eq("plain") { if !meth.eq("S256") && !meth.eq("plain") {
return error_redirect(&query, "invalid_request", return error_redirect(
"Only S256 and plain code challenge methods are supported!"); &query,
"invalid_request",
"Only S256 and plain code challenge methods are supported!",
);
} }
Some(CodeChallenge { code_challenge: chal, code_challenge_method: meth.to_string() }) Some(CodeChallenge {
code_challenge: chal,
code_challenge_method: meth.to_string(),
})
} }
_ => None _ => None,
}; };
// Check if user is authorized to access the application // Check if user is authorized to access the application
if !user.can_access_app(&client.id) { if !user.can_access_app(&client.id) {
return error_redirect(&query, "invalid_request", return error_redirect(
"User is not authorized to access this application!"); &query,
"invalid_request",
"User is not authorized to access this application!",
);
} }
// Save all authentication information in memory // Save all authentication information in memory
@ -157,18 +184,25 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
nonce: query.0.nonce, nonce: query.0.nonce,
code_challenge, code_challenge,
}; };
sessions.send(openid_sessions_actor::PushNewSession(session.clone())).await.unwrap(); sessions
.send(openid_sessions_actor::PushNewSession(session.clone()))
.await
.unwrap();
log::trace!("New OpenID session: {:#?}", session); log::trace!("New OpenID session: {:#?}", session);
HttpResponse::Found() HttpResponse::Found()
.append_header(("Location", format!( .append_header((
"{}?state={}&session_state={}&code={}", "Location",
session.redirect_uri, format!(
urlencoding::encode(&query.0.state), "{}?state={}&session_state={}&code={}",
urlencoding::encode(&session.session_id.0), session.redirect_uri,
urlencoding::encode(&session.authorization_code) urlencoding::encode(&query.0.state),
))).finish() urlencoding::encode(&session.session_id.0),
urlencoding::encode(&session.authorization_code)
),
))
.finish()
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@ -178,12 +212,16 @@ struct ErrorResponse {
} }
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse { pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
log::warn!("request failed: {} - {} => '{:#?}'", error, description, query); log::warn!(
HttpResponse::BadRequest() "request failed: {} - {} => '{:#?}'",
.json(ErrorResponse { error,
error: error.to_string(), description,
error_description: description.to_string(), query
}) );
HttpResponse::BadRequest().json(ErrorResponse {
error: error.to_string(),
error_description: description.to_string(),
})
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
@ -198,7 +236,6 @@ pub struct TokenRefreshTokenQuery {
refresh_token: String, refresh_token: String,
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub struct TokenQuery { pub struct TokenQuery {
grant_type: String, grant_type: String,
@ -222,115 +259,175 @@ pub struct TokenResponse {
id_token: Option<String>, id_token: Option<String>,
} }
pub async fn token(req: HttpRequest, pub async fn token(
query: web::Form<TokenQuery>, req: HttpRequest,
clients: web::Data<ClientManager>, query: web::Form<TokenQuery>,
app_config: web::Data<AppConfig>, clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>, app_config: web::Data<AppConfig>,
users: web::Data<Addr<UsersActor>>, sessions: web::Data<Addr<OpenIDSessionsActor>>,
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> { users: web::Data<Addr<UsersActor>>,
jwt_signer: web::Data<JWTSigner>,
) -> actix_web::Result<HttpResponse> {
// Extraction authentication information // Extraction authentication information
let authorization_header = req.headers().get("authorization"); let authorization_header = req.headers().get("authorization");
let (client_id, client_secret) = match (&query.client_id, &query.client_secret, authorization_header) { let (client_id, client_secret) =
// post authentication match (&query.client_id, &query.client_secret, authorization_header) {
(Some(client_id), Some(client_secret), None) => { // post authentication
(client_id.clone(), client_secret.to_string()) (Some(client_id), Some(client_secret), None) => {
} (client_id.clone(), client_secret.to_string())
// Basic authentication
(None, None, Some(v)) => {
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
None => {
return Ok(error_response(
&query,
"invalid_request",
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
));
}
Some(v) => v
};
let decode = String::from_utf8_lossy(&match base64::decode(token) {
Ok(d) => d,
Err(e) => {
log::error!("Failed to decode authorization header: {:?}", e);
return Ok(error_response(&query, "invalid_request", "Failed to decode authorization header!"));
}
}).to_string();
match decode.split_once(':') {
None => (ClientID(decode), "".to_string()),
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string())
} }
}
_ => { // Basic authentication
return Ok(error_response(&query, "invalid_request", "Authentication method unknown!")); (None, None, Some(v)) => {
} let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
}; None => {
return Ok(error_response(
&query,
"invalid_request",
&format!(
"Authorization header does not start with 'Basic ', got '{:#?}'",
v
),
));
}
Some(v) => v,
};
let decode = String::from_utf8_lossy(&match base64::decode(token) {
Ok(d) => d,
Err(e) => {
log::error!("Failed to decode authorization header: {:?}", e);
return Ok(error_response(
&query,
"invalid_request",
"Failed to decode authorization header!",
));
}
})
.to_string();
match decode.split_once(':') {
None => (ClientID(decode), "".to_string()),
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string()),
}
}
_ => {
return Ok(error_response(
&query,
"invalid_request",
"Authentication method unknown!",
));
}
};
let client = clients let client = clients
.find_by_id(&client_id) .find_by_id(&client_id)
.ok_or_else(|| ErrorUnauthorized("Client not found"))?; .ok_or_else(|| ErrorUnauthorized("Client not found"))?;
if !client.secret.eq(&client_secret) { if !client.secret.eq(&client_secret) {
return Ok(error_response(&query, "invalid_request", "Client secret is invalid!")); return Ok(error_response(
&query,
"invalid_request",
"Client secret is invalid!",
));
} }
let token_response = match (query.grant_type.as_str(), let token_response = match (
&query.authorization_code_query, query.grant_type.as_str(),
&query.refresh_token_query) { &query.authorization_code_query,
&query.refresh_token_query,
) {
("authorization_code", Some(q), _) => { ("authorization_code", Some(q), _) => {
let mut session: Session = match sessions let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone())) .send(openid_sessions_actor::FindSessionByAuthorizationCode(
.await.unwrap() q.code.clone(),
))
.await
.unwrap()
{ {
None => { None => {
return Ok(error_response(&query, "invalid_request", "Session not found!")); return Ok(error_response(
&query,
"invalid_request",
"Session not found!",
));
} }
Some(s) => s, Some(s) => s,
}; };
if session.client != client.id { if session.client != client.id {
return Ok(error_response(&query, "invalid_request", "Client mismatch!")); return Ok(error_response(
&query,
"invalid_request",
"Client mismatch!",
));
} }
if session.redirect_uri != q.redirect_uri { if session.redirect_uri != q.redirect_uri {
return Ok(error_response(&query, "invalid_request", "Invalid redirect URI!")); return Ok(error_response(
&query,
"invalid_request",
"Invalid redirect URI!",
));
} }
if session.authorization_code_expire_at < time() { if session.authorization_code_expire_at < time() {
return Ok(error_response(&query, "invalid_request", "Authorization code expired!")); return Ok(error_response(
&query,
"invalid_request",
"Authorization code expired!",
));
} }
// Check code challenge, if needed // Check code challenge, if needed
if let Some(chall) = &session.code_challenge { if let Some(chall) = &session.code_challenge {
let code_verifier = match &q.code_verifier { let code_verifier = match &q.code_verifier {
None => { None => {
return Ok(error_response(&query, "access_denied", "Code verifier missing")); return Ok(error_response(
&query,
"access_denied",
"Code verifier missing",
));
} }
Some(s) => s Some(s) => s,
}; };
if !chall.verify_code(code_verifier) { if !chall.verify_code(code_verifier) {
return Ok(error_response(&query, "invalid_grant", "Invalid code verifier")); return Ok(error_response(
&query,
"invalid_grant",
"Invalid code verifier",
));
} }
} else if q.code_verifier.is_some() { } else if q.code_verifier.is_some() {
return Ok(error_response(&query, "invalid_grant", "Unexpected `code_verifier` parameter!")); return Ok(error_response(
&query,
"invalid_grant",
"Unexpected `code_verifier` parameter!",
));
} }
if session.access_token.is_some() { if session.access_token.is_some() {
return Ok(error_response(&query, "invalid_request", "Authorization code already used!")); return Ok(error_response(
&query,
"invalid_request",
"Authorization code already used!",
));
} }
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?; session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
sessions.send(openid_sessions_actor::UpdateSession(session.clone())) sessions
.await.unwrap(); .send(openid_sessions_actor::UpdateSession(session.clone()))
.await
.unwrap();
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user.clone())) let user: Option<User> = users
.await.unwrap().0; .send(users_actor::GetUserRequest(session.user.clone()))
.await
.unwrap()
.0;
let user = match user { let user = match user {
None => return Ok(error_response(&query, "invalid_request", "User not found!")), None => return Ok(error_response(&query, "invalid_request", "User not found!")),
Some(u) => u, Some(u) => u,
@ -359,28 +456,44 @@ pub async fn token(req: HttpRequest,
("refresh_token", _, Some(q)) => { ("refresh_token", _, Some(q)) => {
let mut session: Session = match sessions let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone())) .send(openid_sessions_actor::FindSessionByRefreshToken(
.await.unwrap() q.refresh_token.clone(),
))
.await
.unwrap()
{ {
None => { None => {
return Ok(error_response(&query, "invalid_request", "Session not found!")); return Ok(error_response(
&query,
"invalid_request",
"Session not found!",
));
} }
Some(s) => s, Some(s) => s,
}; };
if session.client != client.id { if session.client != client.id {
return Ok(error_response(&query, "invalid_request", "Client mismatch!")); return Ok(error_response(
&query,
"invalid_request",
"Client mismatch!",
));
} }
if session.refresh_token_expire_at < time() { if session.refresh_token_expire_at < time() {
return Ok(error_response(&query, "access_denied", "Refresh token has expired!")); return Ok(error_response(
&query,
"access_denied",
"Refresh token has expired!",
));
} }
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?; session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
sessions sessions
.send(openid_sessions_actor::UpdateSession(session.clone())) .send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap(); .await
.unwrap();
TokenResponse { TokenResponse {
access_token: session.access_token.expect("Missing access token!"), access_token: session.access_token.expect("Missing access token!"),
@ -392,7 +505,11 @@ pub async fn token(req: HttpRequest,
} }
_ => { _ => {
return Ok(error_response(&query, "invalid_request", "Grant type unsupported!")); return Ok(error_response(
&query,
"invalid_request",
"Grant type unsupported!",
));
} }
}; };
@ -408,16 +525,20 @@ struct CertsResponse {
} }
pub async fn cert_uri(jwt_signer: web::Data<JWTSigner>) -> impl Responder { pub async fn cert_uri(jwt_signer: web::Data<JWTSigner>) -> impl Responder {
HttpResponse::Ok().json(CertsResponse { keys: vec![jwt_signer.get_json_web_key()] }) HttpResponse::Ok().json(CertsResponse {
keys: vec![jwt_signer.get_json_web_key()],
})
} }
fn user_info_error(err: &str, description: &str) -> HttpResponse { fn user_info_error(err: &str, description: &str) -> HttpResponse {
HttpResponse::Unauthorized() HttpResponse::Unauthorized()
.insert_header(("WWW-Authenticate", format!( .insert_header((
"Bearer error=\"{}\", error_description=\"{}\"", "WWW-Authenticate",
err, format!(
description "Bearer error=\"{}\", error_description=\"{}\"",
))) err, description
),
))
.finish() .finish()
} }
@ -426,37 +547,46 @@ pub struct UserInfoQuery {
access_token: Option<String>, access_token: Option<String>,
} }
pub async fn user_info_post(req: HttpRequest, pub async fn user_info_post(
form: Option<web::Form<UserInfoQuery>>, req: HttpRequest,
query: web::Query<UserInfoQuery>, form: Option<web::Form<UserInfoQuery>>,
sessions: web::Data<Addr<OpenIDSessionsActor>>, query: web::Query<UserInfoQuery>,
users: web::Data<Addr<UsersActor>>) -> impl Responder { sessions: web::Data<Addr<OpenIDSessionsActor>>,
user_info(req, users: web::Data<Addr<UsersActor>>,
form ) -> impl Responder {
.map(|f| f.0.access_token) user_info(
.unwrap_or_default() req,
.or(query.0.access_token), form.map(|f| f.0.access_token)
sessions, .unwrap_or_default()
users, .or(query.0.access_token),
).await sessions,
users,
)
.await
} }
pub async fn user_info_get(req: HttpRequest, query: web::Query<UserInfoQuery>, pub async fn user_info_get(
sessions: web::Data<Addr<OpenIDSessionsActor>>, req: HttpRequest,
users: web::Data<Addr<UsersActor>>) -> impl Responder { query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
user_info(req, query.0.access_token, sessions, users).await user_info(req, query.0.access_token, sessions, users).await
} }
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>/// /// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
async fn user_info(req: HttpRequest, token: Option<String>, async fn user_info(
sessions: web::Data<Addr<OpenIDSessionsActor>>, req: HttpRequest,
users: web::Data<Addr<UsersActor>>) -> impl Responder { token: Option<String>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let token = match token { let token = match token {
Some(t) => t, Some(t) => t,
None => { None => {
let token = match req.headers().get("Authorization") { let token = match req.headers().get("Authorization") {
None => return user_info_error("invalid_request", "Missing access token!"), None => return user_info_error("invalid_request", "Missing access token!"),
Some(t) => t Some(t) => t,
}; };
let token = match token.to_str() { let token = match token.to_str() {
@ -465,7 +595,12 @@ async fn user_info(req: HttpRequest, token: Option<String>,
}; };
let token = match token.strip_prefix("Bearer ") { let token = match token.strip_prefix("Bearer ") {
None => return user_info_error("invalid_request", "Header token does not start with 'Bearer '!"), None => {
return user_info_error(
"invalid_request",
"Header token does not start with 'Bearer '!",
)
}
Some(t) => t, Some(t) => t,
}; };
@ -474,7 +609,9 @@ async fn user_info(req: HttpRequest, token: Option<String>,
}; };
let session: Option<Session> = sessions let session: Option<Session> = sessions
.send(openid_sessions_actor::FindSessionByAccessToken(token)).await.unwrap(); .send(openid_sessions_actor::FindSessionByAccessToken(token))
.await
.unwrap();
let session = match session { let session = match session {
None => { None => {
return user_info_error("invalid_request", "Session not found!"); return user_info_error("invalid_request", "Session not found!");
@ -486,7 +623,11 @@ async fn user_info(req: HttpRequest, token: Option<String>,
return user_info_error("invalid_request", "Access token has expired!"); return user_info_error("invalid_request", "Access token has expired!");
} }
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user)).await.unwrap().0; let user: Option<User> = users
.send(users_actor::GetUserRequest(session.user))
.await
.unwrap()
.0;
let user = match user { let user = match user {
None => { None => {
return user_info_error("invalid_request", "Failed to extract user information!"); return user_info_error("invalid_request", "Failed to extract user information!");
@ -494,14 +635,13 @@ async fn user_info(req: HttpRequest, token: Option<String>,
Some(u) => u, Some(u) => u,
}; };
HttpResponse::Ok() HttpResponse::Ok().json(OpenIDUserInfo {
.json(OpenIDUserInfo { name: user.full_name(),
name: user.full_name(), sub: user.uid.0,
sub: user.uid.0, given_name: user.first_name,
given_name: user.first_name, family_name: user.last_name,
family_name: user.last_name, preferred_username: user.username,
preferred_username: user.username, email: user.email,
email: user.email, email_verified: true,
email_verified: true, })
})
} }

View File

@ -1,10 +1,10 @@
use actix::Addr; use actix::Addr;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use askama::Template; use askama::Template;
use crate::actors::{bruteforce_actor, users_actor};
use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::bruteforce_actor::BruteForceActor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::UsersActor;
use crate::actors::{bruteforce_actor, users_actor};
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
use crate::data::current_user::CurrentUser; use crate::data::current_user::CurrentUser;
use crate::data::remote_ip::RemoteIP; use crate::data::remote_ip::RemoteIP;
@ -21,8 +21,12 @@ pub(crate) struct BaseSettingsPage {
} }
impl BaseSettingsPage { impl BaseSettingsPage {
pub fn get(page_title: &'static str, user: &User, pub fn get(
danger_message: Option<String>, success_message: Option<String>) -> BaseSettingsPage { page_title: &'static str,
user: &User,
danger_message: Option<String>,
success_message: Option<String>,
) -> BaseSettingsPage {
Self { Self {
danger_message, danger_message,
success_message, success_message,
@ -52,11 +56,14 @@ struct ChangePasswordPage {
/// Account details page /// Account details page
pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder { pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder {
let user = user.into(); let user = user.into();
HttpResponse::Ok() HttpResponse::Ok().body(
.body(AccountDetailsPage { AccountDetailsPage {
_p: BaseSettingsPage::get("Account details", &user, None, None), _p: BaseSettingsPage::get("Account details", &user, None, None),
u: user, u: user,
}.render().unwrap()) }
.render()
.unwrap(),
)
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -66,53 +73,71 @@ pub struct PassChangeRequest {
} }
/// Change password route /// Change password route
pub async fn change_password_route(user: CurrentUser, pub async fn change_password_route(
users: web::Data<Addr<UsersActor>>, user: CurrentUser,
req: Option<web::Form<PassChangeRequest>>, users: web::Data<Addr<UsersActor>>,
bruteforce: web::Data<Addr<BruteForceActor>>, req: Option<web::Form<PassChangeRequest>>,
remote_ip: RemoteIP) -> impl Responder { bruteforce: web::Data<Addr<BruteForceActor>>,
remote_ip: RemoteIP,
) -> impl Responder {
let mut danger = None; let mut danger = None;
let mut success = None; let mut success = None;
let user: User = user.into(); let user: User = user.into();
let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() }).await.unwrap(); let failed_attempts = bruteforce
.send(bruteforce_actor::CountFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS { if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
danger = Some("Too many invalid password attempts. Please try to change your password later.".to_string()); danger = Some(
"Too many invalid password attempts. Please try to change your password later."
.to_string(),
);
} else if let Some(req) = req { } else if let Some(req) = req {
// Invalid password // Invalid password
if !user.verify_password(&req.old_pass) { if !user.verify_password(&req.old_pass) {
danger = Some("Old password is invalid!".to_string()); danger = Some("Old password is invalid!".to_string());
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap(); bruteforce
.send(bruteforce_actor::RecordFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
} }
// Password too short // Password too short
else if req.new_pass.len() < MIN_PASS_LEN { else if req.new_pass.len() < MIN_PASS_LEN {
danger = Some("New password is too short!".to_string()); danger = Some("New password is too short!".to_string());
} }
// Change password // Change password
else { else {
let res = users.send( let res = users
users_actor::ChangePasswordRequest { .send(users_actor::ChangePasswordRequest {
user_id: user.uid.clone(), user_id: user.uid.clone(),
new_password: req.new_pass.to_string(), new_password: req.new_pass.to_string(),
temporary: false, temporary: false,
} })
).await.unwrap().0; .await
.unwrap()
.0;
if !res { if !res {
danger = Some("An error occurred while trying to change your password!".to_string()); danger =
Some("An error occurred while trying to change your password!".to_string());
} else { } else {
success = Some("Your password was successfully changed!".to_string()); success = Some("Your password was successfully changed!".to_string());
} }
} }
} }
HttpResponse::Ok() HttpResponse::Ok().body(
.body(ChangePasswordPage { ChangePasswordPage {
_p: BaseSettingsPage::get("Change password", &user, danger, success), _p: BaseSettingsPage::get("Change password", &user, danger, success),
min_pwd_len: MIN_PASS_LEN, min_pwd_len: MIN_PASS_LEN,
}.render().unwrap()) }
.render()
.unwrap(),
)
} }

View File

@ -1,5 +1,5 @@
use actix::Addr; use actix::Addr;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use uuid::Uuid; use uuid::Uuid;
use webauthn_rs::prelude::RegisterPublicKeyCredential; use webauthn_rs::prelude::RegisterPublicKeyCredential;
@ -17,15 +17,19 @@ pub struct AddTOTPRequest {
first_code: String, first_code: String,
} }
pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>, pub async fn save_totp_factor(
users: web::Data<Addr<UsersActor>>) -> impl Responder { user: CurrentUser,
form: web::Json<AddTOTPRequest>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let key = TotpKey::from_encoded_secret(&form.secret); let key = TotpKey::from_encoded_secret(&form.secret);
if !key.check_code(&form.first_code).unwrap_or(false) { if !key.check_code(&form.first_code).unwrap_or(false) {
return HttpResponse::BadRequest() return HttpResponse::BadRequest().body(format!(
.body(format!("Given code is invalid (expected {} or {})!", "Given code is invalid (expected {} or {})!",
key.current_code().unwrap_or_default(), key.current_code().unwrap_or_default(),
key.previous_code().unwrap_or_default())); key.previous_code().unwrap_or_default()
));
} }
if form.factor_name.is_empty() { if form.factor_name.is_empty() {
@ -38,7 +42,11 @@ pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>
name: form.0.factor_name, name: form.0.factor_name,
kind: TwoFactorType::TOTP(key), kind: TwoFactorType::TOTP(key),
}); });
let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; let res = users
.send(users_actor::UpdateUserRequest(user))
.await
.unwrap()
.0;
if !res { if !res {
HttpResponse::InternalServerError().body("Failed to update user information!") HttpResponse::InternalServerError().body("Failed to update user information!")
@ -54,14 +62,13 @@ pub struct AddWebauthnRequest {
credential: RegisterPublicKeyCredential, credential: RegisterPublicKeyCredential,
} }
pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json<AddWebauthnRequest>, pub async fn save_webauthn_factor(
users: web::Data<Addr<UsersActor>>, user: CurrentUser,
manager: WebAuthManagerReq) -> impl Responder { form: web::Json<AddWebauthnRequest>,
let key = match manager.finish_registration( users: web::Data<Addr<UsersActor>>,
&user, manager: WebAuthManagerReq,
&form.0.opaque_state, ) -> impl Responder {
form.0.credential, let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) {
) {
Ok(k) => k, Ok(k) => k,
Err(e) => { Err(e) => {
log::error!("Failed to register security key! {:?}", e); log::error!("Failed to register security key! {:?}", e);
@ -75,7 +82,11 @@ pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json<AddWebauthn
name: form.0.factor_name, name: form.0.factor_name,
kind: TwoFactorType::WEBAUTHN(Box::new(key)), kind: TwoFactorType::WEBAUTHN(Box::new(key)),
}); });
let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; let res = users
.send(users_actor::UpdateUserRequest(user))
.await
.unwrap()
.0;
if !res { if !res {
HttpResponse::InternalServerError().body("Failed to update user information!") HttpResponse::InternalServerError().body("Failed to update user information!")
@ -89,12 +100,19 @@ pub struct DeleteFactorRequest {
id: FactorID, id: FactorID,
} }
pub async fn delete_factor(user: CurrentUser, form: web::Json<DeleteFactorRequest>, pub async fn delete_factor(
users: web::Data<Addr<UsersActor>>) -> impl Responder { user: CurrentUser,
form: web::Json<DeleteFactorRequest>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let mut user = User::from(user); let mut user = User::from(user);
user.remove_factor(form.0.id); user.remove_factor(form.0.id);
let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; let res = users
.send(users_actor::UpdateUserRequest(user))
.await
.unwrap()
.0;
if !res { if !res {
HttpResponse::InternalServerError().body("Failed to update user information!") HttpResponse::InternalServerError().body("Failed to update user information!")

View File

@ -1,6 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use askama::Template; use askama::Template;
use qrcode_generator::QrCodeEcc; use qrcode_generator::QrCodeEcc;
@ -37,27 +37,25 @@ struct AddWebauhtnPage {
/// Manage two factors authentication methods route /// Manage two factors authentication methods route
pub async fn two_factors_route(user: CurrentUser) -> impl Responder { pub async fn two_factors_route(user: CurrentUser) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok().body(
.body(TwoFactorsPage { TwoFactorsPage {
_p: BaseSettingsPage::get( _p: BaseSettingsPage::get("Two factor auth", &user, None, None),
"Two factor auth",
&user,
None,
None),
user: user.deref(), user: user.deref(),
}.render().unwrap()) }
.render()
.unwrap(),
)
} }
/// Configure a new TOTP authentication factor /// Configure a new TOTP authentication factor
pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppConfig>) -> impl Responder { pub async fn add_totp_factor_route(
user: CurrentUser,
app_conf: web::Data<AppConfig>,
) -> impl Responder {
let key = TotpKey::new_random(); let key = TotpKey::new_random();
let qr_code = qrcode_generator::to_png_to_vec( let qr_code =
key.url_for_user(&user, &app_conf), qrcode_generator::to_png_to_vec(key.url_for_user(&user, &app_conf), QrCodeEcc::Low, 1024);
QrCodeEcc::Low,
1024,
);
let qr_code = match qr_code { let qr_code = match qr_code {
Ok(q) => q, Ok(q) => q,
Err(e) => { Err(e) => {
@ -66,26 +64,29 @@ pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppCon
} }
}; };
HttpResponse::Ok() HttpResponse::Ok().body(
.body(AddTotpPage { AddTotpPage {
_p: BaseSettingsPage::get( _p: BaseSettingsPage::get("New authenticator app", &user, None, None),
"New authenticator app",
&user,
None,
None),
qr_code: base64::encode(qr_code), qr_code: base64::encode(qr_code),
account_name: key.account_name(&user, &app_conf), account_name: key.account_name(&user, &app_conf),
secret_key: key.get_secret(), secret_key: key.get_secret(),
}.render().unwrap()) }
.render()
.unwrap(),
)
} }
/// Configure a new security key factor /// Configure a new security key factor
pub async fn add_webauthn_factor_route(user: CurrentUser, manager: WebAuthManagerReq) -> impl Responder { pub async fn add_webauthn_factor_route(
user: CurrentUser,
manager: WebAuthManagerReq,
) -> impl Responder {
let registration_request = match manager.start_register(&user) { let registration_request = match manager.start_register(&user) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
log::error!("Failed to request new key! {:?}", e); log::error!("Failed to request new key! {:?}", e);
return HttpResponse::InternalServerError().body("Failed to generate request for registration!"); return HttpResponse::InternalServerError()
.body("Failed to generate request for registration!");
} }
}; };
@ -97,15 +98,14 @@ pub async fn add_webauthn_factor_route(user: CurrentUser, manager: WebAuthManage
} }
}; };
HttpResponse::Ok() HttpResponse::Ok().body(
.body(AddWebauhtnPage { AddWebauhtnPage {
_p: BaseSettingsPage::get( _p: BaseSettingsPage::get("New security key", &user, None, None),
"New security key",
&user,
None,
None),
opaque_state: registration_request.opaque_state, opaque_state: registration_request.opaque_state,
challenge_json: urlencoding::encode(&challenge_json).to_string(), challenge_json: urlencoding::encode(&challenge_json).to_string(),
}.render().unwrap()) }
.render()
.unwrap(),
)
} }

View File

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

View File

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

View File

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

View File

@ -4,9 +4,9 @@ use std::pin::Pin;
use actix::Addr; use actix::Addr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{Error, FromRequest, HttpRequest, web};
use actix_web::dev::Payload; use actix_web::dev::Payload;
use actix_web::error::ErrorInternalServerError; use actix_web::error::ErrorInternalServerError;
use actix_web::{web, Error, FromRequest, HttpRequest};
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::UsersActor;
@ -31,23 +31,29 @@ impl Deref for CurrentUser {
impl FromRequest for CurrentUser { impl FromRequest for CurrentUser {
type Error = Error; 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 { 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 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!"); .expect("Failed to get identity!");
let user_id = SessionIdentity(Some(&identity)).user_id(); let user_id = SessionIdentity(Some(&identity)).user_id();
Box::pin(async move { Box::pin(async move {
let user = match user_actor.send( let user = match user_actor
users_actor::GetUserRequest(user_id) .send(users_actor::GetUserRequest(user_id))
).await.unwrap().0 { .await
.unwrap()
.0
{
Some(u) => u, Some(u) => u,
None => { None => {
return Err(ErrorInternalServerError("Could not extract user information!")); return Err(ErrorInternalServerError(
"Could not extract user information!",
));
} }
}; };

View File

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

View File

@ -27,8 +27,9 @@ pub struct JWTSigner(RS256KeyPair);
impl JWTSigner { impl JWTSigner {
pub fn gen_from_memory() -> Res<Self> { pub fn gen_from_memory() -> Res<Self> {
Ok(Self(RS256KeyPair::generate(2048)? Ok(Self(
.with_key_id(&format!("key-{}", rand_str(15))))) RS256KeyPair::generate(2048)?.with_key_id(&format!("key-{}", rand_str(15))),
))
} }
pub fn get_json_web_key(&self) -> JsonWebKey { pub fn get_json_web_key(&self) -> JsonWebKey {

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 access_token;
pub mod totp_key; pub mod app_config;
pub mod login_redirect; pub mod client;
pub mod webauthn_manager; pub mod code_challenge;
pub mod crypto_wrapper; 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;

View File

@ -1,8 +1,8 @@
use std::net::IpAddr; use std::net::IpAddr;
use actix_web::{Error, FromRequest, HttpRequest, web};
use actix_web::dev::Payload; 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::data::app_config::AppConfig;
use crate::utils::network_utils::get_remote_ip; use crate::utils::network_utils::get_remote_ip;

View File

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

View File

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

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 { pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String {
match self.kind { match self.kind {
TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}", TwoFactorType::TOTP(_) => format!("/2fa_otp?redirect={}", redirect_uri.get_encoded()),
self.id.0, redirect_uri.get_encoded()), TwoFactorType::WEBAUTHN(_) => {
TwoFactorType::WEBAUTHN(_) => format!("/2fa_webauthn?id={}&redirect={}", format!("/2fa_webauthn?redirect={}", redirect_uri.get_encoded())
self.id.0, redirect_uri.get_encoded()), }
} }
} }
pub fn is_webauthn(&self) -> bool {
matches!(self.kind, TwoFactorType::WEBAUTHN(_))
}
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -71,7 +89,7 @@ impl User {
pub fn can_access_app(&self, id: &ClientID) -> bool { pub fn can_access_app(&self, id: &ClientID) -> bool {
match &self.authorized_clients { match &self.authorized_clients {
None => true, 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> { pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> {
self.two_factor.iter().find(|f| f.id.eq(factor_id)) 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 { impl PartialEq for User {
@ -157,8 +218,8 @@ impl EntityManager<User> {
/// Update user information /// Update user information
fn update_user<F>(&mut self, id: &UserID, update: F) -> bool fn update_user<F>(&mut self, id: &UserID, update: F) -> bool
where where
F: FnOnce(User) -> User, F: FnOnce(User) -> User,
{ {
let user = match self.find_by_user_id(id) { let user = match self.find_by_user_id(id) {
None => return false, None => return false,

View File

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

View File

@ -4,19 +4,19 @@ use std::sync::Arc;
use actix::Actor; use actix::Actor;
use actix_identity::config::LogoutBehaviour; use actix_identity::config::LogoutBehaviour;
use actix_identity::IdentityMiddleware; use actix_identity::IdentityMiddleware;
use actix_session::SessionMiddleware;
use actix_session::storage::CookieSessionStore; use actix_session::storage::CookieSessionStore;
use actix_web::{App, get, HttpResponse, HttpServer, middleware, web}; use actix_session::SessionMiddleware;
use actix_web::cookie::{Key, SameSite}; use actix_web::cookie::{Key, SameSite};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
use clap::Parser; use clap::Parser;
use basic_oidc::actors::bruteforce_actor::BruteForceActor; use basic_oidc::actors::bruteforce_actor::BruteForceActor;
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor; use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
use basic_oidc::actors::users_actor::UsersActor; use basic_oidc::actors::users_actor::UsersActor;
use basic_oidc::constants::*; use basic_oidc::constants::*;
use basic_oidc::controllers::*;
use basic_oidc::controllers::assets_controller::assets_route; use basic_oidc::controllers::assets_controller::assets_route;
use basic_oidc::controllers::*;
use basic_oidc::data::app_config::AppConfig; use basic_oidc::data::app_config::AppConfig;
use basic_oidc::data::client::ClientManager; use basic_oidc::data::client::ClientManager;
use basic_oidc::data::entity_manager::EntityManager; use basic_oidc::data::entity_manager::EntityManager;
@ -72,8 +72,7 @@ async fn main() -> std::io::Result<()> {
let users_actor = UsersActor::new(users).start(); let users_actor = UsersActor::new(users).start();
let bruteforce_actor = BruteForceActor::default().start(); let bruteforce_actor = BruteForceActor::default().start();
let openid_sessions_actor = OpenIDSessionsActor::default().start(); let openid_sessions_actor = OpenIDSessionsActor::default().start();
let jwt_signer = JWTSigner::gen_from_memory() let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
.expect("Failed to generate JWKS key");
let webauthn_manager = Arc::new(WebAuthManager::init(&config)); let webauthn_manager = Arc::new(WebAuthManager::init(&config));
log::info!("Server will listen on {}", config.listen_address); log::info!("Server will listen on {}", config.listen_address);
@ -84,13 +83,14 @@ async fn main() -> std::io::Result<()> {
.expect("Failed to load clients list!"); .expect("Failed to load clients list!");
clients.apply_environment_variables(); clients.apply_environment_variables();
let session_mw = let session_mw = SessionMiddleware::builder(
SessionMiddleware::builder(CookieSessionStore::default(), CookieSessionStore::default(),
Key::from(config.token_key.as_bytes())) Key::from(config.token_key.as_bytes()),
.cookie_name(SESSION_COOKIE_NAME.to_string()) )
.cookie_secure(config.secure_cookie()) .cookie_name(SESSION_COOKIE_NAME.to_string())
.cookie_same_site(SameSite::Lax) .cookie_secure(config.secure_cookie())
.build(); .cookie_same_site(SameSite::Lax)
.build();
let identity_middleware = IdentityMiddleware::builder() let identity_middleware = IdentityMiddleware::builder()
.logout_behaviour(LogoutBehaviour::PurgeSession) .logout_behaviour(LogoutBehaviour::PurgeSession)
@ -106,74 +106,145 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(clients)) .app_data(web::Data::new(clients))
.app_data(web::Data::new(jwt_signer.clone())) .app_data(web::Data::new(jwt_signer.clone()))
.app_data(web::Data::new(webauthn_manager.clone())) .app_data(web::Data::new(webauthn_manager.clone()))
.wrap(
.wrap(middleware::DefaultHeaders::new() middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")),
.add(("Permissions-Policy", "interest-cohort=()"))) )
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(AuthMiddleware {}) .wrap(AuthMiddleware {})
.wrap(identity_middleware) .wrap(identity_middleware)
.wrap(session_mw) .wrap(session_mw)
// main route // main route
.route("/", web::get() .route(
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() })) "/",
web::get().to(|| async {
HttpResponse::Found()
.append_header(("Location", "/settings"))
.finish()
}),
)
.route("/robots.txt", web::get().to(assets_controller::robots_txt)) .route("/robots.txt", web::get().to(assets_controller::robots_txt))
// health route // health route
.service(health) .service(health)
// Assets serving // Assets serving
.route("/assets/{path:.*}", web::get().to(assets_route)) .route("/assets/{path:.*}", web::get().to(assets_route))
// Login pages // Login pages
.route("/logout", web::get().to(login_controller::logout_route)) .route("/logout", web::get().to(login_controller::logout_route))
.route("/login", web::get().to(login_controller::login_route)) .route("/login", web::get().to(login_controller::login_route))
.route("/login", web::post().to(login_controller::login_route)) .route("/login", web::post().to(login_controller::login_route))
.route("/reset_password", web::get().to(login_controller::reset_password_route)) .route(
.route("/reset_password", web::post().to(login_controller::reset_password_route)) "/reset_password",
.route("/2fa_auth", web::get().to(login_controller::choose_2fa_method)) web::get().to(login_controller::reset_password_route),
)
.route(
"/reset_password",
web::post().to(login_controller::reset_password_route),
)
.route(
"/2fa_auth",
web::get().to(login_controller::choose_2fa_method),
)
.route("/2fa_otp", web::get().to(login_controller::login_with_otp)) .route("/2fa_otp", web::get().to(login_controller::login_with_otp))
.route("/2fa_otp", web::post().to(login_controller::login_with_otp)) .route("/2fa_otp", web::post().to(login_controller::login_with_otp))
.route("/2fa_webauthn", web::get().to(login_controller::login_with_webauthn)) .route(
"/2fa_webauthn",
web::get().to(login_controller::login_with_webauthn),
)
// Login api // Login api
.route("/login/api/auth_webauthn", web::post().to(login_api::auth_webauthn)) .route(
"/login/api/auth_webauthn",
web::post().to(login_api::auth_webauthn),
)
// Settings routes // Settings routes
.route("/settings", web::get().to(settings_controller::account_settings_details_route)) .route(
.route("/settings/change_password", web::get().to(settings_controller::change_password_route)) "/settings",
.route("/settings/change_password", web::post().to(settings_controller::change_password_route)) web::get().to(settings_controller::account_settings_details_route),
.route("/settings/two_factors", web::get().to(two_factors_controller::two_factors_route)) )
.route("/settings/two_factors/add_totp", web::get().to(two_factors_controller::add_totp_factor_route)) .route(
.route("/settings/two_factors/add_webauthn", web::get().to(two_factors_controller::add_webauthn_factor_route)) "/settings/change_password",
web::get().to(settings_controller::change_password_route),
)
.route(
"/settings/change_password",
web::post().to(settings_controller::change_password_route),
)
.route(
"/settings/two_factors",
web::get().to(two_factors_controller::two_factors_route),
)
.route(
"/settings/two_factors/add_totp",
web::get().to(two_factors_controller::add_totp_factor_route),
)
.route(
"/settings/two_factors/add_webauthn",
web::get().to(two_factors_controller::add_webauthn_factor_route),
)
// User API // User API
.route("/settings/api/two_factor/save_totp_factor", web::post().to(two_factor_api::save_totp_factor)) .route(
.route("/settings/api/two_factor/save_webauthn_factor", web::post().to(two_factor_api::save_webauthn_factor)) "/settings/api/two_factor/save_totp_factor",
.route("/settings/api/two_factor/delete_factor", web::post().to(two_factor_api::delete_factor)) web::post().to(two_factor_api::save_totp_factor),
)
.route(
"/settings/api/two_factor/save_webauthn_factor",
web::post().to(two_factor_api::save_webauthn_factor),
)
.route(
"/settings/api/two_factor/delete_factor",
web::post().to(two_factor_api::delete_factor),
)
// Admin routes // Admin routes
.route("/admin", web::get() .route(
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() })) "/admin",
.route("/admin/clients", web::get().to(admin_controller::clients_route)) web::get().to(|| async {
HttpResponse::Found()
.append_header(("Location", "/settings"))
.finish()
}),
)
.route(
"/admin/clients",
web::get().to(admin_controller::clients_route),
)
.route("/admin/users", web::get().to(admin_controller::users_route)) .route("/admin/users", web::get().to(admin_controller::users_route))
.route("/admin/users", web::post().to(admin_controller::users_route)) .route(
.route("/admin/create_user", web::get().to(admin_controller::create_user)) "/admin/users",
.route("/admin/edit_user", web::get().to(admin_controller::edit_user)) web::post().to(admin_controller::users_route),
)
.route(
"/admin/create_user",
web::get().to(admin_controller::create_user),
)
.route(
"/admin/edit_user",
web::get().to(admin_controller::edit_user),
)
// Admin API // Admin API
.route("/admin/api/find_username", web::post().to(admin_api::find_username)) .route(
.route("/admin/api/delete_user", web::post().to(admin_api::delete_user)) "/admin/api/find_username",
web::post().to(admin_api::find_username),
)
.route(
"/admin/api/delete_user",
web::post().to(admin_api::delete_user),
)
// OpenID routes // OpenID routes
.route("/.well-known/openid-configuration", web::get().to(openid_controller::get_configuration)) .route(
"/.well-known/openid-configuration",
web::get().to(openid_controller::get_configuration),
)
.route(AUTHORIZE_URI, web::get().to(openid_controller::authorize)) .route(AUTHORIZE_URI, web::get().to(openid_controller::authorize))
.route(TOKEN_URI, web::post().to(openid_controller::token)) .route(TOKEN_URI, web::post().to(openid_controller::token))
.route(CERT_URI, web::get().to(openid_controller::cert_uri)) .route(CERT_URI, web::get().to(openid_controller::cert_uri))
.route(USERINFO_URI, web::post().to(openid_controller::user_info_post)) .route(
.route(USERINFO_URI, web::get().to(openid_controller::user_info_get)) USERINFO_URI,
web::post().to(openid_controller::user_info_post),
)
.route(
USERINFO_URI,
web::get().to(openid_controller::user_info_get),
)
}) })
.bind(listen_address)? .bind(listen_address)?
.run() .run()
.await .await
} }

View File

@ -1,18 +1,20 @@
//! # Authentication middleware //! # Authentication middleware
use std::future::{Future, ready, Ready}; use std::future::{ready, Future, Ready};
use std::pin::Pin; use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use actix_identity::IdentityExt; use actix_identity::IdentityExt;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpResponse, web,
};
use actix_web::body::EitherBody; use actix_web::body::EitherBody;
use actix_web::http::{header, Method}; use actix_web::http::{header, Method};
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
web, Error, HttpResponse,
};
use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI, TOKEN_URI, USERINFO_URI}; use crate::constants::{
ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI, TOKEN_URI, USERINFO_URI,
};
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user_for_login}; use crate::controllers::base_controller::{build_fatal_error_page, redirect_user_for_login};
use crate::data::app_config::AppConfig; use crate::data::app_config::AppConfig;
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus}; use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
@ -27,10 +29,10 @@ pub struct AuthMiddleware;
// `S` - type of the next service // `S` - type of the next service
// `B` - type of response's body // `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where where
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
{ {
type Response = ServiceResponse<EitherBody<B>>; type Response = ServiceResponse<EitherBody<B>>;
type Error = Error; type Error = Error;
@ -62,22 +64,21 @@ impl ConnStatus {
} }
} }
pub struct AuthInnerMiddleware<S> { pub struct AuthInnerMiddleware<S> {
service: Rc<S>, service: Rc<S>,
} }
impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S> impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
where where
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
{ {
type Response = ServiceResponse<EitherBody<B>>; type Response = ServiceResponse<EitherBody<B>>;
type Error = Error; type Error = Error;
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output=Result<Self::Response, Self::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
forward_ready!(service); forward_ready!(service);
@ -90,7 +91,8 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Check if POST request comes from another website (block invalid origins) // Check if POST request comes from another website (block invalid origins)
let origin = req.headers().get(header::ORIGIN); let origin = req.headers().get(header::ORIGIN);
if req.method() == Method::POST && req.path() != TOKEN_URI && req.path() != USERINFO_URI { if req.method() == Method::POST && req.path() != TOKEN_URI && req.path() != USERINFO_URI
{
if let Some(o) = origin { if let Some(o) = origin {
if !o.to_str().unwrap_or("bad").eq(&config.website_origin) { if !o.to_str().unwrap_or("bad").eq(&config.website_origin) {
log::warn!( log::warn!(
@ -118,14 +120,14 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
let session_data = SessionIdentity::deserialize_session_data(id); let session_data = SessionIdentity::deserialize_session_data(id);
let session = match session_data { let session = match session_data {
Some(SessionIdentityData { Some(SessionIdentityData {
status: SessionStatus::SignedIn, status: SessionStatus::SignedIn,
is_admin: true, is_admin: true,
.. ..
}) => ConnStatus::Admin, }) => ConnStatus::Admin,
Some(SessionIdentityData { Some(SessionIdentityData {
status: SessionStatus::SignedIn, status: SessionStatus::SignedIn,
.. ..
}) => ConnStatus::RegularUser, }) => ConnStatus::RegularUser,
_ => ConnStatus::SignedOut, _ => ConnStatus::SignedOut,
}; };
@ -135,10 +137,13 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Redirect user to login page // Redirect user to login page
if !session.is_auth() if !session.is_auth()
&& (req.path().starts_with(ADMIN_ROUTES) && (req.path().starts_with(ADMIN_ROUTES)
|| req.path().starts_with(AUTHENTICATED_ROUTES) || req.path().eq(AUTHORIZE_URI)) || req.path().starts_with(AUTHENTICATED_ROUTES)
|| req.path().eq(AUTHORIZE_URI))
{ {
log::debug!("Redirect unauthenticated user from {} to authorization route.", log::debug!(
req.path()); "Redirect unauthenticated user from {} to authorization route.",
req.path()
);
let path = req.uri().to_string(); let path = req.uri().to_string();
return Ok(req return Ok(req
@ -149,10 +154,9 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Restrict access to admin pages // Restrict access to admin pages
if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) { if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) {
return Ok(req return Ok(req
.into_response( .into_response(HttpResponse::Unauthorized().body(build_fatal_error_page(
HttpResponse::Unauthorized().body( "You are not allowed to access this resource.",
build_fatal_error_page("You are not allowed to access this resource.")), )))
)
.map_into_right_body()); .map_into_right_body());
} }

View File

@ -1,5 +1,5 @@
pub mod crypt_utils;
pub mod err; pub mod err;
pub mod time;
pub mod network_utils; pub mod network_utils;
pub mod string_utils; pub mod string_utils;
pub mod crypt_utils; pub mod time;

View File

@ -16,7 +16,6 @@ pub fn match_ip(pattern: &str, ip: &str) -> bool {
false false
} }
/// Get the remote IP address /// Get the remote IP address
pub fn get_remote_ip(req: &HttpRequest, proxy_ip: Option<&str>) -> IpAddr { pub fn get_remote_ip(req: &HttpRequest, proxy_ip: Option<&str>) -> IpAddr {
let mut ip = req.peer_addr().unwrap().ip(); let mut ip = req.peer_addr().unwrap().ip();
@ -78,7 +77,10 @@ mod test {
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap()) .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.to_http_request(); .to_http_request();
assert_eq!(get_remote_ip(&req, None), "192.168.1.1".parse::<IpAddr>().unwrap()); assert_eq!(
get_remote_ip(&req, None),
"192.168.1.1".parse::<IpAddr>().unwrap()
);
} }
#[test] #[test]
@ -87,7 +89,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap()) .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1")) .insert_header(("X-Forwarded-For", "1.1.1.1"))
.to_http_request(); .to_http_request();
assert_eq!(get_remote_ip(&req, Some("192.168.1.1")), "1.1.1.1".parse::<IpAddr>().unwrap()); assert_eq!(
get_remote_ip(&req, Some("192.168.1.1")),
"1.1.1.1".parse::<IpAddr>().unwrap()
);
} }
#[test] #[test]
@ -96,7 +101,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap()) .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2")) .insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
.to_http_request(); .to_http_request();
assert_eq!(get_remote_ip(&req, Some("192.168.1.1")), "1.1.1.1".parse::<IpAddr>().unwrap()); assert_eq!(
get_remote_ip(&req, Some("192.168.1.1")),
"1.1.1.1".parse::<IpAddr>().unwrap()
);
} }
#[test] #[test]
@ -105,7 +113,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap()) .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "10::1, 1.2.2.2")) .insert_header(("X-Forwarded-For", "10::1, 1.2.2.2"))
.to_http_request(); .to_http_request();
assert_eq!(get_remote_ip(&req, Some("192.168.1.1")), "10::".parse::<IpAddr>().unwrap()); assert_eq!(
get_remote_ip(&req, Some("192.168.1.1")),
"10::".parse::<IpAddr>().unwrap()
);
} }
#[test] #[test]
@ -114,7 +125,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap()) .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2")) .insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
.to_http_request(); .to_http_request();
assert_eq!(get_remote_ip(&req, None), "192.168.1.1".parse::<IpAddr>().unwrap()); assert_eq!(
get_remote_ip(&req, None),
"192.168.1.1".parse::<IpAddr>().unwrap()
);
} }
#[test] #[test]
@ -123,7 +137,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap()) .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2")) .insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
.to_http_request(); .to_http_request();
assert_eq!(get_remote_ip(&req, Some("192.168.1.2")), "192.168.1.1".parse::<IpAddr>().unwrap()); assert_eq!(
get_remote_ip(&req, Some("192.168.1.2")),
"192.168.1.1".parse::<IpAddr>().unwrap()
);
} }
#[test] #[test]
@ -141,7 +158,10 @@ mod test {
#[test] #[test]
fn parse_ip_v6_address() { fn parse_ip_v6_address() {
let ip = parse_ip("2a00:1450:4007:813::200e").unwrap(); let ip = parse_ip("2a00:1450:4007:813::200e").unwrap();
assert_eq!(ip, IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4007, 0x813, 0, 0, 0, 0))); assert_eq!(
ip,
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4007, 0x813, 0, 0, 0, 0))
);
} }
#[test] #[test]

View File

@ -16,7 +16,11 @@ pub fn apply_env_vars(val: &str) -> String {
let mut val = val.to_string(); let mut val = val.to_string();
if let Some(varname_with_wrapper) = regex_find!(r#"\$\{[a-zA-Z0-9_-]+\}"#, &val) { if let Some(varname_with_wrapper) = regex_find!(r#"\$\{[a-zA-Z0-9_-]+\}"#, &val) {
let varname = varname_with_wrapper.strip_prefix("${").unwrap().strip_suffix('}').unwrap(); let varname = varname_with_wrapper
.strip_prefix("${")
.unwrap()
.strip_suffix('}')
.unwrap();
let value = match std::env::var(varname) { let value = match std::env::var(varname) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
@ -34,8 +38,8 @@ pub fn apply_env_vars(val: &str) -> String {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::env;
use crate::utils::string_utils::apply_env_vars; use crate::utils::string_utils::apply_env_vars;
use std::env;
const VAR_ONE: &str = "VAR_ONE"; const VAR_ONE: &str = "VAR_ONE";
#[test] #[test]

View File

@ -4,13 +4,15 @@
<div> <div>
<p>You need to validate a second factor to complete your login.</p> <p>You need to validate a second factor to complete your login.</p>
{% for factor in factors %} {% for factor in user.get_distinct_factors_types() %}
<p> <a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%; display: flex;">
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%;"> <img src="{{ factor.type_image() }}" alt="Factor icon" style="margin-right: 1em;" />
{{ factor.name }} <br/> <div style="text-align: left;">
<small>{{ factor.type_str() }}</small> {{ factor.type_str() }} <br/>
</a> <small style="font-size: 0.7em;">{{ factor.description_str() }}</small>
</p> </div>
</a>
<br />
{% endfor %} {% endfor %}
</div> </div>

View File

@ -10,8 +10,8 @@
</style> </style>
<div> <div>
<p>Please go to your authenticator app <i>{{ factor.name }}</i>, generate a new code and enter it here:</p> <p>Please open one of your registered authenticator app, generate a new code and enter it here:</p>
<form id="totp_form" method="post" action="{{ factor.login_url(_p.redirect_uri) }}"> <form id="totp_form" method="post">
<input type="hidden" id="code" name="code"/> <input type="hidden" id="code" name="code"/>
<div class="form-group"> <div class="form-group">
<div id="otp" class="inputs d-flex flex-row justify-content-center mt-2"> <div id="otp" class="inputs d-flex flex-row justify-content-center mt-2">
@ -34,6 +34,9 @@
<script> <script>
function OTPInput() { function OTPInput() {
// Set form destination
document.getElementById("totp_form").action = location.href;
const inputs = document.querySelectorAll('#otp > *[id]'); const inputs = document.querySelectorAll('#otp > *[id]');
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
// Reset form on init // Reset form on init

View File

@ -4,7 +4,7 @@
<p style="color:red" id="err_target"></p> <p style="color:red" id="err_target"></p>
<div> <div>
<p>Please insert now your security key <i>{{ factor.name }}</i>, and accept authentication request.</p> <p>Please insert now on of your registered security key, and accept authentication request.</p>
</div> </div>
<div style="margin: 10px 0px;"> <div style="margin: 10px 0px;">