Add a page to choose second factor

This commit is contained in:
Pierre HUBERT 2022-04-19 18:27:21 +02:00
parent 3add7a5d37
commit c1677071fc
5 changed files with 89 additions and 2 deletions

View File

@ -11,6 +11,7 @@ use crate::controllers::base_controller::{FatalErrorPage, redirect_user, redirec
use crate::data::login_redirect_query::LoginRedirectQuery; use crate::data::login_redirect_query::LoginRedirectQuery;
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::{TwoFactor, User};
struct BaseLoginPage { struct BaseLoginPage {
danger: Option<String>, danger: Option<String>,
@ -34,6 +35,14 @@ struct PasswordResetTemplate {
min_pass_len: usize, min_pass_len: usize,
} }
#[derive(Template)]
#[template(path = "login/choose_second_factor.html")]
struct ChooseSecondFactorTemplate<'a> {
_p: BaseLoginPage,
factors: &'a [TwoFactor],
}
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct LoginRequestBody { pub struct LoginRequestBody {
login: String, login: String,
@ -87,6 +96,11 @@ pub async fn login_route(
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
if SessionIdentity(&id).need_2fa_auth() {
return redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded()));
}
// Try to authenticate user // Try to authenticate user
if let Some(req) = &req { if let Some(req) = &req {
login = req.login.clone(); login = req.login.clone();
@ -105,6 +119,9 @@ pub async fn login_route(
return if user.need_reset_password { return if user.need_reset_password {
SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword);
redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded())) redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded()))
} else if user.has_two_factor() {
SessionIdentity(&id).set_status(SessionStatus::Need2FA);
redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded()))
} else { } else {
redirect_user(query.redirect.get()) redirect_user(query.redirect.get())
}; };
@ -159,7 +176,7 @@ pub struct PasswordResetQuery {
/// Reset user password route /// Reset user password route
pub async fn reset_password_route(id: Identity, query: web::Query<PasswordResetQuery>, pub async fn reset_password_route(id: Identity, query: web::Query<PasswordResetQuery>,
req: Option<web::Form<ChangePasswordRequestBody>>, req: Option<web::Form<ChangePasswordRequestBody>>,
users: web::Data<Addr<UsersActor>>, ) -> impl Responder { users: web::Data<Addr<UsersActor>>) -> impl Responder {
let mut danger = None; let mut danger = None;
if !SessionIdentity(&id).need_new_password() { if !SessionIdentity(&id).need_new_password() {
@ -204,3 +221,36 @@ pub async fn reset_password_route(id: Identity, query: web::Query<PasswordResetQ
.unwrap(), .unwrap(),
) )
} }
#[derive(serde::Deserialize)]
pub struct ChooseSecondFactorQuery {
#[serde(default)]
redirect: LoginRedirectQuery,
}
/// Let the user select the factor to use to authenticate
pub async fn choose_2fa_method(id: Identity, query: web::Query<ChooseSecondFactorQuery>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
if !SessionIdentity(&id).need_2fa_auth() {
return redirect_user_for_login(query.redirect.get());
}
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id()))
.await.unwrap().0.expect("Could not find user!");
HttpResponse::Ok().content_type("text/html").body(
ChooseSecondFactorTemplate {
_p: BaseLoginPage {
page_title: "Two factor authentication",
danger: None,
success: None,
app_name: APP_NAME,
redirect_uri: query.redirect.get_encoded(),
},
factors: &user.two_factor,
}
.render()
.unwrap(),
)
}

View File

@ -9,7 +9,7 @@ pub enum SessionStatus {
Invalid, Invalid,
SignedIn, SignedIn,
NeedNewPassword, NeedNewPassword,
NeedMFA, Need2FA,
} }
impl Default for SessionStatus { impl Default for SessionStatus {
@ -89,6 +89,12 @@ impl<'a> SessionIdentity<'a> {
.unwrap_or(false) .unwrap_or(false)
} }
pub fn need_2fa_auth(&self) -> bool {
self.get_session_data()
.map(|s| s.status == SessionStatus::Need2FA)
.unwrap_or(false)
}
pub fn is_admin(&self) -> bool { pub fn is_admin(&self) -> bool {
self.get_session_data().unwrap_or_default().is_admin self.get_session_data().unwrap_or_default().is_admin
} }

View File

@ -26,6 +26,13 @@ impl TwoFactor {
TwoFactorType::TOTP(_) => "Authenticator app" TwoFactorType::TOTP(_) => "Authenticator app"
} }
} }
pub fn login_url(&self, redirect_uri: &str) -> String {
match self.kind {
TwoFactorType::TOTP(_) => format!("/2fa_totp?id={}&redirect_uri={}",
self.id.0, redirect_uri)
}
}
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]

View File

@ -111,6 +111,7 @@ async fn main() -> std::io::Result<()> {
.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("/reset_password", web::get().to(login_controller::reset_password_route))
.route("/reset_password", web::post().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))
// Logout page // Logout page
.route("/logout", web::get().to(login_controller::logout_route)) .route("/logout", web::get().to(login_controller::logout_route))

View File

@ -0,0 +1,23 @@
{% extends "base_login_page.html" %}
{% block content %}
<div>
<p>You need to validate a second factor to validate 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>
</a>
</p>
{% endfor %}
</div>
<div style="margin-top: 10px;">
<a href="/logout">Sign out</a>
</div>
{% endblock content %}