Two factor authentication : TOTP #5
@ -11,7 +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};
|
use crate::data::user::{FactorID, TwoFactor, User};
|
||||||
|
|
||||||
struct BaseLoginPage {
|
struct BaseLoginPage {
|
||||||
danger: Option<String>,
|
danger: Option<String>,
|
||||||
@ -42,6 +42,13 @@ struct ChooseSecondFactorTemplate<'a> {
|
|||||||
factors: &'a [TwoFactor],
|
factors: &'a [TwoFactor],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "login/opt_input.html")]
|
||||||
|
struct LoginWithOTPTemplate<'a> {
|
||||||
|
_p: BaseLoginPage,
|
||||||
|
factor: &'a TwoFactor,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct LoginRequestBody {
|
pub struct LoginRequestBody {
|
||||||
@ -254,3 +261,39 @@ pub async fn choose_2fa_method(id: Identity, query: web::Query<ChooseSecondFacto
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct LoginWithOTPQuery {
|
||||||
|
#[serde(default)]
|
||||||
|
redirect: LoginRedirectQuery,
|
||||||
|
id: FactorID,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Login with OTP
|
||||||
|
pub async fn login_with_otp(id: Identity, query: web::Query<LoginWithOTPQuery>,
|
||||||
|
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!");
|
||||||
|
|
||||||
|
let factor = match user.find_factor(&query.id) {
|
||||||
|
Some(f) => f,
|
||||||
|
None => return HttpResponse::Ok()
|
||||||
|
.body(FatalErrorPage { message: "Factor not found!" }.render().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponse::Ok().body(LoginWithOTPTemplate {
|
||||||
|
_p: BaseLoginPage {
|
||||||
|
danger: None,
|
||||||
|
success: None,
|
||||||
|
page_title: "Two-Factor Auth",
|
||||||
|
app_name: APP_NAME,
|
||||||
|
redirect_uri: query.redirect.get_encoded(),
|
||||||
|
},
|
||||||
|
factor,
|
||||||
|
}.render().unwrap())
|
||||||
|
}
|
@ -29,7 +29,7 @@ impl TwoFactor {
|
|||||||
|
|
||||||
pub fn login_url(&self, redirect_uri: &str) -> String {
|
pub fn login_url(&self, redirect_uri: &str) -> String {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
TwoFactorType::TOTP(_) => format!("/2fa_totp?id={}&redirect_uri={}",
|
TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect_uri={}",
|
||||||
self.id.0, redirect_uri)
|
self.id.0, redirect_uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,6 +83,10 @@ impl User {
|
|||||||
pub fn remove_factor(&mut self, factor_id: FactorID) {
|
pub fn remove_factor(&mut self, factor_id: FactorID) {
|
||||||
self.two_factor.retain(|f| f.id != factor_id);
|
self.two_factor.retain(|f| f.id != factor_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> {
|
||||||
|
self.two_factor.iter().find(|f| f.id.eq(&factor_id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for User {
|
impl PartialEq for User {
|
||||||
|
@ -112,6 +112,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.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))
|
.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))
|
||||||
|
|
||||||
// Logout page
|
// Logout page
|
||||||
.route("/logout", web::get().to(login_controller::logout_route))
|
.route("/logout", web::get().to(login_controller::logout_route))
|
||||||
|
24
templates/login/opt_input.html
Normal file
24
templates/login/opt_input.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base_login_page.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Please go to your authenticator app <i>{{ factor.name }}</i>, generate a new code and enter it here:</p>
|
||||||
|
<form method="post" action="{{ factor.login_url(_p.redirect_uri) }}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="code" class="form-label mt-4">Generated code</label>
|
||||||
|
<input type="text" name="code" minlength="6" maxlength="6" class="form-control" id="code"
|
||||||
|
placeholder="XXXXXX">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="w-100 btn btn-lg btn-primary" type="submit" style="margin-top: 20px;">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<a href="/2fa_auth?redirect=_p.redirect_uri">Sign in using another factor</a><br />
|
||||||
|
<a href="/logout">Sign out</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
Loading…
Reference in New Issue
Block a user