Two factor authentication : TOTP #5
@ -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;
|
@ -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 {
|
||||||
@ -39,3 +44,22 @@ pub async fn save_totp_factor(user: CurrentUser, form: web::Json<Request>,
|
|||||||
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!")
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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',
|
||||||
|
@ -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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user