Can request new user password on login
This commit is contained in:
parent
0f4a5cde57
commit
4b8c9fdfdc
@ -1,7 +1,7 @@
|
|||||||
use actix::{Actor, Context, Handler, Message, MessageResult};
|
use actix::{Actor, Context, Handler, Message, MessageResult};
|
||||||
|
|
||||||
use crate::data::entity_manager::EntityManager;
|
use crate::data::entity_manager::EntityManager;
|
||||||
use crate::data::user::{User, verify_password};
|
use crate::data::user::{User, UserID, verify_password};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginResult {
|
pub enum LoginResult {
|
||||||
@ -17,6 +17,18 @@ pub struct LoginRequest {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChangePasswordResult(pub bool);
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(ChangePasswordResult)]
|
||||||
|
pub struct ChangePasswordRequest {
|
||||||
|
pub user_id: UserID,
|
||||||
|
pub new_password: String,
|
||||||
|
pub temporary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct UsersActor {
|
pub struct UsersActor {
|
||||||
manager: EntityManager<User>,
|
manager: EntityManager<User>,
|
||||||
}
|
}
|
||||||
@ -46,4 +58,13 @@ impl Handler<LoginRequest> for UsersActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<ChangePasswordRequest> for UsersActor {
|
||||||
|
type Result = MessageResult<ChangePasswordRequest>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: ChangePasswordRequest, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
MessageResult(ChangePasswordResult(
|
||||||
|
self.manager.change_user_password(&msg.user_id, &msg.new_password, msg.temporary)))
|
||||||
|
}
|
||||||
}
|
}
|
@ -12,4 +12,7 @@ pub const APP_NAME: &str = "Basic OIDC";
|
|||||||
pub const MAX_SESSION_DURATION: u64 = 60 * 30;
|
pub const MAX_SESSION_DURATION: u64 = 60 * 30;
|
||||||
|
|
||||||
/// Minimum interval between each last activity record in session
|
/// Minimum interval between each last activity record in session
|
||||||
pub const MIN_ACTIVITY_RECORD_TIME: u64 = 10;
|
pub const MIN_ACTIVITY_RECORD_TIME: u64 = 10;
|
||||||
|
|
||||||
|
/// Minimum password length
|
||||||
|
pub const MIN_PASS_LEN: usize = 4;
|
@ -3,11 +3,11 @@ use actix_identity::Identity;
|
|||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::actors::users_actor::{LoginResult, UsersActor};
|
use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor};
|
||||||
use crate::actors::users_actor;
|
use crate::actors::users_actor;
|
||||||
use crate::constants::APP_NAME;
|
use crate::constants::{APP_NAME, MIN_PASS_LEN};
|
||||||
use crate::controllers::base_controller::redirect_user;
|
use crate::controllers::base_controller::redirect_user;
|
||||||
use crate::data::session_identity::SessionIdentity;
|
use crate::data::session_identity::{SessionIdentity, SessionStatus};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "base_login_page.html")]
|
#[template(path = "base_login_page.html")]
|
||||||
@ -25,6 +25,13 @@ struct LoginTemplate {
|
|||||||
login: String,
|
login: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "password_reset.html")]
|
||||||
|
struct PasswordResetTemplate {
|
||||||
|
_parent: BaseLoginPage,
|
||||||
|
min_pass_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct LoginRequestBody {
|
pub struct LoginRequestBody {
|
||||||
login: String,
|
login: String,
|
||||||
@ -56,9 +63,29 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>,
|
|||||||
return redirect_user("/");
|
return redirect_user("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user is setting a new password
|
||||||
|
if let (Some(req), true) = (&req, SessionIdentity(&id).need_new_password()) {
|
||||||
|
if req.password.len() < MIN_PASS_LEN {
|
||||||
|
danger = "Password is too short!".to_string();
|
||||||
|
} else {
|
||||||
|
let res: ChangePasswordResult = users.send(users_actor::ChangePasswordRequest {
|
||||||
|
user_id: SessionIdentity(&id).user_id(),
|
||||||
|
new_password: req.password.clone(),
|
||||||
|
temporary: false,
|
||||||
|
}).await.unwrap();
|
||||||
|
|
||||||
|
if !res.0 {
|
||||||
|
danger = "Failed to change password!".to_string();
|
||||||
|
} else {
|
||||||
|
SessionIdentity(&id).set_status(SessionStatus::SignedIn);
|
||||||
|
return redirect_user("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try to authenticate user
|
// Try to authenticate user
|
||||||
if let Some(req) = &req {
|
else if let Some(req) = &req {
|
||||||
// TODO : check request origin
|
// TODO : check request origin (check for valid Referer)
|
||||||
|
|
||||||
login = req.login.clone();
|
login = req.login.clone();
|
||||||
let response: LoginResult = users.send(users_actor::LoginRequest {
|
let response: LoginResult = users.send(users_actor::LoginRequest {
|
||||||
@ -70,7 +97,11 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>,
|
|||||||
LoginResult::Success(user) => {
|
LoginResult::Success(user) => {
|
||||||
SessionIdentity(&id).set_user(&user);
|
SessionIdentity(&id).set_user(&user);
|
||||||
|
|
||||||
return redirect_user("/");
|
if user.need_reset_password {
|
||||||
|
SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword);
|
||||||
|
} else {
|
||||||
|
return redirect_user("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c => {
|
c => {
|
||||||
@ -81,6 +112,21 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display password reset form if it is appropriate
|
||||||
|
if SessionIdentity(&id).need_new_password() {
|
||||||
|
return HttpResponse::Ok()
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(PasswordResetTemplate {
|
||||||
|
_parent: BaseLoginPage {
|
||||||
|
page_title: "Password reset",
|
||||||
|
danger,
|
||||||
|
success,
|
||||||
|
app_name: APP_NAME,
|
||||||
|
},
|
||||||
|
min_pass_len: MIN_PASS_LEN,
|
||||||
|
}.render().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("text/html")
|
.content_type("text/html")
|
||||||
|
@ -46,6 +46,17 @@ impl<E> EntityManager<E> where E: serde::Serialize + serde::de::DeserializeOwned
|
|||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace entries in the list that matches a criteria
|
||||||
|
pub fn replace_entries<F>(&mut self, filter: F, el: &E) -> Res where F: Fn(&E) -> bool {
|
||||||
|
for i in 0..self.list.len() {
|
||||||
|
if filter(&self.list[i]) {
|
||||||
|
self.list[i] = el.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
}
|
||||||
|
|
||||||
/// Iterate over the entries of this entity manager
|
/// Iterate over the entries of this entity manager
|
||||||
pub fn iter(&self) -> Iter<'_, E> {
|
pub fn iter(&self) -> Iter<'_, E> {
|
||||||
self.list.iter()
|
self.list.iter()
|
||||||
|
@ -6,18 +6,28 @@ use crate::data::user::User;
|
|||||||
use crate::utils::time::time;
|
use crate::utils::time::time;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
enum SessionStatus {
|
pub enum SessionStatus {
|
||||||
|
Invalid,
|
||||||
SignedIn,
|
SignedIn,
|
||||||
NeedNewPassword,
|
NeedNewPassword,
|
||||||
NeedMFA,
|
NeedMFA,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
impl Default for SessionStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||||
struct SessionIdentityData {
|
struct SessionIdentityData {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
last_access: u64,
|
last_access: u64,
|
||||||
pub status: SessionStatus,
|
pub status: SessionStatus,
|
||||||
|
|
||||||
|
// TODO : add session max duration (1 day)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionIdentity<'a>(pub &'a Identity);
|
pub struct SessionIdentity<'a>(pub &'a Identity);
|
||||||
@ -39,6 +49,13 @@ impl<'a> SessionIdentity<'a> {
|
|||||||
.map(serde_json::from_str)
|
.map(serde_json::from_str)
|
||||||
.map(|f| f.expect("Failed to deserialize session data!"));
|
.map(|f| f.expect("Failed to deserialize session data!"));
|
||||||
|
|
||||||
|
// Check if session is valid
|
||||||
|
if let Some(sess) = &res {
|
||||||
|
if sess.id.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(session) = res.as_mut() {
|
if let Some(session) = res.as_mut() {
|
||||||
if session.last_access + MAX_SESSION_DURATION < time() {
|
if session.last_access + MAX_SESSION_DURATION < time() {
|
||||||
log::info!("Session is expired for {}", session.id);
|
log::info!("Session is expired for {}", session.id);
|
||||||
@ -63,9 +80,27 @@ impl<'a> SessionIdentity<'a> {
|
|||||||
self.0.remember(s);
|
self.0.remember(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_status(&self, status: SessionStatus) {
|
||||||
|
let mut sess = self.get_session_data().unwrap_or_default();
|
||||||
|
sess.status = status;
|
||||||
|
self.set_session_data(&sess);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_authenticated(&self) -> bool {
|
pub fn is_authenticated(&self) -> bool {
|
||||||
self.get_session_data()
|
self.get_session_data()
|
||||||
.map(|s| s.status == SessionStatus::SignedIn)
|
.map(|s| s.status == SessionStatus::SignedIn)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn need_new_password(&self) -> bool {
|
||||||
|
self.get_session_data()
|
||||||
|
.map(|s| s.status == SessionStatus::NeedNewPassword)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_id(&self) -> String {
|
||||||
|
self.get_session_data()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.id
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,9 +2,11 @@ use crate::data::entity_manager::EntityManager;
|
|||||||
use crate::data::service::ServiceID;
|
use crate::data::service::ServiceID;
|
||||||
use crate::utils::err::Res;
|
use crate::utils::err::Res;
|
||||||
|
|
||||||
|
pub type UserID = String;
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub uid: String,
|
pub uid: UserID,
|
||||||
pub first_name: String,
|
pub first_name: String,
|
||||||
pub last_last: String,
|
pub last_last: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
@ -67,4 +69,44 @@ impl EntityManager<User> {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_by_user_id(&self, id: &UserID) -> Option<User> {
|
||||||
|
for entry in self.iter() {
|
||||||
|
if entry.uid.eq(id) {
|
||||||
|
return Some(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update user information
|
||||||
|
fn update_user<F>(&mut self, id: &UserID, update: F) -> bool where F: FnOnce(User) -> User {
|
||||||
|
let user = match self.find_by_user_id(id) {
|
||||||
|
None => return false,
|
||||||
|
Some(user) => user
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = self.replace_entries(|u| u.uid.eq(id), &update(user)) {
|
||||||
|
log::error!("Failed to update user information! {:?}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_user_password(&mut self, id: &UserID, password: &str, temporary: bool) -> bool {
|
||||||
|
let new_hash = match hash_password(password) {
|
||||||
|
Ok(h) => { h }
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to hash user password! {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_user(id, |mut user| {
|
||||||
|
user.password = new_hash;
|
||||||
|
user.need_reset_password = temporary;
|
||||||
|
user
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
53
templates/password_reset.html
Normal file
53
templates/password_reset.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% extends "base_login_page.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<form action="/login" method="post" id="reset_password_form">
|
||||||
|
<div>
|
||||||
|
<p>You need to configure a new password:</p>
|
||||||
|
|
||||||
|
<p style="color:red" id="err_target"></p>
|
||||||
|
|
||||||
|
<!-- Needed for controller -->
|
||||||
|
<input type="hidden" name="login" value="."/>
|
||||||
|
|
||||||
|
<div class="form-floating">
|
||||||
|
<input name="password" type="password" required class="form-control" id="pass1"
|
||||||
|
placeholder="unsername"/>
|
||||||
|
<label for="pass1">New password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="password" required class="form-control" id="pass2"
|
||||||
|
placeholder="Password"/>
|
||||||
|
<label for="pass2">Confirm new password</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="w-100 btn btn-lg btn-primary" type="submit">Change password</button>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<a href="/logout">Sign out</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById("reset_password_form");
|
||||||
|
const error_target = document.getElementById("err_target");
|
||||||
|
form.addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
error_target.innerHTML = "";
|
||||||
|
const pass1 = document.getElementById("pass1");
|
||||||
|
const pass2 = document.getElementById("pass2");
|
||||||
|
|
||||||
|
if (pass1.value.length < {{ min_pass_len }})
|
||||||
|
error_target.innerHTML = "Your password must have at least {{ min_pass_len }} characters!";
|
||||||
|
else if (pass1.value !== pass2.value)
|
||||||
|
error_target.innerHTML = "The password and its confirmation are not the same !";
|
||||||
|
else
|
||||||
|
form.submit();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock content %}
|
Loading…
Reference in New Issue
Block a user