Compare commits

...

3 Commits

Author SHA1 Message Date
feb6db09b9 Fix typo 2022-04-19 19:33:16 +02:00
806a085c97 Improve redirect URI management 2022-04-19 19:30:24 +02:00
ce7118ff81 Display form to enter OTP code 2022-04-19 19:24:07 +02:00
8 changed files with 98 additions and 24 deletions

View File

@ -8,40 +8,47 @@ use crate::actors::bruteforce_actor::BruteForceActor;
use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor};
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
use crate::controllers::base_controller::{FatalErrorPage, redirect_user, redirect_user_for_login};
use crate::data::login_redirect_query::LoginRedirectQuery;
use crate::data::login_redirect::LoginRedirect;
use crate::data::remote_ip::RemoteIP;
use crate::data::session_identity::{SessionIdentity, SessionStatus};
use crate::data::user::{TwoFactor, User};
use crate::data::user::{FactorID, TwoFactor, User};
struct BaseLoginPage {
struct BaseLoginPage<'a> {
danger: Option<String>,
success: Option<String>,
page_title: &'static str,
app_name: &'static str,
redirect_uri: String,
redirect_uri: &'a LoginRedirect,
}
#[derive(Template)]
#[template(path = "login/login.html")]
struct LoginTemplate {
_p: BaseLoginPage,
struct LoginTemplate<'a> {
_p: BaseLoginPage<'a>,
login: String,
}
#[derive(Template)]
#[template(path = "login/password_reset.html")]
struct PasswordResetTemplate {
_p: BaseLoginPage,
struct PasswordResetTemplate<'a> {
_p: BaseLoginPage<'a>,
min_pass_len: usize,
}
#[derive(Template)]
#[template(path = "login/choose_second_factor.html")]
struct ChooseSecondFactorTemplate<'a> {
_p: BaseLoginPage,
_p: BaseLoginPage<'a>,
factors: &'a [TwoFactor],
}
#[derive(Template)]
#[template(path = "login/opt_input.html")]
struct LoginWithOTPTemplate<'a> {
_p: BaseLoginPage<'a>,
factor: &'a TwoFactor,
}
#[derive(serde::Deserialize)]
pub struct LoginRequestBody {
@ -53,7 +60,7 @@ pub struct LoginRequestBody {
pub struct LoginRequestQuery {
logout: Option<bool>,
#[serde(default)]
redirect: LoginRedirectQuery,
redirect: LoginRedirect,
}
/// Authenticate user
@ -148,7 +155,7 @@ pub async fn login_route(
danger,
success,
app_name: APP_NAME,
redirect_uri: query.redirect.get_encoded(),
redirect_uri: &query.redirect,
},
login,
}
@ -170,7 +177,7 @@ pub struct ChangePasswordRequestBody {
#[derive(serde::Deserialize)]
pub struct PasswordResetQuery {
#[serde(default)]
redirect: LoginRedirectQuery,
redirect: LoginRedirect,
}
/// Reset user password route
@ -213,7 +220,7 @@ pub async fn reset_password_route(id: Identity, query: web::Query<PasswordResetQ
danger,
success: None,
app_name: APP_NAME,
redirect_uri: query.redirect.get_encoded(),
redirect_uri: &query.redirect,
},
min_pass_len: MIN_PASS_LEN,
}
@ -225,7 +232,7 @@ pub async fn reset_password_route(id: Identity, query: web::Query<PasswordResetQ
#[derive(serde::Deserialize)]
pub struct ChooseSecondFactorQuery {
#[serde(default)]
redirect: LoginRedirectQuery,
redirect: LoginRedirect,
}
@ -246,11 +253,47 @@ pub async fn choose_2fa_method(id: Identity, query: web::Query<ChooseSecondFacto
danger: None,
success: None,
app_name: APP_NAME,
redirect_uri: query.redirect.get_encoded(),
redirect_uri: &query.redirect,
},
factors: &user.two_factor,
}
.render()
.unwrap(),
)
}
#[derive(serde::Deserialize)]
pub struct LoginWithOTPQuery {
#[serde(default)]
redirect: LoginRedirect,
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,
},
factor,
}.render().unwrap())
}

View File

@ -1,7 +1,7 @@
#[derive(serde::Serialize, serde::Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct LoginRedirectQuery(String);
pub struct LoginRedirect(String);
impl LoginRedirectQuery {
impl LoginRedirect {
pub fn get(&self) -> &str {
match self.0.starts_with('/') && !self.0.starts_with("//") {
true => self.0.as_str(),
@ -14,7 +14,7 @@ impl LoginRedirectQuery {
}
}
impl Default for LoginRedirectQuery {
impl Default for LoginRedirect {
fn default() -> Self {
Self("/".to_string())
}

View File

@ -12,4 +12,4 @@ pub mod code_challenge;
pub mod open_id_user_info;
pub mod access_token;
pub mod totp_key;
pub mod login_redirect_query;
pub mod login_redirect;

View File

@ -1,5 +1,6 @@
use crate::data::client::ClientID;
use crate::data::entity_manager::EntityManager;
use crate::data::login_redirect::LoginRedirect;
use crate::data::totp_key::TotpKey;
use crate::utils::err::Res;
@ -27,10 +28,10 @@ impl TwoFactor {
}
}
pub fn login_url(&self, redirect_uri: &str) -> String {
pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String {
match self.kind {
TwoFactorType::TOTP(_) => format!("/2fa_totp?id={}&redirect_uri={}",
self.id.0, redirect_uri)
TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}",
self.id.0, redirect_uri.get_encoded())
}
}
}
@ -83,6 +84,10 @@ impl User {
pub fn remove_factor(&mut self, factor_id: FactorID) {
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 {

View File

@ -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::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))
// Logout page
.route("/logout", web::get().to(login_controller::logout_route))

View File

@ -1,6 +1,6 @@
{% extends "base_login_page.html" %}
{% block content %}
<form action="/login?redirect={{ _p.redirect_uri }}" method="post">
<form action="/login?redirect={{ _p.redirect_uri.get_encoded() }}" method="post">
<div>
<div class="form-floating">
<input name="login" type="text" required class="form-control" id="floatingName" placeholder="unsername"

View 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.get_encoded() }}">Sign in using another factor</a><br />
<a href="/logout">Sign out</a>
</div>
{% endblock content %}

View File

@ -1,6 +1,6 @@
{% extends "base_login_page.html" %}
{% block content %}
<form action="/reset_password?redirect={{ _p.redirect_uri }}" method="post" id="reset_password_form">
<form action="/reset_password?redirect={{ _p.redirect_uri.get_encoded() }}" method="post" id="reset_password_form">
<div>
<p>You need to configure a new password:</p>