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

View File

@ -1,3 +1,3 @@
pub mod users_actor;
pub mod bruteforce_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::{Actor, AsyncContext, Context, Handler};
use crate::constants::*;
use crate::data::access_token::AccessToken;
@ -37,13 +37,16 @@ pub struct Session {
impl Session {
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()
}
pub fn regenerate_access_and_refresh_tokens(&mut self,
pub fn regenerate_access_and_refresh_tokens(
&mut self,
app_config: &AppConfig,
jwt_signer: &JWTSigner) -> Res {
jwt_signer: &JWTSigner,
) -> Res {
let access_token = AccessToken {
issuer: app_config.website_origin.to_string(),
subject_identifier: self.user.clone().0,
@ -116,7 +119,11 @@ impl Handler<PushNewSession> for OpenIDSessionsActor {
impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
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
.iter()
.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 {
self.session
.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()
}
}
@ -150,8 +162,13 @@ impl Handler<UpdateSession> for OpenIDSessionsActor {
type Result = ();
fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result {
if let Some(r) = self.session.iter().enumerate()
.find(|f| f.1.session_id.eq(&msg.0.session_id)).map(|f| f.0) {
if let Some(r) = self
.session
.iter()
.enumerate()
.find(|f| f.1.session_id.eq(&msg.0.session_id))
.map(|f| f.0)
{
self.session[r] = msg.0;
}
}

View File

@ -123,7 +123,9 @@ impl Handler<FindUserByUsername> for UsersActor {
type Result = MessageResult<FindUserByUsername>;
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 {
let user = match self.manager.find_by_user_id(&msg.0) {
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));
}
Some(s) => s
Some(s) => s,
};
MessageResult(DeleteUserResult(match self.manager.remove(&user) {

View File

@ -1,5 +1,5 @@
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::data::current_user::CurrentUser;
@ -15,10 +15,16 @@ struct FindUserResult {
user_id: Option<String>,
}
pub async fn find_username(req: web::Form<FindUserNameReq>, users: web::Data<Addr<UsersActor>>) -> impl Responder {
let res = users.send(FindUserByUsername(req.0.username)).await.unwrap();
pub async fn find_username(
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 {
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,
}
pub async fn delete_user(user: CurrentUser, req: web::Form<DeleteUserReq>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
pub async fn delete_user(
user: CurrentUser,
req: web::Form<DeleteUserReq>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
if user.uid == req.user_id {
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()
}
}

View File

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

View File

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

View File

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

View File

@ -1,17 +1,19 @@
use actix::Addr;
use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use askama::Template;
use crate::actors::{bruteforce_actor, users_actor};
use crate::actors::bruteforce_actor::BruteForceActor;
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::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::remote_ip::RemoteIP;
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;
struct BaseLoginPage<'a> {
@ -40,26 +42,23 @@ struct PasswordResetTemplate<'a> {
#[template(path = "login/choose_second_factor.html")]
struct ChooseSecondFactorTemplate<'a> {
_p: BaseLoginPage<'a>,
factors: &'a [TwoFactor],
user: &'a User,
}
#[derive(Template)]
#[template(path = "login/opt_input.html")]
#[template(path = "login/otp_input.html")]
struct LoginWithOTPTemplate<'a> {
_p: BaseLoginPage<'a>,
factor: &'a TwoFactor,
}
#[derive(Template)]
#[template(path = "login/webauthn_input.html")]
struct LoginWithWebauthnTemplate<'a> {
_p: BaseLoginPage<'a>,
factor: &'a TwoFactor,
opaque_state: String,
challenge_json: String,
}
#[derive(serde::Deserialize)]
pub struct LoginRequestBody {
login: String,
@ -87,13 +86,17 @@ pub async fn login_route(
let mut success = None;
let mut login = String::new();
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 {
return HttpResponse::TooManyRequests().body(
build_fatal_error_page("Too many failed login attempts, please try again later!")
);
return HttpResponse::TooManyRequests().body(build_fatal_error_page(
"Too many failed login attempts, please try again later!",
));
}
// Check if user session must be closed
@ -103,22 +106,24 @@ pub async fn login_route(
}
success = Some("Goodbye!".to_string());
}
// Check if user is already authenticated
else if SessionIdentity(id.as_ref()).is_authenticated() {
return redirect_user(query.redirect.get());
}
// Check if the password of the user has to be changed
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
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
else if let Some(req) = &req {
login = req.login.clone();
@ -150,10 +155,20 @@ pub async fn login_route(
}
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());
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap();
bruteforce
.send(bruteforce_actor::RecordFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
}
}
}
@ -191,10 +206,13 @@ pub struct PasswordResetQuery {
}
/// Reset user password route
pub async fn reset_password_route(id: Option<Identity>, query: web::Query<PasswordResetQuery>,
pub async fn reset_password_route(
id: Option<Identity>,
query: web::Query<PasswordResetQuery>,
req: Option<web::Form<ChangePasswordRequestBody>>,
users: web::Data<Addr<UsersActor>>,
http_req: HttpRequest) -> impl Responder {
http_req: HttpRequest,
) -> impl Responder {
let mut danger = None;
if !SessionIdentity(id.as_ref()).need_new_password() {
@ -249,18 +267,27 @@ pub struct ChooseSecondFactorQuery {
}
/// Let the user select the factor to use to authenticate
pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSecondFactorQuery>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
pub async fn choose_2fa_method(
id: Option<Identity>,
query: web::Query<ChooseSecondFactorQuery>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
log::trace!("User does not require 2fa auth, redirecting");
return redirect_user_for_login(query.redirect.get());
}
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
.await.unwrap().0.expect("Could not find user!");
let user: User = users
.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
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");
return redirect_user(&user.two_factor[0].login_url(&query.redirect));
}
@ -274,7 +301,7 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
app_name: APP_NAME,
redirect_uri: &query.redirect,
},
factors: &user.two_factor,
user: &user,
}
.render()
.unwrap(),
@ -285,7 +312,6 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
pub struct LoginWithOTPQuery {
#[serde(default)]
redirect: LoginRedirect,
id: FactorID,
}
#[derive(serde::Deserialize)]
@ -293,35 +319,39 @@ pub struct LoginWithOTPForm {
code: String,
}
/// Login with OTP
pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTPQuery>,
pub async fn login_with_otp(
id: Option<Identity>,
query: web::Query<LoginWithOTPQuery>,
form: Option<web::Form<LoginWithOTPForm>>,
users: web::Data<Addr<UsersActor>>,
http_req: HttpRequest) -> impl Responder {
http_req: HttpRequest,
) -> impl Responder {
let mut danger = None;
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
return redirect_user_for_login(query.redirect.get());
}
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
.await.unwrap().0.expect("Could not find user!");
let user: User = users
.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) {
Some(f) => f,
None => 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!"));
let keys = user.get_otp_factors();
if keys.is_empty() {
return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"));
}
};
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());
} else {
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
@ -329,7 +359,8 @@ pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTP
}
}
HttpResponse::Ok().body(LoginWithOTPTemplate {
HttpResponse::Ok().body(
LoginWithOTPTemplate {
_p: BaseLoginPage {
danger,
success: None,
@ -337,48 +368,51 @@ pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTP
app_name: APP_NAME,
redirect_uri: &query.redirect,
},
factor,
}.render().unwrap())
}
.render()
.unwrap(),
)
}
#[derive(serde::Deserialize)]
pub struct LoginWithWebauthnQuery {
#[serde(default)]
redirect: LoginRedirect,
id: FactorID,
}
/// Login with Webauthn
pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWithWebauthnQuery>,
pub async fn login_with_webauthn(
id: Option<Identity>,
query: web::Query<LoginWithWebauthnQuery>,
manager: WebAuthManagerReq,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
return redirect_user_for_login(query.redirect.get());
}
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
.await.unwrap().0.expect("Could not find user!");
let user: User = users
.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) {
Some(f) => f,
None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"))
};
let key = match &factor.kind {
TwoFactorType::WEBAUTHN(key) => key,
_ => {
let pub_keys = user.get_webauthn_pub_keys();
if pub_keys.is_empty() {
return HttpResponse::Ok()
.body(build_fatal_error_page("Factor is not a Webauthn key!"));
.body(build_fatal_error_page("No Webauthn public key registered!"));
}
};
let challenge = match manager.start_authentication(&user.uid, key) {
let challenge = match manager.start_authentication(&user.uid, &pub_keys) {
Ok(c) => c,
Err(e) => {
log::error!("Failed to generate webauthn challenge! {:?}", e);
return HttpResponse::InternalServerError()
.body(build_fatal_error_page("Failed to generate webauthn challenge"));
return HttpResponse::InternalServerError().body(build_fatal_error_page(
"Failed to generate webauthn challenge",
));
}
};
@ -390,7 +424,8 @@ pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWi
}
};
HttpResponse::Ok().body(LoginWithWebauthnTemplate {
HttpResponse::Ok().body(
LoginWithWebauthnTemplate {
_p: BaseLoginPage {
danger: None,
success: None,
@ -398,8 +433,10 @@ pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWi
app_name: APP_NAME,
redirect_uri: &query.redirect,
},
factor,
opaque_state: challenge.opaque_state,
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 base_controller;
pub mod login_controller;
pub mod login_api;
pub mod settings_controller;
pub mod admin_controller;
pub mod admin_api;
pub mod login_controller;
pub mod openid_controller;
pub mod two_factors_controller;
pub mod settings_controller;
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_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
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::users_actor::UsersActor;
use crate::actors::{openid_sessions_actor, users_actor};
use crate::constants::*;
use crate::controllers::base_controller::build_fatal_error_page;
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::current_user::CurrentUser;
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::openid_config::OpenIDConfig;
use crate::data::session_identity::SessionIdentity;
@ -24,7 +24,9 @@ use crate::utils::string_utils::rand_str;
use crate::utils::time::time;
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"))
.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(),
};
let curr_origin = format!("{}://{}", match is_secure_request {
let curr_origin = format!(
"{}://{}",
match is_secure_request {
true => "https",
false => "http"
}, host);
false => "http",
},
host
);
HttpResponse::Ok().json(OpenIDConfig {
issuer: app_conf.website_origin.clone(),
@ -80,35 +86,43 @@ pub struct AuthorizeQuery {
}
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()
.append_header(
("Location", format!(
.append_header((
"Location",
format!(
"{}?error={}?error_description={}&state={}",
query.redirect_uri,
urlencoding::encode(error),
urlencoding::encode(description),
urlencoding::encode(&query.state)
),
))
)
.finish()
}
pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<AuthorizeQuery>,
pub async fn authorize(
user: CurrentUser,
id: Identity,
query: web::Query<AuthorizeQuery>,
clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> impl Responder {
sessions: web::Data<Addr<OpenIDSessionsActor>>,
) -> impl Responder {
let client = match clients.find_by_id(&query.client_id) {
None => {
return HttpResponse::BadRequest()
.body(build_fatal_error_page("Client is invalid!"));
return HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!"));
}
Some(c) => c
Some(c) => c,
};
let redirect_uri = query.redirect_uri.trim().to_string();
if !redirect_uri.starts_with(&client.redirect_uri) {
return HttpResponse::BadRequest()
.body(build_fatal_error_page("Redirect URI is invalid!"));
return HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!"));
}
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") {
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() {
@ -127,18 +145,27 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
Some(chal) => {
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
if !meth.eq("S256") && !meth.eq("plain") {
return error_redirect(&query, "invalid_request",
"Only S256 and plain code challenge methods are supported!");
return error_redirect(
&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
if !user.can_access_app(&client.id) {
return error_redirect(&query, "invalid_request",
"User is not authorized to access this application!");
return error_redirect(
&query,
"invalid_request",
"User is not authorized to access this application!",
);
}
// 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,
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);
HttpResponse::Found()
.append_header(("Location", format!(
.append_header((
"Location",
format!(
"{}?state={}&session_state={}&code={}",
session.redirect_uri,
urlencoding::encode(&query.0.state),
urlencoding::encode(&session.session_id.0),
urlencoding::encode(&session.authorization_code)
))).finish()
),
))
.finish()
}
#[derive(serde::Serialize)]
@ -178,9 +212,13 @@ struct ErrorResponse {
}
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
log::warn!("request failed: {} - {} => '{:#?}'", error, description, query);
HttpResponse::BadRequest()
.json(ErrorResponse {
log::warn!(
"request failed: {} - {} => '{:#?}'",
error,
description,
query
);
HttpResponse::BadRequest().json(ErrorResponse {
error: error.to_string(),
error_description: description.to_string(),
})
@ -198,7 +236,6 @@ pub struct TokenRefreshTokenQuery {
refresh_token: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct TokenQuery {
grant_type: String,
@ -222,17 +259,19 @@ pub struct TokenResponse {
id_token: Option<String>,
}
pub async fn token(req: HttpRequest,
pub async fn token(
req: HttpRequest,
query: web::Form<TokenQuery>,
clients: web::Data<ClientManager>,
app_config: web::Data<AppConfig>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> {
jwt_signer: web::Data<JWTSigner>,
) -> actix_web::Result<HttpResponse> {
// Extraction authentication information
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) =
match (&query.client_id, &query.client_secret, authorization_header) {
// post authentication
(Some(client_id), Some(client_secret), None) => {
(client_id.clone(), client_secret.to_string())
@ -245,28 +284,40 @@ pub async fn token(req: HttpRequest,
return Ok(error_response(
&query,
"invalid_request",
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
&format!(
"Authorization header does not start with 'Basic ', got '{:#?}'",
v
),
));
}
Some(v) => 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!"));
return Ok(error_response(
&query,
"invalid_request",
"Failed to decode authorization header!",
));
}
}).to_string();
})
.to_string();
match decode.split_once(':') {
None => (ClientID(decode), "".to_string()),
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string())
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string()),
}
}
_ => {
return Ok(error_response(&query, "invalid_request", "Authentication method unknown!"));
return Ok(error_response(
&query,
"invalid_request",
"Authentication method unknown!",
));
}
};
@ -275,62 +326,108 @@ pub async fn token(req: HttpRequest,
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
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.grant_type.as_str(),
&query.authorization_code_query,
&query.refresh_token_query) {
&query.refresh_token_query,
) {
("authorization_code", Some(q), _) => {
let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
.await.unwrap()
.send(openid_sessions_actor::FindSessionByAuthorizationCode(
q.code.clone(),
))
.await
.unwrap()
{
None => {
return Ok(error_response(&query, "invalid_request", "Session not found!"));
return Ok(error_response(
&query,
"invalid_request",
"Session not found!",
));
}
Some(s) => s,
};
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 {
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() {
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
if let Some(chall) = &session.code_challenge {
let code_verifier = match &q.code_verifier {
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) {
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() {
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() {
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)?;
sessions.send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap();
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await
.unwrap();
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user.clone()))
.await.unwrap().0;
let user: Option<User> = users
.send(users_actor::GetUserRequest(session.user.clone()))
.await
.unwrap()
.0;
let user = match user {
None => return Ok(error_response(&query, "invalid_request", "User not found!")),
Some(u) => u,
@ -359,28 +456,44 @@ pub async fn token(req: HttpRequest,
("refresh_token", _, Some(q)) => {
let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone()))
.await.unwrap()
.send(openid_sessions_actor::FindSessionByRefreshToken(
q.refresh_token.clone(),
))
.await
.unwrap()
{
None => {
return Ok(error_response(&query, "invalid_request", "Session not found!"));
return Ok(error_response(
&query,
"invalid_request",
"Session not found!",
));
}
Some(s) => s,
};
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() {
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)?;
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap();
.await
.unwrap();
TokenResponse {
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 {
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 {
HttpResponse::Unauthorized()
.insert_header(("WWW-Authenticate", format!(
.insert_header((
"WWW-Authenticate",
format!(
"Bearer error=\"{}\", error_description=\"{}\"",
err,
description
)))
err, description
),
))
.finish()
}
@ -426,37 +547,46 @@ pub struct UserInfoQuery {
access_token: Option<String>,
}
pub async fn user_info_post(req: HttpRequest,
pub async fn user_info_post(
req: HttpRequest,
form: Option<web::Form<UserInfoQuery>>,
query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
user_info(req,
form
.map(|f| f.0.access_token)
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
user_info(
req,
form.map(|f| f.0.access_token)
.unwrap_or_default()
.or(query.0.access_token),
sessions,
users,
).await
)
.await
}
pub async fn user_info_get(req: HttpRequest, query: web::Query<UserInfoQuery>,
pub async fn user_info_get(
req: HttpRequest,
query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
user_info(req, query.0.access_token, sessions, users).await
}
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
async fn user_info(req: HttpRequest, token: Option<String>,
async fn user_info(
req: HttpRequest,
token: Option<String>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let token = match token {
Some(t) => t,
None => {
let token = match req.headers().get("Authorization") {
None => return user_info_error("invalid_request", "Missing access token!"),
Some(t) => t
Some(t) => t,
};
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 ") {
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,
};
@ -474,7 +609,9 @@ async fn user_info(req: HttpRequest, token: Option<String>,
};
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 {
None => {
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!");
}
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 {
None => {
return user_info_error("invalid_request", "Failed to extract user information!");
@ -494,8 +635,7 @@ async fn user_info(req: HttpRequest, token: Option<String>,
Some(u) => u,
};
HttpResponse::Ok()
.json(OpenIDUserInfo {
HttpResponse::Ok().json(OpenIDUserInfo {
name: user.full_name(),
sub: user.uid.0,
given_name: user.first_name,

View File

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

View File

@ -1,5 +1,5 @@
use actix::Addr;
use actix_web::{HttpResponse, Responder, web};
use actix_web::{web, HttpResponse, Responder};
use uuid::Uuid;
use webauthn_rs::prelude::RegisterPublicKeyCredential;
@ -17,15 +17,19 @@ pub struct AddTOTPRequest {
first_code: String,
}
pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
pub async fn save_totp_factor(
user: CurrentUser,
form: web::Json<AddTOTPRequest>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let key = TotpKey::from_encoded_secret(&form.secret);
if !key.check_code(&form.first_code).unwrap_or(false) {
return HttpResponse::BadRequest()
.body(format!("Given code is invalid (expected {} or {})!",
return HttpResponse::BadRequest().body(format!(
"Given code is invalid (expected {} or {})!",
key.current_code().unwrap_or_default(),
key.previous_code().unwrap_or_default()));
key.previous_code().unwrap_or_default()
));
}
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,
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 {
HttpResponse::InternalServerError().body("Failed to update user information!")
@ -54,14 +62,13 @@ pub struct AddWebauthnRequest {
credential: RegisterPublicKeyCredential,
}
pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json<AddWebauthnRequest>,
pub async fn save_webauthn_factor(
user: CurrentUser,
form: web::Json<AddWebauthnRequest>,
users: web::Data<Addr<UsersActor>>,
manager: WebAuthManagerReq) -> impl Responder {
let key = match manager.finish_registration(
&user,
&form.0.opaque_state,
form.0.credential,
) {
manager: WebAuthManagerReq,
) -> impl Responder {
let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) {
Ok(k) => k,
Err(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,
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 {
HttpResponse::InternalServerError().body("Failed to update user information!")
@ -89,12 +100,19 @@ pub struct DeleteFactorRequest {
id: FactorID,
}
pub async fn delete_factor(user: CurrentUser, form: web::Json<DeleteFactorRequest>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
pub async fn delete_factor(
user: CurrentUser,
form: web::Json<DeleteFactorRequest>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let mut user = User::from(user);
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 {
HttpResponse::InternalServerError().body("Failed to update user information!")

View File

@ -1,6 +1,6 @@
use std::ops::Deref;
use actix_web::{HttpResponse, Responder, web};
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use qrcode_generator::QrCodeEcc;
@ -37,27 +37,25 @@ struct AddWebauhtnPage {
/// Manage two factors authentication methods route
pub async fn two_factors_route(user: CurrentUser) -> impl Responder {
HttpResponse::Ok()
.body(TwoFactorsPage {
_p: BaseSettingsPage::get(
"Two factor auth",
&user,
None,
None),
HttpResponse::Ok().body(
TwoFactorsPage {
_p: BaseSettingsPage::get("Two factor auth", &user, None, None),
user: user.deref(),
}.render().unwrap())
}
.render()
.unwrap(),
)
}
/// 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 qr_code = qrcode_generator::to_png_to_vec(
key.url_for_user(&user, &app_conf),
QrCodeEcc::Low,
1024,
);
let qr_code =
qrcode_generator::to_png_to_vec(key.url_for_user(&user, &app_conf), QrCodeEcc::Low, 1024);
let qr_code = match qr_code {
Ok(q) => q,
Err(e) => {
@ -66,26 +64,29 @@ pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppCon
}
};
HttpResponse::Ok()
.body(AddTotpPage {
_p: BaseSettingsPage::get(
"New authenticator app",
&user,
None,
None),
HttpResponse::Ok().body(
AddTotpPage {
_p: BaseSettingsPage::get("New authenticator app", &user, None, None),
qr_code: base64::encode(qr_code),
account_name: key.account_name(&user, &app_conf),
secret_key: key.get_secret(),
}.render().unwrap())
}
.render()
.unwrap(),
)
}
/// 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) {
Ok(r) => r,
Err(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()
.body(AddWebauhtnPage {
_p: BaseSettingsPage::get(
"New security key",
&user,
None,
None),
HttpResponse::Ok().body(
AddWebauhtnPage {
_p: BaseSettingsPage::get("New security key", &user, None, None),
opaque_state: registration_request.opaque_state,
challenge_json: urlencoding::encode(&challenge_json).to_string(),
}.render().unwrap())
}
.render()
.unwrap(),
)
}

View File

@ -27,7 +27,7 @@ impl AccessToken {
jwt_id: None,
nonce: self.nonce,
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() {
"plain" => code_verifer.eq(&self.code_challenge),
"S256" => {
let encoded = base64::encode_config(
sha256(code_verifer.as_bytes()),
URL_SAFE_NO_PAD,
);
let encoded =
base64::encode_config(sha256(code_verifer.as_bytes()), URL_SAFE_NO_PAD);
encoded.eq(&self.code_challenge)
}
@ -64,7 +62,10 @@ mod test {
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
};
assert_eq!(true, chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"));
assert_eq!(
true,
chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
);
assert_eq!(false, chal.verify_code("text1"));
}
}

View File

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

View File

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

View File

@ -3,7 +3,10 @@ use std::slice::{Iter, IterMut};
use crate::utils::err::Res;
enum FileFormat { Json, Yaml }
enum FileFormat {
Json,
Yaml,
}
pub struct EntityManager<E> {
file_path: PathBuf,
@ -11,7 +14,7 @@ pub struct EntityManager<E> {
}
impl<E> EntityManager<E>
where
where
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
{
/// Open entity
@ -30,7 +33,7 @@ impl<E> EntityManager<E>
file_path: path.as_ref().to_path_buf(),
list: match Self::file_format(path.as_ref()) {
FileFormat::Json => serde_json::from_str(&file_content)?,
FileFormat::Yaml => serde_yaml::from_str(&file_content)?
FileFormat::Yaml => serde_yaml::from_str(&file_content)?,
},
})
}
@ -49,7 +52,7 @@ impl<E> EntityManager<E>
fn file_format(p: &Path) -> FileFormat {
match p.to_string_lossy().ends_with(".json") {
true => FileFormat::Json,
false => FileFormat::Yaml
false => FileFormat::Yaml,
}
}

View File

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

View File

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

View File

@ -1,8 +1,8 @@
use std::net::IpAddr;
use actix_web::{Error, FromRequest, HttpRequest, web};
use actix_web::dev::Payload;
use futures_util::future::{Ready, ready};
use actix_web::{web, Error, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::data::app_config::AppConfig;
use crate::utils::network_utils::get_remote_ip;

View File

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

View File

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

View File

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

View File

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

View File

@ -4,19 +4,19 @@ use std::sync::Arc;
use actix::Actor;
use actix_identity::config::LogoutBehaviour;
use actix_identity::IdentityMiddleware;
use actix_session::SessionMiddleware;
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::middleware::Logger;
use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
use clap::Parser;
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
use basic_oidc::actors::users_actor::UsersActor;
use basic_oidc::constants::*;
use basic_oidc::controllers::*;
use basic_oidc::controllers::assets_controller::assets_route;
use basic_oidc::controllers::*;
use basic_oidc::data::app_config::AppConfig;
use basic_oidc::data::client::ClientManager;
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 bruteforce_actor = BruteForceActor::default().start();
let openid_sessions_actor = OpenIDSessionsActor::default().start();
let jwt_signer = JWTSigner::gen_from_memory()
.expect("Failed to generate JWKS key");
let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
let webauthn_manager = Arc::new(WebAuthManager::init(&config));
log::info!("Server will listen on {}", config.listen_address);
@ -84,9 +83,10 @@ async fn main() -> std::io::Result<()> {
.expect("Failed to load clients list!");
clients.apply_environment_variables();
let session_mw =
SessionMiddleware::builder(CookieSessionStore::default(),
Key::from(config.token_key.as_bytes()))
let session_mw = SessionMiddleware::builder(
CookieSessionStore::default(),
Key::from(config.token_key.as_bytes()),
)
.cookie_name(SESSION_COOKIE_NAME.to_string())
.cookie_secure(config.secure_cookie())
.cookie_same_site(SameSite::Lax)
@ -106,72 +106,143 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(clients))
.app_data(web::Data::new(jwt_signer.clone()))
.app_data(web::Data::new(webauthn_manager.clone()))
.wrap(middleware::DefaultHeaders::new()
.add(("Permissions-Policy", "interest-cohort=()")))
.wrap(
middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")),
)
.wrap(Logger::default())
.wrap(AuthMiddleware {})
.wrap(identity_middleware)
.wrap(session_mw)
// main route
.route("/", web::get()
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
.route(
"/",
web::get().to(|| async {
HttpResponse::Found()
.append_header(("Location", "/settings"))
.finish()
}),
)
.route("/robots.txt", web::get().to(assets_controller::robots_txt))
// health route
.service(health)
// Assets serving
.route("/assets/{path:.*}", web::get().to(assets_route))
// Login pages
.route("/logout", web::get().to(login_controller::logout_route))
.route("/login", web::get().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("/reset_password", web::post().to(login_controller::reset_password_route))
.route("/2fa_auth", web::get().to(login_controller::choose_2fa_method))
.route(
"/reset_password",
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::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
.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
.route("/settings", web::get().to(settings_controller::account_settings_details_route))
.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))
.route(
"/settings",
web::get().to(settings_controller::account_settings_details_route),
)
.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
.route("/settings/api/two_factor/save_totp_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))
.route(
"/settings/api/two_factor/save_totp_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
.route("/admin", web::get()
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
.route("/admin/clients", web::get().to(admin_controller::clients_route))
.route(
"/admin",
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::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))
.route(
"/admin/users",
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
.route("/admin/api/find_username", web::post().to(admin_api::find_username))
.route("/admin/api/delete_user", web::post().to(admin_api::delete_user))
.route(
"/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
.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(TOKEN_URI, web::post().to(openid_controller::token))
.route(CERT_URI, web::get().to(openid_controller::cert_uri))
.route(USERINFO_URI, web::post().to(openid_controller::user_info_post))
.route(USERINFO_URI, web::get().to(openid_controller::user_info_get))
.route(
USERINFO_URI,
web::post().to(openid_controller::user_info_post),
)
.route(
USERINFO_URI,
web::get().to(openid_controller::user_info_get),
)
})
.bind(listen_address)?
.run()

View File

@ -1,18 +1,20 @@
//! # Authentication middleware
use std::future::{Future, ready, Ready};
use std::future::{ready, Future, Ready};
use std::pin::Pin;
use std::rc::Rc;
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::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::data::app_config::AppConfig;
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
@ -27,8 +29,8 @@ pub struct AuthMiddleware;
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
@ -62,14 +64,13 @@ impl ConnStatus {
}
}
pub struct AuthInnerMiddleware<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
where
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
@ -77,7 +78,7 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
type Error = Error;
#[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);
@ -90,7 +91,8 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Check if POST request comes from another website (block invalid origins)
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 !o.to_str().unwrap_or("bad").eq(&config.website_origin) {
log::warn!(
@ -135,10 +137,13 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Redirect user to login page
if !session.is_auth()
&& (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.",
req.path());
log::debug!(
"Redirect unauthenticated user from {} to authorization route.",
req.path()
);
let path = req.uri().to_string();
return Ok(req
@ -149,10 +154,9 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Restrict access to admin pages
if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) {
return Ok(req
.into_response(
HttpResponse::Unauthorized().body(
build_fatal_error_page("You are not allowed to access this resource.")),
)
.into_response(HttpResponse::Unauthorized().body(build_fatal_error_page(
"You are not allowed to access this resource.",
)))
.map_into_right_body());
}

View File

@ -1,5 +1,5 @@
pub mod crypt_utils;
pub mod err;
pub mod time;
pub mod network_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
}
/// Get the remote IP address
pub fn get_remote_ip(req: &HttpRequest, proxy_ip: Option<&str>) -> IpAddr {
let mut ip = req.peer_addr().unwrap().ip();
@ -78,7 +77,10 @@ mod test {
let req = TestRequest::default()
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.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]
@ -87,7 +89,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1"))
.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]
@ -96,7 +101,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
.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]
@ -105,7 +113,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "10::1, 1.2.2.2"))
.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]
@ -114,7 +125,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
.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]
@ -123,7 +137,10 @@ mod test {
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
.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]
@ -141,7 +158,10 @@ mod test {
#[test]
fn parse_ip_v6_address() {
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]

View File

@ -16,7 +16,11 @@ pub fn apply_env_vars(val: &str) -> String {
let mut val = val.to_string();
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) {
Ok(v) => v,
Err(e) => {
@ -34,8 +38,8 @@ pub fn apply_env_vars(val: &str) -> String {
#[cfg(test)]
mod test {
use std::env;
use crate::utils::string_utils::apply_env_vars;
use std::env;
const VAR_ONE: &str = "VAR_ONE";
#[test]

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<p style="color:red" id="err_target"></p>
<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 style="margin: 10px 0px;">