Two factor authentication : TOTP #5
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)]
|
||||||
|
@ -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))
|
||||||
|
23
templates/login/choose_second_factor.html
Normal file
23
templates/login/choose_second_factor.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user