Two factor authentication : TOTP #5

Merged
pierre merged 22 commits from twofactors into master 2022-04-20 07:40:51 +00:00
6 changed files with 104 additions and 9 deletions
Showing only changes of commit 630ebe2ddd - Show all commits

View File

@ -6,4 +6,4 @@ pub mod admin_controller;
pub mod admin_api; pub mod admin_api;
pub mod openid_controller; pub mod openid_controller;
pub mod two_factors_controller; pub mod two_factors_controller;
pub mod two_factors_api; pub mod two_factor_api;

View File

@ -1,20 +1,21 @@
use actix::Addr; use actix::Addr;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use uuid::Uuid;
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::UsersActor;
use crate::data::current_user::CurrentUser; use crate::data::current_user::CurrentUser;
use crate::data::totp_key::TotpKey; use crate::data::totp_key::TotpKey;
use crate::data::user::{SecondFactor, User}; use crate::data::user::{FactorID, SecondFactor, SecondFactorType, User};
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct Request { pub struct AddTOTPRequest {
factor_name: String, factor_name: String,
secret: String, secret: String,
first_code: String, first_code: String,
} }
pub async fn save_totp_factor(user: CurrentUser, form: web::Json<Request>, pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>,
users: web::Data<Addr<UsersActor>>) -> impl Responder { users: web::Data<Addr<UsersActor>>) -> impl Responder {
let key = TotpKey::from_encoded_secret(&form.secret); let key = TotpKey::from_encoded_secret(&form.secret);
@ -30,7 +31,11 @@ pub async fn save_totp_factor(user: CurrentUser, form: web::Json<Request>,
} }
let mut user = User::from(user); let mut user = User::from(user);
user.add_factor(SecondFactor::TOTP(key)); user.add_factor(SecondFactor {
id: FactorID(Uuid::new_v4().to_string()),
name: form.0.factor_name,
kind: SecondFactorType::TOTP(key),
});
let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0;
if !res { if !res {
@ -38,4 +43,23 @@ pub async fn save_totp_factor(user: CurrentUser, form: web::Json<Request>,
} else { } else {
HttpResponse::Ok().body("Added new factor!") HttpResponse::Ok().body("Added new factor!")
} }
}
#[derive(serde::Deserialize)]
pub struct DeleteFactorRequest {
id: FactorID,
}
pub async fn delete_factor(user: CurrentUser, form: web::Json<DeleteFactorRequest>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
let mut user = User::from(user);
user.remove_factor(form.0.id);
let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0;
if !res {
HttpResponse::InternalServerError().body("Failed to update user information!")
} else {
HttpResponse::Ok().body("Removed factor!")
}
} }

View File

@ -5,11 +5,29 @@ use crate::utils::err::Res;
pub type UserID = String; pub type UserID = String;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FactorID(pub String);
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum SecondFactor { pub enum SecondFactorType {
TOTP(TotpKey) TOTP(TotpKey)
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SecondFactor {
pub id: FactorID,
pub name: String,
pub kind: SecondFactorType,
}
impl SecondFactor {
pub fn type_str(&self) -> &'static str {
match self.kind {
SecondFactorType::TOTP(_) => "Authenticator app"
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct User { pub struct User {
pub uid: UserID, pub uid: UserID,
@ -53,6 +71,12 @@ impl User {
self.second_factors.as_mut().unwrap().push(factor); self.second_factors.as_mut().unwrap().push(factor);
} }
pub fn remove_factor(&mut self, factor_id: FactorID) {
if let Some(f) = self.second_factors.as_mut() {
f.retain(|f| f.id != factor_id);
}
}
} }
impl PartialEq for User { impl PartialEq for User {

View File

@ -122,7 +122,8 @@ async fn main() -> std::io::Result<()> {
.route("/settings/two_factors/add_totp", web::get().to(two_factors_controller::add_totp_factor_route)) .route("/settings/two_factors/add_totp", web::get().to(two_factors_controller::add_totp_factor_route))
// User API // User API
.route("/settings/api/two_factors/save_totp_factor", web::post().to(two_factors_api::save_totp_factor)) .route("/settings/api/two_factor/save_totp_factor", web::post().to(two_factor_api::save_totp_factor))
.route("/settings/api/two_factor/delete_factor", web::post().to(two_factor_api::delete_factor))
// Admin routes // Admin routes
.route("/admin", web::get() .route("/admin", web::get()

View File

@ -75,7 +75,7 @@
return; return;
try { try {
const res = await fetch("/settings/api/two_factors/save_totp_factor", { const res = await fetch("/settings/api/two_factor/save_totp_factor", {
method: "post", method: "post",
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -13,6 +13,52 @@
<p> <p>
<a href="/settings/two_factors/add_totp" type="button" class="btn btn-primary">Add Authenticator App</a> <a href="/settings/two_factors/add_totp" type="button" class="btn btn-primary">Add Authenticator App</a>
</p> </p>
TODO : show the list of currently registered 2 factors methods
<table class="table table-hover" style="max-width: 800px;" aria-describedby="Factors list">
<thead>
<tr>
<th scope="col">Factor type</th>
<th scope="col">Name</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for f in user.second_factors.as_deref().unwrap_or_default() %}
<tr id="factor-{{ f.id.0 }}">
<td>{{ f.type_str() }}</td>
<td>{{ f.name }}</td>
<td><a href="javascript:delete_factor('{{ f.id.0 }}');">Delete</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
async function delete_factor(id) {
if (!confirm("Do you really want to remove this factor?"))
return;
try {
const res = await fetch("/settings/api/two_factor/delete_factor", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: id,
})
});
let text = await res.text();
alert(text);
if (res.status == 200)
document.getElementById("factor-" + id).remove();
} catch(e) {
console.error(e);
alert("Failed to remove factor!");
}
}
</script>
{% endblock content %} {% endblock content %}