Merge factors type for authentication
This commit is contained in:
parent
8d231c0b45
commit
af383720b7
3
assets/img/key.svg
Normal file
3
assets/img/key.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
|
||||||
|
<path fill="#FFFFFF" d="M14 27.4q-1.4 0-2.4-1t-1-2.4q0-1.4 1-2.4t2.4-1q1.4 0 2.4 1t1 2.4q0 1.4-1 2.4t-2.4 1Zm0 8.6q-5 0-8.5-3.5T2 24q0-5 3.5-8.5T14 12q3.6 0 6.3 1.7 2.7 1.7 4.25 5.15h17.8L48 24.5l-8.35 7.65-4.4-3.2-4.4 3.2-3.75-3h-2.55q-1.25 3-3.925 4.925Q17.95 36 14 36Zm0-3q2.9 0 5.35-1.925 2.45-1.925 3.15-4.925h5.7l2.7 2.25 4.4-3.15 4.1 3.1 4.25-3.95-2.55-2.55H22.5q-.6-2.8-3-4.825Q17.1 15 14 15q-3.75 0-6.375 2.625T5 24q0 3.75 2.625 6.375T14 33Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 528 B |
3
assets/img/pin.svg
Normal file
3
assets/img/pin.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
|
||||||
|
<path fill="#FFFFFF" d="M7 40q-1.2 0-2.1-.9Q4 38.2 4 37V11q0-1.2.9-2.1Q5.8 8 7 8h34q1.2 0 2.1.9.9.9.9 2.1v26q0 1.2-.9 2.1-.9.9-2.1.9Zm5.35-9.7h2.1V17.7h-1.6L9.2 20.15l1.05 1.65 2.1-1.4Zm6.65 0h8.15v-1.9H21.7v-.1q.7-.6 1.525-1.325T24.7 25.6q1.05-1 1.65-1.925.6-.925.6-2.225 0-1.6-1.1-2.675Q24.75 17.7 23 17.7q-1.5 0-2.525.7t-1.525 1.85l1.9.9q.25-.55.85-.95.6-.4 1.3-.4.9 0 1.4.5.5.5.5 1.2 0 1-.5 1.675T23.05 24.6l-2.025 1.85q-.875.8-2.025 1.9Zm15.7 0q1.8 0 2.975-.875T38.85 26.7q0-1.25-.65-2.05-.65-.8-1.7-.9v-.05q.95-.3 1.425-.975.475-.675.475-1.775 0-1.5-1.075-2.375T34.65 17.7q-1.1 0-2.1.6t-1.65 1.75l1.7.9q.4-.55.925-.875.525-.325 1.125-.325.75 0 1.225.425.475.425.475 1.175 0 .85-.575 1.15-.575.3-1.525.3h-.7v1.95h.75q.95 0 1.725.5.775.5.775 1.45 0 .9-.575 1.375t-1.525.475q-.75 0-1.45-.5t-1-1.35l-1.75.65q.4 1.35 1.425 2.15 1.025.8 2.775.8ZM7 37h34V11H7v26Zm0 0V11v26Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 951 B |
@ -19,7 +19,6 @@ pub struct CountFailedAttempt {
|
|||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct BruteForceActor {
|
pub struct BruteForceActor {
|
||||||
failed_attempts: HashMap<IpAddr, Vec<u64>>,
|
failed_attempts: HashMap<IpAddr, Vec<u64>>,
|
||||||
@ -28,10 +27,7 @@ pub struct BruteForceActor {
|
|||||||
impl BruteForceActor {
|
impl BruteForceActor {
|
||||||
pub fn clean_attempts(&mut self) {
|
pub fn clean_attempts(&mut self) {
|
||||||
#[allow(clippy::map_clone)]
|
#[allow(clippy::map_clone)]
|
||||||
let keys = self.failed_attempts
|
let keys = self.failed_attempts.keys().map(|i| *i).collect::<Vec<_>>();
|
||||||
.keys()
|
|
||||||
.map(|i| *i)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for ip in keys {
|
for ip in keys {
|
||||||
// Remove old attempts
|
// Remove old attempts
|
||||||
@ -102,7 +98,9 @@ mod test {
|
|||||||
let mut actor = BruteForceActor::default();
|
let mut actor = BruteForceActor::default();
|
||||||
actor.failed_attempts.insert(IP_1, vec![1, 10]);
|
actor.failed_attempts.insert(IP_1, vec![1, 10]);
|
||||||
actor.failed_attempts.insert(IP_2, vec![1, 10, time() + 10]);
|
actor.failed_attempts.insert(IP_2, vec![1, 10, time() + 10]);
|
||||||
actor.failed_attempts.insert(IP_3, vec![time() + 10, time() + 20]);
|
actor
|
||||||
|
.failed_attempts
|
||||||
|
.insert(IP_3, vec![time() + 10, time() + 20]);
|
||||||
|
|
||||||
actor.clean_attempts();
|
actor.clean_attempts();
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
pub mod users_actor;
|
|
||||||
pub mod bruteforce_actor;
|
pub mod bruteforce_actor;
|
||||||
pub mod openid_sessions_actor;
|
pub mod openid_sessions_actor;
|
||||||
|
pub mod users_actor;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix::{Actor, AsyncContext, Context, Handler};
|
|
||||||
use actix::Message;
|
use actix::Message;
|
||||||
|
use actix::{Actor, AsyncContext, Context, Handler};
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::data::access_token::AccessToken;
|
use crate::data::access_token::AccessToken;
|
||||||
@ -37,13 +37,16 @@ pub struct Session {
|
|||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
self.authorization_code_expire_at < time() && self.access_token_expire_at < time()
|
self.authorization_code_expire_at < time()
|
||||||
|
&& self.access_token_expire_at < time()
|
||||||
&& self.refresh_token_expire_at < time()
|
&& self.refresh_token_expire_at < time()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn regenerate_access_and_refresh_tokens(&mut self,
|
pub fn regenerate_access_and_refresh_tokens(
|
||||||
|
&mut self,
|
||||||
app_config: &AppConfig,
|
app_config: &AppConfig,
|
||||||
jwt_signer: &JWTSigner) -> Res {
|
jwt_signer: &JWTSigner,
|
||||||
|
) -> Res {
|
||||||
let access_token = AccessToken {
|
let access_token = AccessToken {
|
||||||
issuer: app_config.website_origin.to_string(),
|
issuer: app_config.website_origin.to_string(),
|
||||||
subject_identifier: self.user.clone().0,
|
subject_identifier: self.user.clone().0,
|
||||||
@ -116,7 +119,11 @@ impl Handler<PushNewSession> for OpenIDSessionsActor {
|
|||||||
impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
|
impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
|
||||||
type Result = Option<Session>;
|
type Result = Option<Session>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: FindSessionByAuthorizationCode, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: FindSessionByAuthorizationCode,
|
||||||
|
_ctx: &mut Self::Context,
|
||||||
|
) -> Self::Result {
|
||||||
self.session
|
self.session
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.authorization_code.eq(&msg.0))
|
.find(|f| f.authorization_code.eq(&msg.0))
|
||||||
@ -141,7 +148,12 @@ impl Handler<FindSessionByAccessToken> for OpenIDSessionsActor {
|
|||||||
fn handle(&mut self, msg: FindSessionByAccessToken, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: FindSessionByAccessToken, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
self.session
|
self.session
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.access_token.as_ref().map(|t| t.eq(&msg.0)).unwrap_or(false))
|
.find(|f| {
|
||||||
|
f.access_token
|
||||||
|
.as_ref()
|
||||||
|
.map(|t| t.eq(&msg.0))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,8 +162,13 @@ impl Handler<UpdateSession> for OpenIDSessionsActor {
|
|||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
if let Some(r) = self.session.iter().enumerate()
|
if let Some(r) = self
|
||||||
.find(|f| f.1.session_id.eq(&msg.0.session_id)).map(|f| f.0) {
|
.session
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|f| f.1.session_id.eq(&msg.0.session_id))
|
||||||
|
.map(|f| f.0)
|
||||||
|
{
|
||||||
self.session[r] = msg.0;
|
self.session[r] = msg.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,9 @@ impl Handler<FindUserByUsername> for UsersActor {
|
|||||||
type Result = MessageResult<FindUserByUsername>;
|
type Result = MessageResult<FindUserByUsername>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: FindUserByUsername, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: FindUserByUsername, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
MessageResult(FindUserByUsernameResult(self.manager.find_by_username_or_email(&msg.0)))
|
MessageResult(FindUserByUsernameResult(
|
||||||
|
self.manager.find_by_username_or_email(&msg.0),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,10 +157,13 @@ impl Handler<DeleteUserRequest> for UsersActor {
|
|||||||
fn handle(&mut self, msg: DeleteUserRequest, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: DeleteUserRequest, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let user = match self.manager.find_by_user_id(&msg.0) {
|
let user = match self.manager.find_by_user_id(&msg.0) {
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Could not delete account {:?} because it was not found!", msg.0);
|
log::warn!(
|
||||||
|
"Could not delete account {:?} because it was not found!",
|
||||||
|
msg.0
|
||||||
|
);
|
||||||
return MessageResult(DeleteUserResult(false));
|
return MessageResult(DeleteUserResult(false));
|
||||||
}
|
}
|
||||||
Some(s) => s
|
Some(s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
MessageResult(DeleteUserResult(match self.manager.remove(&user) {
|
MessageResult(DeleteUserResult(match self.manager.remove(&user) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
|
||||||
use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor};
|
use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor};
|
||||||
use crate::data::current_user::CurrentUser;
|
use crate::data::current_user::CurrentUser;
|
||||||
@ -15,10 +15,16 @@ struct FindUserResult {
|
|||||||
user_id: Option<String>,
|
user_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_username(req: web::Form<FindUserNameReq>, users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
pub async fn find_username(
|
||||||
let res = users.send(FindUserByUsername(req.0.username)).await.unwrap();
|
req: web::Form<FindUserNameReq>,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let res = users
|
||||||
|
.send(FindUserByUsername(req.0.username))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
HttpResponse::Ok().json(FindUserResult {
|
HttpResponse::Ok().json(FindUserResult {
|
||||||
user_id: res.0.map(|r| r.uid.0)
|
user_id: res.0.map(|r| r.uid.0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,9 +33,11 @@ pub struct DeleteUserReq {
|
|||||||
user_id: UserID,
|
user_id: UserID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user(
|
||||||
pub async fn delete_user(user: CurrentUser, req: web::Form<DeleteUserReq>,
|
user: CurrentUser,
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
req: web::Form<DeleteUserReq>,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
if user.uid == req.user_id {
|
if user.uid == req.user_id {
|
||||||
return HttpResponse::BadRequest().body("You can not remove your own account!");
|
return HttpResponse::BadRequest().body("You can not remove your own account!");
|
||||||
}
|
}
|
||||||
@ -41,4 +49,3 @@ pub async fn delete_user(user: CurrentUser, req: web::Form<DeleteUserReq>,
|
|||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::actors::users_actor;
|
use crate::actors::users_actor;
|
||||||
@ -35,17 +35,15 @@ struct EditUserTemplate {
|
|||||||
clients: Vec<Client>,
|
clients: Vec<Client>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
|
pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
|
||||||
HttpResponse::Ok().body(ClientsListTemplate {
|
HttpResponse::Ok().body(
|
||||||
_p: BaseSettingsPage::get(
|
ClientsListTemplate {
|
||||||
"Clients list",
|
_p: BaseSettingsPage::get("Clients list", &user, None, None),
|
||||||
&user,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
clients: clients.cloned(),
|
clients: clients.cloned(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
@ -63,13 +61,20 @@ pub struct UpdateUserQuery {
|
|||||||
two_factor: String,
|
two_factor: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>, update_query: Option<web::Form<UpdateUserQuery>>) -> impl Responder {
|
pub async fn users_route(
|
||||||
|
user: CurrentUser,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
update_query: Option<web::Form<UpdateUserQuery>>,
|
||||||
|
) -> impl Responder {
|
||||||
let mut danger = None;
|
let mut danger = None;
|
||||||
let mut success = None;
|
let mut success = None;
|
||||||
|
|
||||||
if let Some(update) = update_query {
|
if let Some(update) = update_query {
|
||||||
let current_user: Option<User> = users.send(users_actor::FindUserByUsername(update.username.to_string()))
|
let current_user: Option<User> = users
|
||||||
.await.unwrap().0;
|
.send(users_actor::FindUserByUsername(update.username.to_string()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
let is_creating = current_user.is_none();
|
let is_creating = current_user.is_none();
|
||||||
|
|
||||||
let mut user = current_user.unwrap_or_default();
|
let mut user = current_user.unwrap_or_default();
|
||||||
@ -82,67 +87,84 @@ pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>,
|
|||||||
user.admin = update.0.admin.is_some();
|
user.admin = update.0.admin.is_some();
|
||||||
|
|
||||||
let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>();
|
let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>();
|
||||||
user.two_factor.retain(|f| factors_to_keep.contains(&f.id.0.as_str()));
|
user.two_factor
|
||||||
|
.retain(|f| factors_to_keep.contains(&f.id.0.as_str()));
|
||||||
|
|
||||||
user.authorized_clients = match update.0.grant_type.as_str() {
|
user.authorized_clients = match update.0.grant_type.as_str() {
|
||||||
"all_clients" => None,
|
"all_clients" => None,
|
||||||
"custom_clients" => Some(update.0.granted_clients.split(',')
|
"custom_clients" => Some(
|
||||||
|
update
|
||||||
|
.0
|
||||||
|
.granted_clients
|
||||||
|
.split(',')
|
||||||
.map(|c| ClientID(c.to_string()))
|
.map(|c| ClientID(c.to_string()))
|
||||||
.collect::<Vec<_>>()),
|
.collect::<Vec<_>>(),
|
||||||
_ => Some(Vec::new())
|
),
|
||||||
|
_ => Some(Vec::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_password = match update.0.gen_new_password.is_some() {
|
let new_password = match update.0.gen_new_password.is_some() {
|
||||||
false => None,
|
false => None,
|
||||||
true => {
|
true => {
|
||||||
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
|
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
|
||||||
user.password = hash_password(&temp_pass)
|
user.password = hash_password(&temp_pass).expect("Failed to hash password");
|
||||||
.expect("Failed to hash password");
|
|
||||||
user.need_reset_password = true;
|
user.need_reset_password = true;
|
||||||
Some(temp_pass)
|
Some(temp_pass)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = users.send(users_actor::UpdateUserRequest(user.clone())).await.unwrap().0;
|
let res = users
|
||||||
|
.send(users_actor::UpdateUserRequest(user.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
if !res {
|
if !res {
|
||||||
danger = Some(match is_creating {
|
danger = Some(
|
||||||
|
match is_creating {
|
||||||
true => "Failed to create user!",
|
true => "Failed to create user!",
|
||||||
false => "Failed to update user!"
|
false => "Failed to update user!",
|
||||||
}.to_string())
|
}
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
success = Some(match is_creating {
|
success = Some(match is_creating {
|
||||||
true => format!("User {} was successfully created!", user.full_name()),
|
true => format!("User {} was successfully created!", user.full_name()),
|
||||||
false => format!("User {} was successfully updated!", user.full_name())
|
false => format!("User {} was successfully updated!", user.full_name()),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(pass) = new_password {
|
if let Some(pass) = new_password {
|
||||||
danger = Some(format!("{}'s temporary password is {}", user.full_name(), pass));
|
danger = Some(format!(
|
||||||
|
"{}'s temporary password is {}",
|
||||||
|
user.full_name(),
|
||||||
|
pass
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0;
|
let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0;
|
||||||
|
|
||||||
HttpResponse::Ok().body(UsersListTemplate {
|
HttpResponse::Ok().body(
|
||||||
_p: BaseSettingsPage::get(
|
UsersListTemplate {
|
||||||
"Users list",
|
_p: BaseSettingsPage::get("Users list", &user, danger, success),
|
||||||
&user,
|
|
||||||
danger,
|
|
||||||
success,
|
|
||||||
),
|
|
||||||
users,
|
users,
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
|
pub async fn create_user(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
|
||||||
HttpResponse::Ok().body(EditUserTemplate {
|
HttpResponse::Ok().body(
|
||||||
|
EditUserTemplate {
|
||||||
_p: BaseSettingsPage::get("Create a new user", user.deref(), None, None),
|
_p: BaseSettingsPage::get("Create a new user", user.deref(), None, None),
|
||||||
u: Default::default(),
|
u: Default::default(),
|
||||||
clients: clients.cloned(),
|
clients: clients.cloned(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@ -150,26 +172,33 @@ pub struct EditUserQuery {
|
|||||||
id: UserID,
|
id: UserID,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn edit_user(user: CurrentUser,
|
pub async fn edit_user(
|
||||||
|
user: CurrentUser,
|
||||||
clients: web::Data<ClientManager>,
|
clients: web::Data<ClientManager>,
|
||||||
users: web::Data<Addr<UsersActor>>,
|
users: web::Data<Addr<UsersActor>>,
|
||||||
query: web::Query<EditUserQuery>,
|
query: web::Query<EditUserQuery>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let edited_account = users.send(users_actor::GetUserRequest(query.0.id))
|
let edited_account = users
|
||||||
.await.unwrap().0;
|
.send(users_actor::GetUserRequest(query.0.id))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
HttpResponse::Ok().body(
|
||||||
HttpResponse::Ok().body(EditUserTemplate {
|
EditUserTemplate {
|
||||||
_p: BaseSettingsPage::get(
|
_p: BaseSettingsPage::get(
|
||||||
"Edit user account",
|
"Edit user account",
|
||||||
user.deref(),
|
user.deref(),
|
||||||
match edited_account.is_none() {
|
match edited_account.is_none() {
|
||||||
true => Some("Could not find requested user!".to_string()),
|
true => Some("Could not find requested user!".to_string()),
|
||||||
false => None
|
false => None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
u: edited_account.unwrap_or_default(),
|
u: edited_account.unwrap_or_default(),
|
||||||
clients: clients.cloned(),
|
clients: clients.cloned(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use actix_web::{HttpResponse, web};
|
use actix_web::{web, HttpResponse};
|
||||||
use include_dir::{Dir, include_dir};
|
use include_dir::{include_dir, Dir};
|
||||||
|
|
||||||
/// Assets directory
|
/// Assets directory
|
||||||
static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets");
|
static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets");
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||||
use webauthn_rs::prelude::PublicKeyCredential;
|
use webauthn_rs::prelude::PublicKeyCredential;
|
||||||
|
|
||||||
use crate::data::session_identity::{SessionIdentity, SessionStatus};
|
use crate::data::session_identity::{SessionIdentity, SessionStatus};
|
||||||
@ -11,10 +11,12 @@ pub struct AuthWebauthnRequest {
|
|||||||
credential: PublicKeyCredential,
|
credential: PublicKeyCredential,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn auth_webauthn(id: Identity,
|
pub async fn auth_webauthn(
|
||||||
|
id: Identity,
|
||||||
req: web::Json<AuthWebauthnRequest>,
|
req: web::Json<AuthWebauthnRequest>,
|
||||||
manager: WebAuthManagerReq,
|
manager: WebAuthManagerReq,
|
||||||
http_req: HttpRequest) -> impl Responder {
|
http_req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
if !SessionIdentity(Some(&id)).need_2fa_auth() {
|
if !SessionIdentity(Some(&id)).need_2fa_auth() {
|
||||||
return HttpResponse::Unauthorized().json("No 2FA required!");
|
return HttpResponse::Unauthorized().json("No 2FA required!");
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::actors::{bruteforce_actor, users_actor};
|
|
||||||
use crate::actors::bruteforce_actor::BruteForceActor;
|
use crate::actors::bruteforce_actor::BruteForceActor;
|
||||||
use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor};
|
use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor};
|
||||||
|
use crate::actors::{bruteforce_actor, users_actor};
|
||||||
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
|
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
|
||||||
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user, redirect_user_for_login};
|
use crate::controllers::base_controller::{
|
||||||
|
build_fatal_error_page, redirect_user, redirect_user_for_login,
|
||||||
|
};
|
||||||
use crate::data::login_redirect::LoginRedirect;
|
use crate::data::login_redirect::LoginRedirect;
|
||||||
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::{FactorID, TwoFactor, TwoFactorType, User};
|
use crate::data::user::User;
|
||||||
use crate::data::webauthn_manager::WebAuthManagerReq;
|
use crate::data::webauthn_manager::WebAuthManagerReq;
|
||||||
|
|
||||||
struct BaseLoginPage<'a> {
|
struct BaseLoginPage<'a> {
|
||||||
@ -40,26 +42,23 @@ struct PasswordResetTemplate<'a> {
|
|||||||
#[template(path = "login/choose_second_factor.html")]
|
#[template(path = "login/choose_second_factor.html")]
|
||||||
struct ChooseSecondFactorTemplate<'a> {
|
struct ChooseSecondFactorTemplate<'a> {
|
||||||
_p: BaseLoginPage<'a>,
|
_p: BaseLoginPage<'a>,
|
||||||
factors: &'a [TwoFactor],
|
user: &'a User,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login/opt_input.html")]
|
#[template(path = "login/otp_input.html")]
|
||||||
struct LoginWithOTPTemplate<'a> {
|
struct LoginWithOTPTemplate<'a> {
|
||||||
_p: BaseLoginPage<'a>,
|
_p: BaseLoginPage<'a>,
|
||||||
factor: &'a TwoFactor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login/webauthn_input.html")]
|
#[template(path = "login/webauthn_input.html")]
|
||||||
struct LoginWithWebauthnTemplate<'a> {
|
struct LoginWithWebauthnTemplate<'a> {
|
||||||
_p: BaseLoginPage<'a>,
|
_p: BaseLoginPage<'a>,
|
||||||
factor: &'a TwoFactor,
|
|
||||||
opaque_state: String,
|
opaque_state: String,
|
||||||
challenge_json: String,
|
challenge_json: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct LoginRequestBody {
|
pub struct LoginRequestBody {
|
||||||
login: String,
|
login: String,
|
||||||
@ -87,13 +86,17 @@ pub async fn login_route(
|
|||||||
let mut success = None;
|
let mut success = None;
|
||||||
let mut login = String::new();
|
let mut login = String::new();
|
||||||
|
|
||||||
let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() })
|
let failed_attempts = bruteforce
|
||||||
.await.unwrap();
|
.send(bruteforce_actor::CountFailedAttempt {
|
||||||
|
ip: remote_ip.into(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
|
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
|
||||||
return HttpResponse::TooManyRequests().body(
|
return HttpResponse::TooManyRequests().body(build_fatal_error_page(
|
||||||
build_fatal_error_page("Too many failed login attempts, please try again later!")
|
"Too many failed login attempts, please try again later!",
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user session must be closed
|
// Check if user session must be closed
|
||||||
@ -103,22 +106,24 @@ pub async fn login_route(
|
|||||||
}
|
}
|
||||||
success = Some("Goodbye!".to_string());
|
success = Some("Goodbye!".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is already authenticated
|
// Check if user is already authenticated
|
||||||
else if SessionIdentity(id.as_ref()).is_authenticated() {
|
else if SessionIdentity(id.as_ref()).is_authenticated() {
|
||||||
return redirect_user(query.redirect.get());
|
return redirect_user(query.redirect.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the password of the user has to be changed
|
// Check if the password of the user has to be changed
|
||||||
else if SessionIdentity(id.as_ref()).need_new_password() {
|
else if SessionIdentity(id.as_ref()).need_new_password() {
|
||||||
return redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded()));
|
return redirect_user(&format!(
|
||||||
|
"/reset_password?redirect={}",
|
||||||
|
query.redirect.get_encoded()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user has to valide a second factor
|
// Check if the user has to valide a second factor
|
||||||
else if SessionIdentity(id.as_ref()).need_2fa_auth() {
|
else if SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||||
return redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded()));
|
return redirect_user(&format!(
|
||||||
|
"/2fa_auth?redirect={}",
|
||||||
|
query.redirect.get_encoded()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to authenticate user
|
// Try to authenticate user
|
||||||
else if let Some(req) = &req {
|
else if let Some(req) = &req {
|
||||||
login = req.login.clone();
|
login = req.login.clone();
|
||||||
@ -150,10 +155,20 @@ pub async fn login_route(
|
|||||||
}
|
}
|
||||||
|
|
||||||
c => {
|
c => {
|
||||||
log::warn!("Failed login for ip {:?} / username {}: {:?}", remote_ip, login, c);
|
log::warn!(
|
||||||
|
"Failed login for ip {:?} / username {}: {:?}",
|
||||||
|
remote_ip,
|
||||||
|
login,
|
||||||
|
c
|
||||||
|
);
|
||||||
danger = Some("Login failed.".to_string());
|
danger = Some("Login failed.".to_string());
|
||||||
|
|
||||||
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap();
|
bruteforce
|
||||||
|
.send(bruteforce_actor::RecordFailedAttempt {
|
||||||
|
ip: remote_ip.into(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,10 +206,13 @@ pub struct PasswordResetQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reset user password route
|
/// Reset user password route
|
||||||
pub async fn reset_password_route(id: Option<Identity>, query: web::Query<PasswordResetQuery>,
|
pub async fn reset_password_route(
|
||||||
|
id: Option<Identity>,
|
||||||
|
query: web::Query<PasswordResetQuery>,
|
||||||
req: Option<web::Form<ChangePasswordRequestBody>>,
|
req: Option<web::Form<ChangePasswordRequestBody>>,
|
||||||
users: web::Data<Addr<UsersActor>>,
|
users: web::Data<Addr<UsersActor>>,
|
||||||
http_req: HttpRequest) -> impl Responder {
|
http_req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
let mut danger = None;
|
let mut danger = None;
|
||||||
|
|
||||||
if !SessionIdentity(id.as_ref()).need_new_password() {
|
if !SessionIdentity(id.as_ref()).need_new_password() {
|
||||||
@ -249,18 +267,27 @@ pub struct ChooseSecondFactorQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Let the user select the factor to use to authenticate
|
/// Let the user select the factor to use to authenticate
|
||||||
pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSecondFactorQuery>,
|
pub async fn choose_2fa_method(
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
id: Option<Identity>,
|
||||||
|
query: web::Query<ChooseSecondFactorQuery>,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||||
log::trace!("User does not require 2fa auth, redirecting");
|
log::trace!("User does not require 2fa auth, redirecting");
|
||||||
return redirect_user_for_login(query.redirect.get());
|
return redirect_user_for_login(query.redirect.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
|
let user: User = users
|
||||||
.await.unwrap().0.expect("Could not find user!");
|
.send(users_actor::GetUserRequest(
|
||||||
|
SessionIdentity(id.as_ref()).user_id(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.expect("Could not find user!");
|
||||||
|
|
||||||
// Automatically choose factor if there is only one factor
|
// Automatically choose factor if there is only one factor
|
||||||
if user.two_factor.len() == 1 && !query.force_display {
|
if user.get_distinct_factors_types().len() == 1 && !query.force_display {
|
||||||
log::trace!("User has only one factor, using it by default");
|
log::trace!("User has only one factor, using it by default");
|
||||||
return redirect_user(&user.two_factor[0].login_url(&query.redirect));
|
return redirect_user(&user.two_factor[0].login_url(&query.redirect));
|
||||||
}
|
}
|
||||||
@ -274,7 +301,7 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
|
|||||||
app_name: APP_NAME,
|
app_name: APP_NAME,
|
||||||
redirect_uri: &query.redirect,
|
redirect_uri: &query.redirect,
|
||||||
},
|
},
|
||||||
factors: &user.two_factor,
|
user: &user,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@ -285,7 +312,6 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
|
|||||||
pub struct LoginWithOTPQuery {
|
pub struct LoginWithOTPQuery {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
redirect: LoginRedirect,
|
redirect: LoginRedirect,
|
||||||
id: FactorID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@ -293,35 +319,39 @@ pub struct LoginWithOTPForm {
|
|||||||
code: String,
|
code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Login with OTP
|
/// Login with OTP
|
||||||
pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTPQuery>,
|
pub async fn login_with_otp(
|
||||||
|
id: Option<Identity>,
|
||||||
|
query: web::Query<LoginWithOTPQuery>,
|
||||||
form: Option<web::Form<LoginWithOTPForm>>,
|
form: Option<web::Form<LoginWithOTPForm>>,
|
||||||
users: web::Data<Addr<UsersActor>>,
|
users: web::Data<Addr<UsersActor>>,
|
||||||
http_req: HttpRequest) -> impl Responder {
|
http_req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
let mut danger = None;
|
let mut danger = None;
|
||||||
|
|
||||||
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||||
return redirect_user_for_login(query.redirect.get());
|
return redirect_user_for_login(query.redirect.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
|
let user: User = users
|
||||||
.await.unwrap().0.expect("Could not find user!");
|
.send(users_actor::GetUserRequest(
|
||||||
|
SessionIdentity(id.as_ref()).user_id(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.expect("Could not find user!");
|
||||||
|
|
||||||
let factor = match user.find_factor(&query.id) {
|
let keys = user.get_otp_factors();
|
||||||
Some(f) => f,
|
if keys.is_empty() {
|
||||||
None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"))
|
return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"));
|
||||||
};
|
|
||||||
|
|
||||||
let key = match &factor.kind {
|
|
||||||
TwoFactorType::TOTP(key) => key,
|
|
||||||
_ => {
|
|
||||||
return HttpResponse::Ok().body(build_fatal_error_page("Factor is not a TOTP key!"));
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(form) = form {
|
if let Some(form) = form {
|
||||||
if !key.check_code(&form.code).unwrap_or(false) {
|
if !keys
|
||||||
|
.iter()
|
||||||
|
.any(|k| k.check_code(&form.code).unwrap_or(false))
|
||||||
|
{
|
||||||
danger = Some("Specified code is invalid!".to_string());
|
danger = Some("Specified code is invalid!".to_string());
|
||||||
} else {
|
} else {
|
||||||
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
|
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
|
||||||
@ -329,7 +359,8 @@ pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse::Ok().body(LoginWithOTPTemplate {
|
HttpResponse::Ok().body(
|
||||||
|
LoginWithOTPTemplate {
|
||||||
_p: BaseLoginPage {
|
_p: BaseLoginPage {
|
||||||
danger,
|
danger,
|
||||||
success: None,
|
success: None,
|
||||||
@ -337,48 +368,51 @@ pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTP
|
|||||||
app_name: APP_NAME,
|
app_name: APP_NAME,
|
||||||
redirect_uri: &query.redirect,
|
redirect_uri: &query.redirect,
|
||||||
},
|
},
|
||||||
factor,
|
}
|
||||||
}.render().unwrap())
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct LoginWithWebauthnQuery {
|
pub struct LoginWithWebauthnQuery {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
redirect: LoginRedirect,
|
redirect: LoginRedirect,
|
||||||
id: FactorID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Login with Webauthn
|
/// Login with Webauthn
|
||||||
pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWithWebauthnQuery>,
|
pub async fn login_with_webauthn(
|
||||||
|
id: Option<Identity>,
|
||||||
|
query: web::Query<LoginWithWebauthnQuery>,
|
||||||
manager: WebAuthManagerReq,
|
manager: WebAuthManagerReq,
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||||
return redirect_user_for_login(query.redirect.get());
|
return redirect_user_for_login(query.redirect.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
|
let user: User = users
|
||||||
.await.unwrap().0.expect("Could not find user!");
|
.send(users_actor::GetUserRequest(
|
||||||
|
SessionIdentity(id.as_ref()).user_id(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.expect("Could not find user!");
|
||||||
|
|
||||||
let factor = match user.find_factor(&query.id) {
|
let pub_keys = user.get_webauthn_pub_keys();
|
||||||
Some(f) => f,
|
if pub_keys.is_empty() {
|
||||||
None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"))
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = match &factor.kind {
|
|
||||||
TwoFactorType::WEBAUTHN(key) => key,
|
|
||||||
_ => {
|
|
||||||
return HttpResponse::Ok()
|
return HttpResponse::Ok()
|
||||||
.body(build_fatal_error_page("Factor is not a Webauthn key!"));
|
.body(build_fatal_error_page("No Webauthn public key registered!"));
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let challenge = match manager.start_authentication(&user.uid, key) {
|
let challenge = match manager.start_authentication(&user.uid, &pub_keys) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to generate webauthn challenge! {:?}", e);
|
log::error!("Failed to generate webauthn challenge! {:?}", e);
|
||||||
return HttpResponse::InternalServerError()
|
return HttpResponse::InternalServerError().body(build_fatal_error_page(
|
||||||
.body(build_fatal_error_page("Failed to generate webauthn challenge"));
|
"Failed to generate webauthn challenge",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -390,7 +424,8 @@ pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpResponse::Ok().body(LoginWithWebauthnTemplate {
|
HttpResponse::Ok().body(
|
||||||
|
LoginWithWebauthnTemplate {
|
||||||
_p: BaseLoginPage {
|
_p: BaseLoginPage {
|
||||||
danger: None,
|
danger: None,
|
||||||
success: None,
|
success: None,
|
||||||
@ -398,8 +433,10 @@ pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWi
|
|||||||
app_name: APP_NAME,
|
app_name: APP_NAME,
|
||||||
redirect_uri: &query.redirect,
|
redirect_uri: &query.redirect,
|
||||||
},
|
},
|
||||||
factor,
|
|
||||||
opaque_state: challenge.opaque_state,
|
opaque_state: challenge.opaque_state,
|
||||||
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
|
pub mod admin_api;
|
||||||
|
pub mod admin_controller;
|
||||||
pub mod assets_controller;
|
pub mod assets_controller;
|
||||||
pub mod base_controller;
|
pub mod base_controller;
|
||||||
pub mod login_controller;
|
|
||||||
pub mod login_api;
|
pub mod login_api;
|
||||||
pub mod settings_controller;
|
pub mod login_controller;
|
||||||
pub mod admin_controller;
|
|
||||||
pub mod admin_api;
|
|
||||||
pub mod openid_controller;
|
pub mod openid_controller;
|
||||||
pub mod two_factors_controller;
|
pub mod settings_controller;
|
||||||
pub mod two_factor_api;
|
pub mod two_factor_api;
|
||||||
|
pub mod two_factors_controller;
|
||||||
|
@ -2,12 +2,12 @@ use std::fmt::Debug;
|
|||||||
|
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
|
||||||
use actix_web::error::ErrorUnauthorized;
|
use actix_web::error::ErrorUnauthorized;
|
||||||
|
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||||
|
|
||||||
use crate::actors::{openid_sessions_actor, users_actor};
|
|
||||||
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
|
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
|
||||||
use crate::actors::users_actor::UsersActor;
|
use crate::actors::users_actor::UsersActor;
|
||||||
|
use crate::actors::{openid_sessions_actor, users_actor};
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::controllers::base_controller::build_fatal_error_page;
|
use crate::controllers::base_controller::build_fatal_error_page;
|
||||||
use crate::data::app_config::AppConfig;
|
use crate::data::app_config::AppConfig;
|
||||||
@ -15,7 +15,7 @@ use crate::data::client::{ClientID, ClientManager};
|
|||||||
use crate::data::code_challenge::CodeChallenge;
|
use crate::data::code_challenge::CodeChallenge;
|
||||||
use crate::data::current_user::CurrentUser;
|
use crate::data::current_user::CurrentUser;
|
||||||
use crate::data::id_token::IdToken;
|
use crate::data::id_token::IdToken;
|
||||||
use crate::data::jwt_signer::{JsonWebKey, JWTSigner};
|
use crate::data::jwt_signer::{JWTSigner, JsonWebKey};
|
||||||
use crate::data::open_id_user_info::OpenIDUserInfo;
|
use crate::data::open_id_user_info::OpenIDUserInfo;
|
||||||
use crate::data::openid_config::OpenIDConfig;
|
use crate::data::openid_config::OpenIDConfig;
|
||||||
use crate::data::session_identity::SessionIdentity;
|
use crate::data::session_identity::SessionIdentity;
|
||||||
@ -24,7 +24,9 @@ use crate::utils::string_utils::rand_str;
|
|||||||
use crate::utils::time::time;
|
use crate::utils::time::time;
|
||||||
|
|
||||||
pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>) -> impl Responder {
|
pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>) -> impl Responder {
|
||||||
let is_secure_request = req.headers().get("HTTP_X_FORWARDED_PROTO")
|
let is_secure_request = req
|
||||||
|
.headers()
|
||||||
|
.get("HTTP_X_FORWARDED_PROTO")
|
||||||
.map(|v| v.to_str().unwrap_or_default().to_lowercase().eq("https"))
|
.map(|v| v.to_str().unwrap_or_default().to_lowercase().eq("https"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
@ -33,10 +35,14 @@ pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>)
|
|||||||
Some(s) => s.to_str().unwrap_or_default(),
|
Some(s) => s.to_str().unwrap_or_default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let curr_origin = format!("{}://{}", match is_secure_request {
|
let curr_origin = format!(
|
||||||
|
"{}://{}",
|
||||||
|
match is_secure_request {
|
||||||
true => "https",
|
true => "https",
|
||||||
false => "http"
|
false => "http",
|
||||||
}, host);
|
},
|
||||||
|
host
|
||||||
|
);
|
||||||
|
|
||||||
HttpResponse::Ok().json(OpenIDConfig {
|
HttpResponse::Ok().json(OpenIDConfig {
|
||||||
issuer: app_conf.website_origin.clone(),
|
issuer: app_conf.website_origin.clone(),
|
||||||
@ -80,35 +86,43 @@ pub struct AuthorizeQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse {
|
fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse {
|
||||||
log::warn!("Failed to process sign in request ({} => {}): {:?}", error, description, query);
|
log::warn!(
|
||||||
|
"Failed to process sign in request ({} => {}): {:?}",
|
||||||
|
error,
|
||||||
|
description,
|
||||||
|
query
|
||||||
|
);
|
||||||
HttpResponse::Found()
|
HttpResponse::Found()
|
||||||
.append_header(
|
.append_header((
|
||||||
("Location", format!(
|
"Location",
|
||||||
|
format!(
|
||||||
"{}?error={}?error_description={}&state={}",
|
"{}?error={}?error_description={}&state={}",
|
||||||
query.redirect_uri,
|
query.redirect_uri,
|
||||||
urlencoding::encode(error),
|
urlencoding::encode(error),
|
||||||
urlencoding::encode(description),
|
urlencoding::encode(description),
|
||||||
urlencoding::encode(&query.state)
|
urlencoding::encode(&query.state)
|
||||||
|
),
|
||||||
))
|
))
|
||||||
)
|
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<AuthorizeQuery>,
|
pub async fn authorize(
|
||||||
|
user: CurrentUser,
|
||||||
|
id: Identity,
|
||||||
|
query: web::Query<AuthorizeQuery>,
|
||||||
clients: web::Data<ClientManager>,
|
clients: web::Data<ClientManager>,
|
||||||
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> impl Responder {
|
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
let client = match clients.find_by_id(&query.client_id) {
|
let client = match clients.find_by_id(&query.client_id) {
|
||||||
None => {
|
None => {
|
||||||
return HttpResponse::BadRequest()
|
return HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!"));
|
||||||
.body(build_fatal_error_page("Client is invalid!"));
|
|
||||||
}
|
}
|
||||||
Some(c) => c
|
Some(c) => c,
|
||||||
};
|
};
|
||||||
|
|
||||||
let redirect_uri = query.redirect_uri.trim().to_string();
|
let redirect_uri = query.redirect_uri.trim().to_string();
|
||||||
if !redirect_uri.starts_with(&client.redirect_uri) {
|
if !redirect_uri.starts_with(&client.redirect_uri) {
|
||||||
return HttpResponse::BadRequest()
|
return HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!"));
|
||||||
.body(build_fatal_error_page("Redirect URI is invalid!"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !query.scope.split(' ').any(|x| x == "openid") {
|
if !query.scope.split(' ').any(|x| x == "openid") {
|
||||||
@ -116,7 +130,11 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !query.response_type.eq("code") {
|
if !query.response_type.eq("code") {
|
||||||
return error_redirect(&query, "invalid_request", "Only code response type is supported!");
|
return error_redirect(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Only code response type is supported!",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.state.is_empty() {
|
if query.state.is_empty() {
|
||||||
@ -127,18 +145,27 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
|
|||||||
Some(chal) => {
|
Some(chal) => {
|
||||||
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
|
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
|
||||||
if !meth.eq("S256") && !meth.eq("plain") {
|
if !meth.eq("S256") && !meth.eq("plain") {
|
||||||
return error_redirect(&query, "invalid_request",
|
return error_redirect(
|
||||||
"Only S256 and plain code challenge methods are supported!");
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Only S256 and plain code challenge methods are supported!",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(CodeChallenge { code_challenge: chal, code_challenge_method: meth.to_string() })
|
Some(CodeChallenge {
|
||||||
|
code_challenge: chal,
|
||||||
|
code_challenge_method: meth.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
_ => None
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if user is authorized to access the application
|
// Check if user is authorized to access the application
|
||||||
if !user.can_access_app(&client.id) {
|
if !user.can_access_app(&client.id) {
|
||||||
return error_redirect(&query, "invalid_request",
|
return error_redirect(
|
||||||
"User is not authorized to access this application!");
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"User is not authorized to access this application!",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save all authentication information in memory
|
// Save all authentication information in memory
|
||||||
@ -157,18 +184,25 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
|
|||||||
nonce: query.0.nonce,
|
nonce: query.0.nonce,
|
||||||
code_challenge,
|
code_challenge,
|
||||||
};
|
};
|
||||||
sessions.send(openid_sessions_actor::PushNewSession(session.clone())).await.unwrap();
|
sessions
|
||||||
|
.send(openid_sessions_actor::PushNewSession(session.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
log::trace!("New OpenID session: {:#?}", session);
|
log::trace!("New OpenID session: {:#?}", session);
|
||||||
|
|
||||||
HttpResponse::Found()
|
HttpResponse::Found()
|
||||||
.append_header(("Location", format!(
|
.append_header((
|
||||||
|
"Location",
|
||||||
|
format!(
|
||||||
"{}?state={}&session_state={}&code={}",
|
"{}?state={}&session_state={}&code={}",
|
||||||
session.redirect_uri,
|
session.redirect_uri,
|
||||||
urlencoding::encode(&query.0.state),
|
urlencoding::encode(&query.0.state),
|
||||||
urlencoding::encode(&session.session_id.0),
|
urlencoding::encode(&session.session_id.0),
|
||||||
urlencoding::encode(&session.authorization_code)
|
urlencoding::encode(&session.authorization_code)
|
||||||
))).finish()
|
),
|
||||||
|
))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@ -178,9 +212,13 @@ struct ErrorResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
|
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
|
||||||
log::warn!("request failed: {} - {} => '{:#?}'", error, description, query);
|
log::warn!(
|
||||||
HttpResponse::BadRequest()
|
"request failed: {} - {} => '{:#?}'",
|
||||||
.json(ErrorResponse {
|
error,
|
||||||
|
description,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
HttpResponse::BadRequest().json(ErrorResponse {
|
||||||
error: error.to_string(),
|
error: error.to_string(),
|
||||||
error_description: description.to_string(),
|
error_description: description.to_string(),
|
||||||
})
|
})
|
||||||
@ -198,7 +236,6 @@ pub struct TokenRefreshTokenQuery {
|
|||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct TokenQuery {
|
pub struct TokenQuery {
|
||||||
grant_type: String,
|
grant_type: String,
|
||||||
@ -222,17 +259,19 @@ pub struct TokenResponse {
|
|||||||
id_token: Option<String>,
|
id_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn token(req: HttpRequest,
|
pub async fn token(
|
||||||
|
req: HttpRequest,
|
||||||
query: web::Form<TokenQuery>,
|
query: web::Form<TokenQuery>,
|
||||||
clients: web::Data<ClientManager>,
|
clients: web::Data<ClientManager>,
|
||||||
app_config: web::Data<AppConfig>,
|
app_config: web::Data<AppConfig>,
|
||||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||||
users: web::Data<Addr<UsersActor>>,
|
users: web::Data<Addr<UsersActor>>,
|
||||||
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> {
|
jwt_signer: web::Data<JWTSigner>,
|
||||||
|
) -> actix_web::Result<HttpResponse> {
|
||||||
// Extraction authentication information
|
// Extraction authentication information
|
||||||
let authorization_header = req.headers().get("authorization");
|
let authorization_header = req.headers().get("authorization");
|
||||||
let (client_id, client_secret) = match (&query.client_id, &query.client_secret, authorization_header) {
|
let (client_id, client_secret) =
|
||||||
|
match (&query.client_id, &query.client_secret, authorization_header) {
|
||||||
// post authentication
|
// post authentication
|
||||||
(Some(client_id), Some(client_secret), None) => {
|
(Some(client_id), Some(client_secret), None) => {
|
||||||
(client_id.clone(), client_secret.to_string())
|
(client_id.clone(), client_secret.to_string())
|
||||||
@ -245,28 +284,40 @@ pub async fn token(req: HttpRequest,
|
|||||||
return Ok(error_response(
|
return Ok(error_response(
|
||||||
&query,
|
&query,
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
|
&format!(
|
||||||
|
"Authorization header does not start with 'Basic ', got '{:#?}'",
|
||||||
|
v
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Some(v) => v
|
Some(v) => v,
|
||||||
};
|
};
|
||||||
|
|
||||||
let decode = String::from_utf8_lossy(&match base64::decode(token) {
|
let decode = String::from_utf8_lossy(&match base64::decode(token) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decode authorization header: {:?}", e);
|
log::error!("Failed to decode authorization header: {:?}", e);
|
||||||
return Ok(error_response(&query, "invalid_request", "Failed to decode authorization header!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Failed to decode authorization header!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}).to_string();
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
match decode.split_once(':') {
|
match decode.split_once(':') {
|
||||||
None => (ClientID(decode), "".to_string()),
|
None => (ClientID(decode), "".to_string()),
|
||||||
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string())
|
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return Ok(error_response(&query, "invalid_request", "Authentication method unknown!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Authentication method unknown!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -275,62 +326,108 @@ pub async fn token(req: HttpRequest,
|
|||||||
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
|
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
|
||||||
|
|
||||||
if !client.secret.eq(&client_secret) {
|
if !client.secret.eq(&client_secret) {
|
||||||
return Ok(error_response(&query, "invalid_request", "Client secret is invalid!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Client secret is invalid!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let token_response = match (query.grant_type.as_str(),
|
let token_response = match (
|
||||||
|
query.grant_type.as_str(),
|
||||||
&query.authorization_code_query,
|
&query.authorization_code_query,
|
||||||
&query.refresh_token_query) {
|
&query.refresh_token_query,
|
||||||
|
) {
|
||||||
("authorization_code", Some(q), _) => {
|
("authorization_code", Some(q), _) => {
|
||||||
let mut session: Session = match sessions
|
let mut session: Session = match sessions
|
||||||
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
|
.send(openid_sessions_actor::FindSessionByAuthorizationCode(
|
||||||
.await.unwrap()
|
q.code.clone(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
{
|
{
|
||||||
None => {
|
None => {
|
||||||
return Ok(error_response(&query, "invalid_request", "Session not found!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Session not found!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
if session.client != client.id {
|
if session.client != client.id {
|
||||||
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Client mismatch!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.redirect_uri != q.redirect_uri {
|
if session.redirect_uri != q.redirect_uri {
|
||||||
return Ok(error_response(&query, "invalid_request", "Invalid redirect URI!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Invalid redirect URI!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.authorization_code_expire_at < time() {
|
if session.authorization_code_expire_at < time() {
|
||||||
return Ok(error_response(&query, "invalid_request", "Authorization code expired!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Authorization code expired!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check code challenge, if needed
|
// Check code challenge, if needed
|
||||||
if let Some(chall) = &session.code_challenge {
|
if let Some(chall) = &session.code_challenge {
|
||||||
let code_verifier = match &q.code_verifier {
|
let code_verifier = match &q.code_verifier {
|
||||||
None => {
|
None => {
|
||||||
return Ok(error_response(&query, "access_denied", "Code verifier missing"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"access_denied",
|
||||||
|
"Code verifier missing",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(s) => s
|
Some(s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !chall.verify_code(code_verifier) {
|
if !chall.verify_code(code_verifier) {
|
||||||
return Ok(error_response(&query, "invalid_grant", "Invalid code verifier"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_grant",
|
||||||
|
"Invalid code verifier",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else if q.code_verifier.is_some() {
|
} else if q.code_verifier.is_some() {
|
||||||
return Ok(error_response(&query, "invalid_grant", "Unexpected `code_verifier` parameter!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_grant",
|
||||||
|
"Unexpected `code_verifier` parameter!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.access_token.is_some() {
|
if session.access_token.is_some() {
|
||||||
return Ok(error_response(&query, "invalid_request", "Authorization code already used!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Authorization code already used!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
|
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
|
||||||
|
|
||||||
sessions.send(openid_sessions_actor::UpdateSession(session.clone()))
|
sessions
|
||||||
.await.unwrap();
|
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user.clone()))
|
let user: Option<User> = users
|
||||||
.await.unwrap().0;
|
.send(users_actor::GetUserRequest(session.user.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
let user = match user {
|
let user = match user {
|
||||||
None => return Ok(error_response(&query, "invalid_request", "User not found!")),
|
None => return Ok(error_response(&query, "invalid_request", "User not found!")),
|
||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
@ -359,28 +456,44 @@ pub async fn token(req: HttpRequest,
|
|||||||
|
|
||||||
("refresh_token", _, Some(q)) => {
|
("refresh_token", _, Some(q)) => {
|
||||||
let mut session: Session = match sessions
|
let mut session: Session = match sessions
|
||||||
.send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone()))
|
.send(openid_sessions_actor::FindSessionByRefreshToken(
|
||||||
.await.unwrap()
|
q.refresh_token.clone(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
{
|
{
|
||||||
None => {
|
None => {
|
||||||
return Ok(error_response(&query, "invalid_request", "Session not found!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Session not found!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
if session.client != client.id {
|
if session.client != client.id {
|
||||||
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Client mismatch!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.refresh_token_expire_at < time() {
|
if session.refresh_token_expire_at < time() {
|
||||||
return Ok(error_response(&query, "access_denied", "Refresh token has expired!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"access_denied",
|
||||||
|
"Refresh token has expired!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
|
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
|
||||||
|
|
||||||
sessions
|
sessions
|
||||||
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||||
.await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
TokenResponse {
|
TokenResponse {
|
||||||
access_token: session.access_token.expect("Missing access token!"),
|
access_token: session.access_token.expect("Missing access token!"),
|
||||||
@ -392,7 +505,11 @@ pub async fn token(req: HttpRequest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return Ok(error_response(&query, "invalid_request", "Grant type unsupported!"));
|
return Ok(error_response(
|
||||||
|
&query,
|
||||||
|
"invalid_request",
|
||||||
|
"Grant type unsupported!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -408,16 +525,20 @@ struct CertsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cert_uri(jwt_signer: web::Data<JWTSigner>) -> impl Responder {
|
pub async fn cert_uri(jwt_signer: web::Data<JWTSigner>) -> impl Responder {
|
||||||
HttpResponse::Ok().json(CertsResponse { keys: vec![jwt_signer.get_json_web_key()] })
|
HttpResponse::Ok().json(CertsResponse {
|
||||||
|
keys: vec![jwt_signer.get_json_web_key()],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_info_error(err: &str, description: &str) -> HttpResponse {
|
fn user_info_error(err: &str, description: &str) -> HttpResponse {
|
||||||
HttpResponse::Unauthorized()
|
HttpResponse::Unauthorized()
|
||||||
.insert_header(("WWW-Authenticate", format!(
|
.insert_header((
|
||||||
|
"WWW-Authenticate",
|
||||||
|
format!(
|
||||||
"Bearer error=\"{}\", error_description=\"{}\"",
|
"Bearer error=\"{}\", error_description=\"{}\"",
|
||||||
err,
|
err, description
|
||||||
description
|
),
|
||||||
)))
|
))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,37 +547,46 @@ pub struct UserInfoQuery {
|
|||||||
access_token: Option<String>,
|
access_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_info_post(req: HttpRequest,
|
pub async fn user_info_post(
|
||||||
|
req: HttpRequest,
|
||||||
form: Option<web::Form<UserInfoQuery>>,
|
form: Option<web::Form<UserInfoQuery>>,
|
||||||
query: web::Query<UserInfoQuery>,
|
query: web::Query<UserInfoQuery>,
|
||||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
users: web::Data<Addr<UsersActor>>,
|
||||||
user_info(req,
|
) -> impl Responder {
|
||||||
form
|
user_info(
|
||||||
.map(|f| f.0.access_token)
|
req,
|
||||||
|
form.map(|f| f.0.access_token)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.or(query.0.access_token),
|
.or(query.0.access_token),
|
||||||
sessions,
|
sessions,
|
||||||
users,
|
users,
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_info_get(req: HttpRequest, query: web::Query<UserInfoQuery>,
|
pub async fn user_info_get(
|
||||||
|
req: HttpRequest,
|
||||||
|
query: web::Query<UserInfoQuery>,
|
||||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
user_info(req, query.0.access_token, sessions, users).await
|
user_info(req, query.0.access_token, sessions, users).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
|
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
|
||||||
async fn user_info(req: HttpRequest, token: Option<String>,
|
async fn user_info(
|
||||||
|
req: HttpRequest,
|
||||||
|
token: Option<String>,
|
||||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
let token = match token {
|
let token = match token {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
let token = match req.headers().get("Authorization") {
|
let token = match req.headers().get("Authorization") {
|
||||||
None => return user_info_error("invalid_request", "Missing access token!"),
|
None => return user_info_error("invalid_request", "Missing access token!"),
|
||||||
Some(t) => t
|
Some(t) => t,
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = match token.to_str() {
|
let token = match token.to_str() {
|
||||||
@ -465,7 +595,12 @@ async fn user_info(req: HttpRequest, token: Option<String>,
|
|||||||
};
|
};
|
||||||
|
|
||||||
let token = match token.strip_prefix("Bearer ") {
|
let token = match token.strip_prefix("Bearer ") {
|
||||||
None => return user_info_error("invalid_request", "Header token does not start with 'Bearer '!"),
|
None => {
|
||||||
|
return user_info_error(
|
||||||
|
"invalid_request",
|
||||||
|
"Header token does not start with 'Bearer '!",
|
||||||
|
)
|
||||||
|
}
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -474,7 +609,9 @@ async fn user_info(req: HttpRequest, token: Option<String>,
|
|||||||
};
|
};
|
||||||
|
|
||||||
let session: Option<Session> = sessions
|
let session: Option<Session> = sessions
|
||||||
.send(openid_sessions_actor::FindSessionByAccessToken(token)).await.unwrap();
|
.send(openid_sessions_actor::FindSessionByAccessToken(token))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let session = match session {
|
let session = match session {
|
||||||
None => {
|
None => {
|
||||||
return user_info_error("invalid_request", "Session not found!");
|
return user_info_error("invalid_request", "Session not found!");
|
||||||
@ -486,7 +623,11 @@ async fn user_info(req: HttpRequest, token: Option<String>,
|
|||||||
return user_info_error("invalid_request", "Access token has expired!");
|
return user_info_error("invalid_request", "Access token has expired!");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user)).await.unwrap().0;
|
let user: Option<User> = users
|
||||||
|
.send(users_actor::GetUserRequest(session.user))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
let user = match user {
|
let user = match user {
|
||||||
None => {
|
None => {
|
||||||
return user_info_error("invalid_request", "Failed to extract user information!");
|
return user_info_error("invalid_request", "Failed to extract user information!");
|
||||||
@ -494,8 +635,7 @@ async fn user_info(req: HttpRequest, token: Option<String>,
|
|||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().json(OpenIDUserInfo {
|
||||||
.json(OpenIDUserInfo {
|
|
||||||
name: user.full_name(),
|
name: user.full_name(),
|
||||||
sub: user.uid.0,
|
sub: user.uid.0,
|
||||||
given_name: user.first_name,
|
given_name: user.first_name,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::actors::{bruteforce_actor, users_actor};
|
|
||||||
use crate::actors::bruteforce_actor::BruteForceActor;
|
use crate::actors::bruteforce_actor::BruteForceActor;
|
||||||
use crate::actors::users_actor::UsersActor;
|
use crate::actors::users_actor::UsersActor;
|
||||||
|
use crate::actors::{bruteforce_actor, users_actor};
|
||||||
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
|
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
|
||||||
use crate::data::current_user::CurrentUser;
|
use crate::data::current_user::CurrentUser;
|
||||||
use crate::data::remote_ip::RemoteIP;
|
use crate::data::remote_ip::RemoteIP;
|
||||||
@ -21,8 +21,12 @@ pub(crate) struct BaseSettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BaseSettingsPage {
|
impl BaseSettingsPage {
|
||||||
pub fn get(page_title: &'static str, user: &User,
|
pub fn get(
|
||||||
danger_message: Option<String>, success_message: Option<String>) -> BaseSettingsPage {
|
page_title: &'static str,
|
||||||
|
user: &User,
|
||||||
|
danger_message: Option<String>,
|
||||||
|
success_message: Option<String>,
|
||||||
|
) -> BaseSettingsPage {
|
||||||
Self {
|
Self {
|
||||||
danger_message,
|
danger_message,
|
||||||
success_message,
|
success_message,
|
||||||
@ -52,11 +56,14 @@ struct ChangePasswordPage {
|
|||||||
/// Account details page
|
/// Account details page
|
||||||
pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder {
|
pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder {
|
||||||
let user = user.into();
|
let user = user.into();
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().body(
|
||||||
.body(AccountDetailsPage {
|
AccountDetailsPage {
|
||||||
_p: BaseSettingsPage::get("Account details", &user, None, None),
|
_p: BaseSettingsPage::get("Account details", &user, None, None),
|
||||||
u: user,
|
u: user,
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@ -66,53 +73,71 @@ pub struct PassChangeRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change password route
|
/// Change password route
|
||||||
pub async fn change_password_route(user: CurrentUser,
|
pub async fn change_password_route(
|
||||||
|
user: CurrentUser,
|
||||||
users: web::Data<Addr<UsersActor>>,
|
users: web::Data<Addr<UsersActor>>,
|
||||||
req: Option<web::Form<PassChangeRequest>>,
|
req: Option<web::Form<PassChangeRequest>>,
|
||||||
bruteforce: web::Data<Addr<BruteForceActor>>,
|
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||||
remote_ip: RemoteIP) -> impl Responder {
|
remote_ip: RemoteIP,
|
||||||
|
) -> impl Responder {
|
||||||
let mut danger = None;
|
let mut danger = None;
|
||||||
let mut success = None;
|
let mut success = None;
|
||||||
|
|
||||||
let user: User = user.into();
|
let user: User = user.into();
|
||||||
|
|
||||||
let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() }).await.unwrap();
|
let failed_attempts = bruteforce
|
||||||
|
.send(bruteforce_actor::CountFailedAttempt {
|
||||||
|
ip: remote_ip.into(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
|
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
|
||||||
danger = Some("Too many invalid password attempts. Please try to change your password later.".to_string());
|
danger = Some(
|
||||||
|
"Too many invalid password attempts. Please try to change your password later."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
} else if let Some(req) = req {
|
} else if let Some(req) = req {
|
||||||
|
|
||||||
// Invalid password
|
// Invalid password
|
||||||
if !user.verify_password(&req.old_pass) {
|
if !user.verify_password(&req.old_pass) {
|
||||||
danger = Some("Old password is invalid!".to_string());
|
danger = Some("Old password is invalid!".to_string());
|
||||||
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap();
|
bruteforce
|
||||||
|
.send(bruteforce_actor::RecordFailedAttempt {
|
||||||
|
ip: remote_ip.into(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password too short
|
// Password too short
|
||||||
else if req.new_pass.len() < MIN_PASS_LEN {
|
else if req.new_pass.len() < MIN_PASS_LEN {
|
||||||
danger = Some("New password is too short!".to_string());
|
danger = Some("New password is too short!".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change password
|
// Change password
|
||||||
else {
|
else {
|
||||||
let res = users.send(
|
let res = users
|
||||||
users_actor::ChangePasswordRequest {
|
.send(users_actor::ChangePasswordRequest {
|
||||||
user_id: user.uid.clone(),
|
user_id: user.uid.clone(),
|
||||||
new_password: req.new_pass.to_string(),
|
new_password: req.new_pass.to_string(),
|
||||||
temporary: false,
|
temporary: false,
|
||||||
}
|
})
|
||||||
).await.unwrap().0;
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
if !res {
|
if !res {
|
||||||
danger = Some("An error occurred while trying to change your password!".to_string());
|
danger =
|
||||||
|
Some("An error occurred while trying to change your password!".to_string());
|
||||||
} else {
|
} else {
|
||||||
success = Some("Your password was successfully changed!".to_string());
|
success = Some("Your password was successfully changed!".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().body(
|
||||||
.body(ChangePasswordPage {
|
ChangePasswordPage {
|
||||||
_p: BaseSettingsPage::get("Change password", &user, danger, success),
|
_p: BaseSettingsPage::get("Change password", &user, danger, success),
|
||||||
min_pwd_len: MIN_PASS_LEN,
|
min_pwd_len: MIN_PASS_LEN,
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use webauthn_rs::prelude::RegisterPublicKeyCredential;
|
use webauthn_rs::prelude::RegisterPublicKeyCredential;
|
||||||
|
|
||||||
@ -17,15 +17,19 @@ pub struct AddTOTPRequest {
|
|||||||
first_code: String,
|
first_code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>,
|
pub async fn save_totp_factor(
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
user: CurrentUser,
|
||||||
|
form: web::Json<AddTOTPRequest>,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
let key = TotpKey::from_encoded_secret(&form.secret);
|
let key = TotpKey::from_encoded_secret(&form.secret);
|
||||||
|
|
||||||
if !key.check_code(&form.first_code).unwrap_or(false) {
|
if !key.check_code(&form.first_code).unwrap_or(false) {
|
||||||
return HttpResponse::BadRequest()
|
return HttpResponse::BadRequest().body(format!(
|
||||||
.body(format!("Given code is invalid (expected {} or {})!",
|
"Given code is invalid (expected {} or {})!",
|
||||||
key.current_code().unwrap_or_default(),
|
key.current_code().unwrap_or_default(),
|
||||||
key.previous_code().unwrap_or_default()));
|
key.previous_code().unwrap_or_default()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.factor_name.is_empty() {
|
if form.factor_name.is_empty() {
|
||||||
@ -38,7 +42,11 @@ pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>
|
|||||||
name: form.0.factor_name,
|
name: form.0.factor_name,
|
||||||
kind: TwoFactorType::TOTP(key),
|
kind: TwoFactorType::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 {
|
||||||
HttpResponse::InternalServerError().body("Failed to update user information!")
|
HttpResponse::InternalServerError().body("Failed to update user information!")
|
||||||
@ -54,14 +62,13 @@ pub struct AddWebauthnRequest {
|
|||||||
credential: RegisterPublicKeyCredential,
|
credential: RegisterPublicKeyCredential,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json<AddWebauthnRequest>,
|
pub async fn save_webauthn_factor(
|
||||||
|
user: CurrentUser,
|
||||||
|
form: web::Json<AddWebauthnRequest>,
|
||||||
users: web::Data<Addr<UsersActor>>,
|
users: web::Data<Addr<UsersActor>>,
|
||||||
manager: WebAuthManagerReq) -> impl Responder {
|
manager: WebAuthManagerReq,
|
||||||
let key = match manager.finish_registration(
|
) -> impl Responder {
|
||||||
&user,
|
let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) {
|
||||||
&form.0.opaque_state,
|
|
||||||
form.0.credential,
|
|
||||||
) {
|
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to register security key! {:?}", e);
|
log::error!("Failed to register security key! {:?}", e);
|
||||||
@ -75,7 +82,11 @@ pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json<AddWebauthn
|
|||||||
name: form.0.factor_name,
|
name: form.0.factor_name,
|
||||||
kind: TwoFactorType::WEBAUTHN(Box::new(key)),
|
kind: TwoFactorType::WEBAUTHN(Box::new(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 {
|
||||||
HttpResponse::InternalServerError().body("Failed to update user information!")
|
HttpResponse::InternalServerError().body("Failed to update user information!")
|
||||||
@ -89,12 +100,19 @@ pub struct DeleteFactorRequest {
|
|||||||
id: FactorID,
|
id: FactorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_factor(user: CurrentUser, form: web::Json<DeleteFactorRequest>,
|
pub async fn delete_factor(
|
||||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
user: CurrentUser,
|
||||||
|
form: web::Json<DeleteFactorRequest>,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
let mut user = User::from(user);
|
let mut user = User::from(user);
|
||||||
user.remove_factor(form.0.id);
|
user.remove_factor(form.0.id);
|
||||||
|
|
||||||
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 {
|
||||||
HttpResponse::InternalServerError().body("Failed to update user information!")
|
HttpResponse::InternalServerError().body("Failed to update user information!")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use qrcode_generator::QrCodeEcc;
|
use qrcode_generator::QrCodeEcc;
|
||||||
|
|
||||||
@ -37,27 +37,25 @@ struct AddWebauhtnPage {
|
|||||||
|
|
||||||
/// Manage two factors authentication methods route
|
/// Manage two factors authentication methods route
|
||||||
pub async fn two_factors_route(user: CurrentUser) -> impl Responder {
|
pub async fn two_factors_route(user: CurrentUser) -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().body(
|
||||||
.body(TwoFactorsPage {
|
TwoFactorsPage {
|
||||||
_p: BaseSettingsPage::get(
|
_p: BaseSettingsPage::get("Two factor auth", &user, None, None),
|
||||||
"Two factor auth",
|
|
||||||
&user,
|
|
||||||
None,
|
|
||||||
None),
|
|
||||||
user: user.deref(),
|
user: user.deref(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Configure a new TOTP authentication factor
|
/// Configure a new TOTP authentication factor
|
||||||
pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppConfig>) -> impl Responder {
|
pub async fn add_totp_factor_route(
|
||||||
|
user: CurrentUser,
|
||||||
|
app_conf: web::Data<AppConfig>,
|
||||||
|
) -> impl Responder {
|
||||||
let key = TotpKey::new_random();
|
let key = TotpKey::new_random();
|
||||||
|
|
||||||
let qr_code = qrcode_generator::to_png_to_vec(
|
let qr_code =
|
||||||
key.url_for_user(&user, &app_conf),
|
qrcode_generator::to_png_to_vec(key.url_for_user(&user, &app_conf), QrCodeEcc::Low, 1024);
|
||||||
QrCodeEcc::Low,
|
|
||||||
1024,
|
|
||||||
);
|
|
||||||
let qr_code = match qr_code {
|
let qr_code = match qr_code {
|
||||||
Ok(q) => q,
|
Ok(q) => q,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -66,26 +64,29 @@ pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppCon
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().body(
|
||||||
.body(AddTotpPage {
|
AddTotpPage {
|
||||||
_p: BaseSettingsPage::get(
|
_p: BaseSettingsPage::get("New authenticator app", &user, None, None),
|
||||||
"New authenticator app",
|
|
||||||
&user,
|
|
||||||
None,
|
|
||||||
None),
|
|
||||||
qr_code: base64::encode(qr_code),
|
qr_code: base64::encode(qr_code),
|
||||||
account_name: key.account_name(&user, &app_conf),
|
account_name: key.account_name(&user, &app_conf),
|
||||||
secret_key: key.get_secret(),
|
secret_key: key.get_secret(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure a new security key factor
|
/// Configure a new security key factor
|
||||||
pub async fn add_webauthn_factor_route(user: CurrentUser, manager: WebAuthManagerReq) -> impl Responder {
|
pub async fn add_webauthn_factor_route(
|
||||||
|
user: CurrentUser,
|
||||||
|
manager: WebAuthManagerReq,
|
||||||
|
) -> impl Responder {
|
||||||
let registration_request = match manager.start_register(&user) {
|
let registration_request = match manager.start_register(&user) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to request new key! {:?}", e);
|
log::error!("Failed to request new key! {:?}", e);
|
||||||
return HttpResponse::InternalServerError().body("Failed to generate request for registration!");
|
return HttpResponse::InternalServerError()
|
||||||
|
.body("Failed to generate request for registration!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,15 +98,14 @@ pub async fn add_webauthn_factor_route(user: CurrentUser, manager: WebAuthManage
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().body(
|
||||||
.body(AddWebauhtnPage {
|
AddWebauhtnPage {
|
||||||
_p: BaseSettingsPage::get(
|
_p: BaseSettingsPage::get("New security key", &user, None, None),
|
||||||
"New security key",
|
|
||||||
&user,
|
|
||||||
None,
|
|
||||||
None),
|
|
||||||
|
|
||||||
opaque_state: registration_request.opaque_state,
|
opaque_state: registration_request.opaque_state,
|
||||||
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
||||||
}.render().unwrap())
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ impl AccessToken {
|
|||||||
jwt_id: None,
|
jwt_id: None,
|
||||||
nonce: self.nonce,
|
nonce: self.nonce,
|
||||||
custom: CustomAccessTokenClaims {
|
custom: CustomAccessTokenClaims {
|
||||||
rand_val: self.rand_val
|
rand_val: self.rand_val,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,8 @@ impl CodeChallenge {
|
|||||||
match self.code_challenge_method.as_str() {
|
match self.code_challenge_method.as_str() {
|
||||||
"plain" => code_verifer.eq(&self.code_challenge),
|
"plain" => code_verifer.eq(&self.code_challenge),
|
||||||
"S256" => {
|
"S256" => {
|
||||||
let encoded = base64::encode_config(
|
let encoded =
|
||||||
sha256(code_verifer.as_bytes()),
|
base64::encode_config(sha256(code_verifer.as_bytes()), URL_SAFE_NO_PAD);
|
||||||
URL_SAFE_NO_PAD,
|
|
||||||
);
|
|
||||||
|
|
||||||
encoded.eq(&self.code_challenge)
|
encoded.eq(&self.code_challenge)
|
||||||
}
|
}
|
||||||
@ -64,7 +62,10 @@ mod test {
|
|||||||
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
|
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(true, chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"));
|
assert_eq!(
|
||||||
|
true,
|
||||||
|
chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
|
||||||
|
);
|
||||||
assert_eq!(false, chal.verify_code("text1"));
|
assert_eq!(false, chal.verify_code("text1"));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
|
||||||
use aes_gcm::aead::{Aead, OsRng};
|
use aes_gcm::aead::{Aead, OsRng};
|
||||||
|
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@ -17,7 +17,9 @@ pub struct CryptoWrapper {
|
|||||||
impl CryptoWrapper {
|
impl CryptoWrapper {
|
||||||
/// Generate a new memory wrapper
|
/// Generate a new memory wrapper
|
||||||
pub fn new_random() -> Self {
|
pub fn new_random() -> Self {
|
||||||
Self { key: Aes256Gcm::generate_key(&mut OsRng) }
|
Self {
|
||||||
|
key: Aes256Gcm::generate_key(&mut OsRng),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt some data
|
/// Encrypt some data
|
||||||
@ -27,11 +29,11 @@ impl CryptoWrapper {
|
|||||||
|
|
||||||
let serialized_data = bincode::serialize(data)?;
|
let serialized_data = bincode::serialize(data)?;
|
||||||
|
|
||||||
let mut enc = aes_key.encrypt(Nonce::from_slice(&nonce_bytes),
|
let mut enc = aes_key
|
||||||
serialized_data.as_slice()).unwrap();
|
.encrypt(Nonce::from_slice(&nonce_bytes), serialized_data.as_slice())
|
||||||
|
.unwrap();
|
||||||
enc.extend_from_slice(&nonce_bytes);
|
enc.extend_from_slice(&nonce_bytes);
|
||||||
|
|
||||||
|
|
||||||
Ok(base64::encode(enc))
|
Ok(base64::encode(enc))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,8 +42,10 @@ impl CryptoWrapper {
|
|||||||
let bytes = base64::decode(input)?;
|
let bytes = base64::decode(input)?;
|
||||||
|
|
||||||
if bytes.len() < NONCE_LEN {
|
if bytes.len() < NONCE_LEN {
|
||||||
return Err(Box::new(std::io::Error::new(ErrorKind::Other,
|
return Err(Box::new(std::io::Error::new(
|
||||||
"Input string is smaller than nonce!")));
|
ErrorKind::Other,
|
||||||
|
"Input string is smaller than nonce!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN);
|
let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN);
|
||||||
@ -53,8 +57,10 @@ impl CryptoWrapper {
|
|||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decrypt wrapped data! {:#?}", e);
|
log::error!("Failed to decrypt wrapped data! {:#?}", e);
|
||||||
return Err(Box::new(std::io::Error::new(ErrorKind::Other,
|
return Err(Box::new(std::io::Error::new(
|
||||||
"Failed to decrypt wrapped data!")));
|
ErrorKind::Other,
|
||||||
|
"Failed to decrypt wrapped data!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ use std::pin::Pin;
|
|||||||
|
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{Error, FromRequest, HttpRequest, web};
|
|
||||||
use actix_web::dev::Payload;
|
use actix_web::dev::Payload;
|
||||||
use actix_web::error::ErrorInternalServerError;
|
use actix_web::error::ErrorInternalServerError;
|
||||||
|
use actix_web::{web, Error, FromRequest, HttpRequest};
|
||||||
|
|
||||||
use crate::actors::users_actor;
|
use crate::actors::users_actor;
|
||||||
use crate::actors::users_actor::UsersActor;
|
use crate::actors::users_actor::UsersActor;
|
||||||
@ -31,23 +31,29 @@ impl Deref for CurrentUser {
|
|||||||
|
|
||||||
impl FromRequest for CurrentUser {
|
impl FromRequest for CurrentUser {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = Pin<Box<dyn Future<Output=Result<Self, Self::Error>>>>;
|
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||||
|
|
||||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||||
let user_actor: &web::Data<Addr<UsersActor>> = req.app_data().expect("UserActor undefined!");
|
let user_actor: &web::Data<Addr<UsersActor>> =
|
||||||
|
req.app_data().expect("UserActor undefined!");
|
||||||
let user_actor: Addr<UsersActor> = user_actor.as_ref().clone();
|
let user_actor: Addr<UsersActor> = user_actor.as_ref().clone();
|
||||||
let identity: Identity = Identity::from_request(req, payload).into_inner()
|
let identity: Identity = Identity::from_request(req, payload)
|
||||||
|
.into_inner()
|
||||||
.expect("Failed to get identity!");
|
.expect("Failed to get identity!");
|
||||||
let user_id = SessionIdentity(Some(&identity)).user_id();
|
let user_id = SessionIdentity(Some(&identity)).user_id();
|
||||||
|
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let user = match user_actor.send(
|
let user = match user_actor
|
||||||
users_actor::GetUserRequest(user_id)
|
.send(users_actor::GetUserRequest(user_id))
|
||||||
).await.unwrap().0 {
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
{
|
||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
None => {
|
None => {
|
||||||
return Err(ErrorInternalServerError("Could not extract user information!"));
|
return Err(ErrorInternalServerError(
|
||||||
|
"Could not extract user information!",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@ use std::slice::{Iter, IterMut};
|
|||||||
|
|
||||||
use crate::utils::err::Res;
|
use crate::utils::err::Res;
|
||||||
|
|
||||||
enum FileFormat { Json, Yaml }
|
enum FileFormat {
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EntityManager<E> {
|
pub struct EntityManager<E> {
|
||||||
file_path: PathBuf,
|
file_path: PathBuf,
|
||||||
@ -11,7 +14,7 @@ pub struct EntityManager<E> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<E> EntityManager<E>
|
impl<E> EntityManager<E>
|
||||||
where
|
where
|
||||||
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
|
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
|
||||||
{
|
{
|
||||||
/// Open entity
|
/// Open entity
|
||||||
@ -30,7 +33,7 @@ impl<E> EntityManager<E>
|
|||||||
file_path: path.as_ref().to_path_buf(),
|
file_path: path.as_ref().to_path_buf(),
|
||||||
list: match Self::file_format(path.as_ref()) {
|
list: match Self::file_format(path.as_ref()) {
|
||||||
FileFormat::Json => serde_json::from_str(&file_content)?,
|
FileFormat::Json => serde_json::from_str(&file_content)?,
|
||||||
FileFormat::Yaml => serde_yaml::from_str(&file_content)?
|
FileFormat::Yaml => serde_yaml::from_str(&file_content)?,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -49,7 +52,7 @@ impl<E> EntityManager<E>
|
|||||||
fn file_format(p: &Path) -> FileFormat {
|
fn file_format(p: &Path) -> FileFormat {
|
||||||
match p.to_string_lossy().ends_with(".json") {
|
match p.to_string_lossy().ends_with(".json") {
|
||||||
true => FileFormat::Json,
|
true => FileFormat::Json,
|
||||||
false => FileFormat::Yaml
|
false => FileFormat::Yaml,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +27,9 @@ pub struct JWTSigner(RS256KeyPair);
|
|||||||
|
|
||||||
impl JWTSigner {
|
impl JWTSigner {
|
||||||
pub fn gen_from_memory() -> Res<Self> {
|
pub fn gen_from_memory() -> Res<Self> {
|
||||||
Ok(Self(RS256KeyPair::generate(2048)?
|
Ok(Self(
|
||||||
.with_key_id(&format!("key-{}", rand_str(15)))))
|
RS256KeyPair::generate(2048)?.with_key_id(&format!("key-{}", rand_str(15))),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_json_web_key(&self) -> JsonWebKey {
|
pub fn get_json_web_key(&self) -> JsonWebKey {
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
pub mod app_config;
|
|
||||||
pub mod entity_manager;
|
|
||||||
pub mod session_identity;
|
|
||||||
pub mod user;
|
|
||||||
pub mod client;
|
|
||||||
pub mod remote_ip;
|
|
||||||
pub mod current_user;
|
|
||||||
pub mod openid_config;
|
|
||||||
pub mod jwt_signer;
|
|
||||||
pub mod id_token;
|
|
||||||
pub mod code_challenge;
|
|
||||||
pub mod open_id_user_info;
|
|
||||||
pub mod access_token;
|
pub mod access_token;
|
||||||
pub mod totp_key;
|
pub mod app_config;
|
||||||
pub mod login_redirect;
|
pub mod client;
|
||||||
pub mod webauthn_manager;
|
pub mod code_challenge;
|
||||||
pub mod crypto_wrapper;
|
pub mod crypto_wrapper;
|
||||||
|
pub mod current_user;
|
||||||
|
pub mod entity_manager;
|
||||||
|
pub mod id_token;
|
||||||
|
pub mod jwt_signer;
|
||||||
|
pub mod login_redirect;
|
||||||
|
pub mod open_id_user_info;
|
||||||
|
pub mod openid_config;
|
||||||
|
pub mod remote_ip;
|
||||||
|
pub mod session_identity;
|
||||||
|
pub mod totp_key;
|
||||||
|
pub mod user;
|
||||||
|
pub mod webauthn_manager;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use actix_web::{Error, FromRequest, HttpRequest, web};
|
|
||||||
use actix_web::dev::Payload;
|
use actix_web::dev::Payload;
|
||||||
use futures_util::future::{Ready, ready};
|
use actix_web::{web, Error, FromRequest, HttpRequest};
|
||||||
|
use futures_util::future::{ready, Ready};
|
||||||
|
|
||||||
use crate::data::app_config::AppConfig;
|
use crate::data::app_config::AppConfig;
|
||||||
use crate::utils::network_utils::get_remote_ip;
|
use crate::utils::network_utils::get_remote_ip;
|
||||||
|
@ -33,8 +33,7 @@ impl<'a> SessionIdentity<'a> {
|
|||||||
fn get_session_data(&self) -> Option<SessionIdentityData> {
|
fn get_session_data(&self) -> Option<SessionIdentityData> {
|
||||||
if let Some(id) = self.0 {
|
if let Some(id) = self.0 {
|
||||||
Self::deserialize_session_data(id.id().ok())
|
Self::deserialize_session_data(id.id().ok())
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,12 +70,15 @@ impl<'a> SessionIdentity<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_user(&self, req: &HttpRequest, user: &User, status: SessionStatus) {
|
pub fn set_user(&self, req: &HttpRequest, user: &User, status: SessionStatus) {
|
||||||
self.set_session_data(req, &SessionIdentityData {
|
self.set_session_data(
|
||||||
|
req,
|
||||||
|
&SessionIdentityData {
|
||||||
id: Some(user.uid.clone()),
|
id: Some(user.uid.clone()),
|
||||||
is_admin: user.admin,
|
is_admin: user.admin,
|
||||||
auth_time: time(),
|
auth_time: time(),
|
||||||
status,
|
status,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_status(&self, req: &HttpRequest, status: SessionStatus) {
|
pub fn set_status(&self, req: &HttpRequest, status: SessionStatus) {
|
||||||
@ -108,7 +110,9 @@ impl<'a> SessionIdentity<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_id(&self) -> UserID {
|
pub fn user_id(&self) -> UserID {
|
||||||
self.get_session_data().unwrap_or_default().id
|
self.get_session_data()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.id
|
||||||
.expect("UserID should never be null here!")
|
.expect("UserID should never be null here!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,13 +23,15 @@ impl TotpKey {
|
|||||||
pub fn new_random() -> Self {
|
pub fn new_random() -> Self {
|
||||||
let random_bytes = rand::thread_rng().gen::<[u8; 10]>();
|
let random_bytes = rand::thread_rng().gen::<[u8; 10]>();
|
||||||
Self {
|
Self {
|
||||||
encoded: base32::encode(BASE32_ALPHABET, &random_bytes)
|
encoded: base32::encode(BASE32_ALPHABET, &random_bytes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a key from an encoded secret
|
/// Get a key from an encoded secret
|
||||||
pub fn from_encoded_secret(s: &str) -> Self {
|
pub fn from_encoded_secret(s: &str) -> Self {
|
||||||
Self { encoded: s.to_string() }
|
Self {
|
||||||
|
encoded: s.to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get QrCode URL for user
|
/// Get QrCode URL for user
|
||||||
@ -74,15 +76,19 @@ impl TotpKey {
|
|||||||
/// Get the code at a specific time
|
/// Get the code at a specific time
|
||||||
fn get_code_at<F: Fn() -> u64>(&self, get_time: F) -> Res<String> {
|
fn get_code_at<F: Fn() -> u64>(&self, get_time: F) -> Res<String> {
|
||||||
let gen = TotpGenerator::new()
|
let gen = TotpGenerator::new()
|
||||||
.set_digit(NUM_DIGITS).unwrap()
|
.set_digit(NUM_DIGITS)
|
||||||
.set_step(PERIOD).unwrap()
|
.unwrap()
|
||||||
|
.set_step(PERIOD)
|
||||||
|
.unwrap()
|
||||||
.set_hash_algorithm(HashAlgorithm::SHA1)
|
.set_hash_algorithm(HashAlgorithm::SHA1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let key = match base32::decode(BASE32_ALPHABET, &self.encoded) {
|
let key = match base32::decode(BASE32_ALPHABET, &self.encoded) {
|
||||||
None => {
|
None => {
|
||||||
return Err(Box::new(
|
return Err(Box::new(std::io::Error::new(
|
||||||
std::io::Error::new(ErrorKind::Other, "Failed to decode base32 secret!")));
|
ErrorKind::Other,
|
||||||
|
"Failed to decode base32 secret!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
Some(k) => k,
|
Some(k) => k,
|
||||||
};
|
};
|
||||||
|
@ -32,14 +32,32 @@ impl TwoFactor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn description_str(&self) -> &'static str {
|
||||||
|
match self.kind {
|
||||||
|
TwoFactorType::TOTP(_) => "Login by entering an OTP code",
|
||||||
|
TwoFactorType::WEBAUTHN(_) => "Login using a security key",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_image(&self) -> &'static str {
|
||||||
|
match self.kind {
|
||||||
|
TwoFactorType::TOTP(_) => "/assets/img/pin.svg",
|
||||||
|
TwoFactorType::WEBAUTHN(_) => "/assets/img/key.svg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String {
|
pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}",
|
TwoFactorType::TOTP(_) => format!("/2fa_otp?redirect={}", redirect_uri.get_encoded()),
|
||||||
self.id.0, redirect_uri.get_encoded()),
|
TwoFactorType::WEBAUTHN(_) => {
|
||||||
TwoFactorType::WEBAUTHN(_) => format!("/2fa_webauthn?id={}&redirect={}",
|
format!("/2fa_webauthn?redirect={}", redirect_uri.get_encoded())
|
||||||
self.id.0, redirect_uri.get_encoded()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_webauthn(&self) -> bool {
|
||||||
|
matches!(self.kind, TwoFactorType::WEBAUTHN(_))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
@ -71,7 +89,7 @@ impl User {
|
|||||||
pub fn can_access_app(&self, id: &ClientID) -> bool {
|
pub fn can_access_app(&self, id: &ClientID) -> bool {
|
||||||
match &self.authorized_clients {
|
match &self.authorized_clients {
|
||||||
None => true,
|
None => true,
|
||||||
Some(c) => c.contains(id)
|
Some(c) => c.contains(id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +112,49 @@ impl User {
|
|||||||
pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> {
|
pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> {
|
||||||
self.two_factor.iter().find(|f| f.id.eq(factor_id))
|
self.two_factor.iter().find(|f| f.id.eq(factor_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_webauthn_factor(&self) -> bool {
|
||||||
|
self.two_factor.iter().any(TwoFactor::is_webauthn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all registered OTP registered passwords
|
||||||
|
pub fn get_otp_factors(&self) -> Vec<TotpKey> {
|
||||||
|
self.two_factor.iter().fold(vec![], |mut acc, factor| {
|
||||||
|
if let TwoFactorType::TOTP(key) = &factor.kind {
|
||||||
|
acc.push(key.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all registered 2FA webauthn public keys
|
||||||
|
pub fn get_webauthn_pub_keys(&self) -> Vec<WebauthnPubKey> {
|
||||||
|
self.two_factor.iter().fold(vec![], |mut acc, factor| {
|
||||||
|
if let TwoFactorType::WEBAUTHN(key) = &factor.kind {
|
||||||
|
acc.push(*key.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first factor of each kind of factors
|
||||||
|
pub fn get_distinct_factors_types(&self) -> Vec<&TwoFactor> {
|
||||||
|
let mut urls = vec![];
|
||||||
|
|
||||||
|
self.two_factor
|
||||||
|
.iter()
|
||||||
|
.filter(|f| {
|
||||||
|
if urls.contains(&f.type_str()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
urls.push(f.type_str());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for User {
|
impl PartialEq for User {
|
||||||
|
@ -3,10 +3,15 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use webauthn_rs::prelude::{
|
||||||
|
CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||||
|
RequestChallengeResponse,
|
||||||
|
};
|
||||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||||
use webauthn_rs::prelude::{CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse};
|
|
||||||
|
|
||||||
use crate::constants::{APP_NAME, WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE};
|
use crate::constants::{
|
||||||
|
APP_NAME, WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE,
|
||||||
|
};
|
||||||
use crate::data::app_config::AppConfig;
|
use crate::data::app_config::AppConfig;
|
||||||
use crate::data::crypto_wrapper::CryptoWrapper;
|
use crate::data::crypto_wrapper::CryptoWrapper;
|
||||||
use crate::data::user::{User, UserID};
|
use crate::data::user::{User, UserID};
|
||||||
@ -42,7 +47,6 @@ struct AuthStateOpaqueData {
|
|||||||
expire: u64,
|
expire: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub type WebAuthManagerReq = web::Data<Arc<WebAuthManager>>;
|
pub type WebAuthManagerReq = web::Data<Arc<WebAuthManager>>;
|
||||||
|
|
||||||
pub struct WebAuthManager {
|
pub struct WebAuthManager {
|
||||||
@ -54,24 +58,23 @@ impl WebAuthManager {
|
|||||||
pub fn init(conf: &AppConfig) -> Self {
|
pub fn init(conf: &AppConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
core: WebauthnBuilder::new(
|
core: WebauthnBuilder::new(
|
||||||
conf.domain_name().split_once(':')
|
conf.domain_name()
|
||||||
|
.split_once(':')
|
||||||
.map(|s| s.0)
|
.map(|s| s.0)
|
||||||
.unwrap_or_else(|| conf.domain_name()),
|
.unwrap_or_else(|| conf.domain_name()),
|
||||||
&url::Url::parse(&conf.website_origin)
|
&url::Url::parse(&conf.website_origin)
|
||||||
.expect("Failed to parse configuration origin!"))
|
.expect("Failed to parse configuration origin!"),
|
||||||
|
)
|
||||||
.expect("Invalid Webauthn configuration")
|
.expect("Invalid Webauthn configuration")
|
||||||
.rp_name(APP_NAME)
|
.rp_name(APP_NAME)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build webauthn")
|
.expect("Failed to build webauthn"),
|
||||||
|
|
||||||
,
|
|
||||||
crypto_wrapper: CryptoWrapper::new_random(),
|
crypto_wrapper: CryptoWrapper::new_random(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_register(&self, user: &User) -> Res<RegisterKeyRequest> {
|
pub fn start_register(&self, user: &User) -> Res<RegisterKeyRequest> {
|
||||||
let (creation_challenge, registration_state)
|
let (creation_challenge, registration_state) = self.core.start_passkey_registration(
|
||||||
= self.core.start_passkey_registration(
|
|
||||||
Uuid::parse_str(&user.uid.0).expect("Failed to parse user id"),
|
Uuid::parse_str(&user.uid.0).expect("Failed to parse user id"),
|
||||||
&user.username,
|
&user.username,
|
||||||
&user.full_name(),
|
&user.full_name(),
|
||||||
@ -88,29 +91,43 @@ impl WebAuthManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish_registration(&self, user: &User, opaque_state: &str,
|
pub fn finish_registration(
|
||||||
pub_cred: RegisterPublicKeyCredential) -> Res<WebauthnPubKey> {
|
&self,
|
||||||
|
user: &User,
|
||||||
|
opaque_state: &str,
|
||||||
|
pub_cred: RegisterPublicKeyCredential,
|
||||||
|
) -> Res<WebauthnPubKey> {
|
||||||
let state: RegisterKeyOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
|
let state: RegisterKeyOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
|
||||||
if state.user_id != user.uid {
|
if state.user_id != user.uid {
|
||||||
return Err(Box::new(
|
return Err(Box::new(std::io::Error::new(
|
||||||
std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!")));
|
ErrorKind::Other,
|
||||||
|
"Invalid user for pubkey!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.expire < time() {
|
if state.expire < time() {
|
||||||
return Err(Box::new(
|
return Err(Box::new(std::io::Error::new(
|
||||||
std::io::Error::new(ErrorKind::Other, "Challenge has expired!")));
|
ErrorKind::Other,
|
||||||
|
"Challenge has expired!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = self.core
|
let res = self.core.finish_passkey_registration(
|
||||||
.finish_passkey_registration(&pub_cred, &serde_json::from_str(&state.registration_state)?)?;
|
&pub_cred,
|
||||||
|
&serde_json::from_str(&state.registration_state)?,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(WebauthnPubKey { creds: res })
|
Ok(WebauthnPubKey { creds: res })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_authentication(&self, user_id: &UserID, key: &WebauthnPubKey) -> Res<AuthRequest> {
|
pub fn start_authentication(
|
||||||
let (login_challenge, authentication_state) = self.core.start_passkey_authentication(&vec![
|
&self,
|
||||||
key.creds.clone()
|
user_id: &UserID,
|
||||||
])?;
|
keys: &[WebauthnPubKey],
|
||||||
|
) -> Res<AuthRequest> {
|
||||||
|
let (login_challenge, authentication_state) = self.core.start_passkey_authentication(
|
||||||
|
&keys.iter().map(|k| k.creds.clone()).collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(AuthRequest {
|
Ok(AuthRequest {
|
||||||
opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData {
|
opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData {
|
||||||
@ -122,21 +139,31 @@ impl WebAuthManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish_authentication(&self, user_id: &UserID, opaque_state: &str,
|
pub fn finish_authentication(
|
||||||
pub_cred: &PublicKeyCredential) -> Res {
|
&self,
|
||||||
|
user_id: &UserID,
|
||||||
|
opaque_state: &str,
|
||||||
|
pub_cred: &PublicKeyCredential,
|
||||||
|
) -> Res {
|
||||||
let state: AuthStateOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
|
let state: AuthStateOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
|
||||||
if &state.user_id != user_id {
|
if &state.user_id != user_id {
|
||||||
return Err(Box::new(
|
return Err(Box::new(std::io::Error::new(
|
||||||
std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!")));
|
ErrorKind::Other,
|
||||||
|
"Invalid user for pubkey!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.expire < time() {
|
if state.expire < time() {
|
||||||
return Err(Box::new(
|
return Err(Box::new(std::io::Error::new(
|
||||||
std::io::Error::new(ErrorKind::Other, "Challenge has expired!")));
|
ErrorKind::Other,
|
||||||
|
"Challenge has expired!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.core.finish_passkey_authentication(pub_cred,
|
self.core.finish_passkey_authentication(
|
||||||
&serde_json::from_str(&state.authentication_state)?)?;
|
pub_cred,
|
||||||
|
&serde_json::from_str(&state.authentication_state)?,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
167
src/main.rs
167
src/main.rs
@ -4,19 +4,19 @@ use std::sync::Arc;
|
|||||||
use actix::Actor;
|
use actix::Actor;
|
||||||
use actix_identity::config::LogoutBehaviour;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
use actix_session::SessionMiddleware;
|
|
||||||
use actix_session::storage::CookieSessionStore;
|
use actix_session::storage::CookieSessionStore;
|
||||||
use actix_web::{App, get, HttpResponse, HttpServer, middleware, web};
|
use actix_session::SessionMiddleware;
|
||||||
use actix_web::cookie::{Key, SameSite};
|
use actix_web::cookie::{Key, SameSite};
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
|
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
|
||||||
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
|
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
|
||||||
use basic_oidc::actors::users_actor::UsersActor;
|
use basic_oidc::actors::users_actor::UsersActor;
|
||||||
use basic_oidc::constants::*;
|
use basic_oidc::constants::*;
|
||||||
use basic_oidc::controllers::*;
|
|
||||||
use basic_oidc::controllers::assets_controller::assets_route;
|
use basic_oidc::controllers::assets_controller::assets_route;
|
||||||
|
use basic_oidc::controllers::*;
|
||||||
use basic_oidc::data::app_config::AppConfig;
|
use basic_oidc::data::app_config::AppConfig;
|
||||||
use basic_oidc::data::client::ClientManager;
|
use basic_oidc::data::client::ClientManager;
|
||||||
use basic_oidc::data::entity_manager::EntityManager;
|
use basic_oidc::data::entity_manager::EntityManager;
|
||||||
@ -72,8 +72,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let users_actor = UsersActor::new(users).start();
|
let users_actor = UsersActor::new(users).start();
|
||||||
let bruteforce_actor = BruteForceActor::default().start();
|
let bruteforce_actor = BruteForceActor::default().start();
|
||||||
let openid_sessions_actor = OpenIDSessionsActor::default().start();
|
let openid_sessions_actor = OpenIDSessionsActor::default().start();
|
||||||
let jwt_signer = JWTSigner::gen_from_memory()
|
let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
|
||||||
.expect("Failed to generate JWKS key");
|
|
||||||
let webauthn_manager = Arc::new(WebAuthManager::init(&config));
|
let webauthn_manager = Arc::new(WebAuthManager::init(&config));
|
||||||
|
|
||||||
log::info!("Server will listen on {}", config.listen_address);
|
log::info!("Server will listen on {}", config.listen_address);
|
||||||
@ -84,9 +83,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.expect("Failed to load clients list!");
|
.expect("Failed to load clients list!");
|
||||||
clients.apply_environment_variables();
|
clients.apply_environment_variables();
|
||||||
|
|
||||||
let session_mw =
|
let session_mw = SessionMiddleware::builder(
|
||||||
SessionMiddleware::builder(CookieSessionStore::default(),
|
CookieSessionStore::default(),
|
||||||
Key::from(config.token_key.as_bytes()))
|
Key::from(config.token_key.as_bytes()),
|
||||||
|
)
|
||||||
.cookie_name(SESSION_COOKIE_NAME.to_string())
|
.cookie_name(SESSION_COOKIE_NAME.to_string())
|
||||||
.cookie_secure(config.secure_cookie())
|
.cookie_secure(config.secure_cookie())
|
||||||
.cookie_same_site(SameSite::Lax)
|
.cookie_same_site(SameSite::Lax)
|
||||||
@ -106,72 +106,143 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.app_data(web::Data::new(clients))
|
.app_data(web::Data::new(clients))
|
||||||
.app_data(web::Data::new(jwt_signer.clone()))
|
.app_data(web::Data::new(jwt_signer.clone()))
|
||||||
.app_data(web::Data::new(webauthn_manager.clone()))
|
.app_data(web::Data::new(webauthn_manager.clone()))
|
||||||
|
.wrap(
|
||||||
.wrap(middleware::DefaultHeaders::new()
|
middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")),
|
||||||
.add(("Permissions-Policy", "interest-cohort=()")))
|
)
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(AuthMiddleware {})
|
.wrap(AuthMiddleware {})
|
||||||
.wrap(identity_middleware)
|
.wrap(identity_middleware)
|
||||||
.wrap(session_mw)
|
.wrap(session_mw)
|
||||||
|
|
||||||
// main route
|
// main route
|
||||||
.route("/", web::get()
|
.route(
|
||||||
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
|
"/",
|
||||||
|
web::get().to(|| async {
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header(("Location", "/settings"))
|
||||||
|
.finish()
|
||||||
|
}),
|
||||||
|
)
|
||||||
.route("/robots.txt", web::get().to(assets_controller::robots_txt))
|
.route("/robots.txt", web::get().to(assets_controller::robots_txt))
|
||||||
|
|
||||||
// health route
|
// health route
|
||||||
.service(health)
|
.service(health)
|
||||||
|
|
||||||
// Assets serving
|
// Assets serving
|
||||||
.route("/assets/{path:.*}", web::get().to(assets_route))
|
.route("/assets/{path:.*}", web::get().to(assets_route))
|
||||||
|
|
||||||
// Login pages
|
// Login pages
|
||||||
.route("/logout", web::get().to(login_controller::logout_route))
|
.route("/logout", web::get().to(login_controller::logout_route))
|
||||||
.route("/login", web::get().to(login_controller::login_route))
|
.route("/login", web::get().to(login_controller::login_route))
|
||||||
.route("/login", web::post().to(login_controller::login_route))
|
.route("/login", web::post().to(login_controller::login_route))
|
||||||
.route("/reset_password", web::get().to(login_controller::reset_password_route))
|
.route(
|
||||||
.route("/reset_password", web::post().to(login_controller::reset_password_route))
|
"/reset_password",
|
||||||
.route("/2fa_auth", web::get().to(login_controller::choose_2fa_method))
|
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::get().to(login_controller::login_with_otp))
|
||||||
.route("/2fa_otp", web::post().to(login_controller::login_with_otp))
|
.route("/2fa_otp", web::post().to(login_controller::login_with_otp))
|
||||||
.route("/2fa_webauthn", web::get().to(login_controller::login_with_webauthn))
|
.route(
|
||||||
|
"/2fa_webauthn",
|
||||||
|
web::get().to(login_controller::login_with_webauthn),
|
||||||
|
)
|
||||||
// Login api
|
// Login api
|
||||||
.route("/login/api/auth_webauthn", web::post().to(login_api::auth_webauthn))
|
.route(
|
||||||
|
"/login/api/auth_webauthn",
|
||||||
|
web::post().to(login_api::auth_webauthn),
|
||||||
|
)
|
||||||
// Settings routes
|
// Settings routes
|
||||||
.route("/settings", web::get().to(settings_controller::account_settings_details_route))
|
.route(
|
||||||
.route("/settings/change_password", web::get().to(settings_controller::change_password_route))
|
"/settings",
|
||||||
.route("/settings/change_password", web::post().to(settings_controller::change_password_route))
|
web::get().to(settings_controller::account_settings_details_route),
|
||||||
.route("/settings/two_factors", web::get().to(two_factors_controller::two_factors_route))
|
)
|
||||||
.route("/settings/two_factors/add_totp", web::get().to(two_factors_controller::add_totp_factor_route))
|
.route(
|
||||||
.route("/settings/two_factors/add_webauthn", web::get().to(two_factors_controller::add_webauthn_factor_route))
|
"/settings/change_password",
|
||||||
|
web::get().to(settings_controller::change_password_route),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/settings/change_password",
|
||||||
|
web::post().to(settings_controller::change_password_route),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/settings/two_factors",
|
||||||
|
web::get().to(two_factors_controller::two_factors_route),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/settings/two_factors/add_totp",
|
||||||
|
web::get().to(two_factors_controller::add_totp_factor_route),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/settings/two_factors/add_webauthn",
|
||||||
|
web::get().to(two_factors_controller::add_webauthn_factor_route),
|
||||||
|
)
|
||||||
// User API
|
// User API
|
||||||
.route("/settings/api/two_factor/save_totp_factor", web::post().to(two_factor_api::save_totp_factor))
|
.route(
|
||||||
.route("/settings/api/two_factor/save_webauthn_factor", web::post().to(two_factor_api::save_webauthn_factor))
|
"/settings/api/two_factor/save_totp_factor",
|
||||||
.route("/settings/api/two_factor/delete_factor", web::post().to(two_factor_api::delete_factor))
|
web::post().to(two_factor_api::save_totp_factor),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/settings/api/two_factor/save_webauthn_factor",
|
||||||
|
web::post().to(two_factor_api::save_webauthn_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(
|
||||||
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
|
"/admin",
|
||||||
.route("/admin/clients", web::get().to(admin_controller::clients_route))
|
web::get().to(|| async {
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header(("Location", "/settings"))
|
||||||
|
.finish()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/clients",
|
||||||
|
web::get().to(admin_controller::clients_route),
|
||||||
|
)
|
||||||
.route("/admin/users", web::get().to(admin_controller::users_route))
|
.route("/admin/users", web::get().to(admin_controller::users_route))
|
||||||
.route("/admin/users", web::post().to(admin_controller::users_route))
|
.route(
|
||||||
.route("/admin/create_user", web::get().to(admin_controller::create_user))
|
"/admin/users",
|
||||||
.route("/admin/edit_user", web::get().to(admin_controller::edit_user))
|
web::post().to(admin_controller::users_route),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/create_user",
|
||||||
|
web::get().to(admin_controller::create_user),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/edit_user",
|
||||||
|
web::get().to(admin_controller::edit_user),
|
||||||
|
)
|
||||||
// Admin API
|
// Admin API
|
||||||
.route("/admin/api/find_username", web::post().to(admin_api::find_username))
|
.route(
|
||||||
.route("/admin/api/delete_user", web::post().to(admin_api::delete_user))
|
"/admin/api/find_username",
|
||||||
|
web::post().to(admin_api::find_username),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/api/delete_user",
|
||||||
|
web::post().to(admin_api::delete_user),
|
||||||
|
)
|
||||||
// OpenID routes
|
// OpenID routes
|
||||||
.route("/.well-known/openid-configuration", web::get().to(openid_controller::get_configuration))
|
.route(
|
||||||
|
"/.well-known/openid-configuration",
|
||||||
|
web::get().to(openid_controller::get_configuration),
|
||||||
|
)
|
||||||
.route(AUTHORIZE_URI, web::get().to(openid_controller::authorize))
|
.route(AUTHORIZE_URI, web::get().to(openid_controller::authorize))
|
||||||
.route(TOKEN_URI, web::post().to(openid_controller::token))
|
.route(TOKEN_URI, web::post().to(openid_controller::token))
|
||||||
.route(CERT_URI, web::get().to(openid_controller::cert_uri))
|
.route(CERT_URI, web::get().to(openid_controller::cert_uri))
|
||||||
.route(USERINFO_URI, web::post().to(openid_controller::user_info_post))
|
.route(
|
||||||
.route(USERINFO_URI, web::get().to(openid_controller::user_info_get))
|
USERINFO_URI,
|
||||||
|
web::post().to(openid_controller::user_info_post),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
USERINFO_URI,
|
||||||
|
web::get().to(openid_controller::user_info_get),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.bind(listen_address)?
|
.bind(listen_address)?
|
||||||
.run()
|
.run()
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
//! # Authentication middleware
|
//! # Authentication middleware
|
||||||
|
|
||||||
use std::future::{Future, ready, Ready};
|
use std::future::{ready, Future, Ready};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use actix_identity::IdentityExt;
|
use actix_identity::IdentityExt;
|
||||||
use actix_web::{
|
|
||||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
|
||||||
Error, HttpResponse, web,
|
|
||||||
};
|
|
||||||
use actix_web::body::EitherBody;
|
use actix_web::body::EitherBody;
|
||||||
use actix_web::http::{header, Method};
|
use actix_web::http::{header, Method};
|
||||||
|
use actix_web::{
|
||||||
|
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
|
web, Error, HttpResponse,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI, TOKEN_URI, USERINFO_URI};
|
use crate::constants::{
|
||||||
|
ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI, TOKEN_URI, USERINFO_URI,
|
||||||
|
};
|
||||||
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user_for_login};
|
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user_for_login};
|
||||||
use crate::data::app_config::AppConfig;
|
use crate::data::app_config::AppConfig;
|
||||||
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
|
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
|
||||||
@ -27,8 +29,8 @@ pub struct AuthMiddleware;
|
|||||||
// `S` - type of the next service
|
// `S` - type of the next service
|
||||||
// `B` - type of response's body
|
// `B` - type of response's body
|
||||||
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
|
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
B: 'static,
|
B: 'static,
|
||||||
{
|
{
|
||||||
@ -62,14 +64,13 @@ impl ConnStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct AuthInnerMiddleware<S> {
|
pub struct AuthInnerMiddleware<S> {
|
||||||
service: Rc<S>,
|
service: Rc<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
B: 'static,
|
B: 'static,
|
||||||
{
|
{
|
||||||
@ -77,7 +78,7 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
type Future = Pin<Box<dyn Future<Output=Result<Self::Response, Self::Error>>>>;
|
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||||
|
|
||||||
forward_ready!(service);
|
forward_ready!(service);
|
||||||
|
|
||||||
@ -90,7 +91,8 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
|||||||
|
|
||||||
// Check if POST request comes from another website (block invalid origins)
|
// Check if POST request comes from another website (block invalid origins)
|
||||||
let origin = req.headers().get(header::ORIGIN);
|
let origin = req.headers().get(header::ORIGIN);
|
||||||
if req.method() == Method::POST && req.path() != TOKEN_URI && req.path() != USERINFO_URI {
|
if req.method() == Method::POST && req.path() != TOKEN_URI && req.path() != USERINFO_URI
|
||||||
|
{
|
||||||
if let Some(o) = origin {
|
if let Some(o) = origin {
|
||||||
if !o.to_str().unwrap_or("bad").eq(&config.website_origin) {
|
if !o.to_str().unwrap_or("bad").eq(&config.website_origin) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@ -135,10 +137,13 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
|||||||
// Redirect user to login page
|
// Redirect user to login page
|
||||||
if !session.is_auth()
|
if !session.is_auth()
|
||||||
&& (req.path().starts_with(ADMIN_ROUTES)
|
&& (req.path().starts_with(ADMIN_ROUTES)
|
||||||
|| req.path().starts_with(AUTHENTICATED_ROUTES) || req.path().eq(AUTHORIZE_URI))
|
|| req.path().starts_with(AUTHENTICATED_ROUTES)
|
||||||
|
|| req.path().eq(AUTHORIZE_URI))
|
||||||
{
|
{
|
||||||
log::debug!("Redirect unauthenticated user from {} to authorization route.",
|
log::debug!(
|
||||||
req.path());
|
"Redirect unauthenticated user from {} to authorization route.",
|
||||||
|
req.path()
|
||||||
|
);
|
||||||
|
|
||||||
let path = req.uri().to_string();
|
let path = req.uri().to_string();
|
||||||
return Ok(req
|
return Ok(req
|
||||||
@ -149,10 +154,9 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
|||||||
// Restrict access to admin pages
|
// Restrict access to admin pages
|
||||||
if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) {
|
if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) {
|
||||||
return Ok(req
|
return Ok(req
|
||||||
.into_response(
|
.into_response(HttpResponse::Unauthorized().body(build_fatal_error_page(
|
||||||
HttpResponse::Unauthorized().body(
|
"You are not allowed to access this resource.",
|
||||||
build_fatal_error_page("You are not allowed to access this resource.")),
|
)))
|
||||||
)
|
|
||||||
.map_into_right_body());
|
.map_into_right_body());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
pub mod crypt_utils;
|
||||||
pub mod err;
|
pub mod err;
|
||||||
pub mod time;
|
|
||||||
pub mod network_utils;
|
pub mod network_utils;
|
||||||
pub mod string_utils;
|
pub mod string_utils;
|
||||||
pub mod crypt_utils;
|
pub mod time;
|
||||||
|
@ -16,7 +16,6 @@ pub fn match_ip(pattern: &str, ip: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Get the remote IP address
|
/// Get the remote IP address
|
||||||
pub fn get_remote_ip(req: &HttpRequest, proxy_ip: Option<&str>) -> IpAddr {
|
pub fn get_remote_ip(req: &HttpRequest, proxy_ip: Option<&str>) -> IpAddr {
|
||||||
let mut ip = req.peer_addr().unwrap().ip();
|
let mut ip = req.peer_addr().unwrap().ip();
|
||||||
@ -78,7 +77,10 @@ mod test {
|
|||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
assert_eq!(get_remote_ip(&req, None), "192.168.1.1".parse::<IpAddr>().unwrap());
|
assert_eq!(
|
||||||
|
get_remote_ip(&req, None),
|
||||||
|
"192.168.1.1".parse::<IpAddr>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -87,7 +89,10 @@ mod test {
|
|||||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||||
.insert_header(("X-Forwarded-For", "1.1.1.1"))
|
.insert_header(("X-Forwarded-For", "1.1.1.1"))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
assert_eq!(get_remote_ip(&req, Some("192.168.1.1")), "1.1.1.1".parse::<IpAddr>().unwrap());
|
assert_eq!(
|
||||||
|
get_remote_ip(&req, Some("192.168.1.1")),
|
||||||
|
"1.1.1.1".parse::<IpAddr>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -96,7 +101,10 @@ mod test {
|
|||||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||||
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
assert_eq!(get_remote_ip(&req, Some("192.168.1.1")), "1.1.1.1".parse::<IpAddr>().unwrap());
|
assert_eq!(
|
||||||
|
get_remote_ip(&req, Some("192.168.1.1")),
|
||||||
|
"1.1.1.1".parse::<IpAddr>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -105,7 +113,10 @@ mod test {
|
|||||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||||
.insert_header(("X-Forwarded-For", "10::1, 1.2.2.2"))
|
.insert_header(("X-Forwarded-For", "10::1, 1.2.2.2"))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
assert_eq!(get_remote_ip(&req, Some("192.168.1.1")), "10::".parse::<IpAddr>().unwrap());
|
assert_eq!(
|
||||||
|
get_remote_ip(&req, Some("192.168.1.1")),
|
||||||
|
"10::".parse::<IpAddr>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -114,7 +125,10 @@ mod test {
|
|||||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||||
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
assert_eq!(get_remote_ip(&req, None), "192.168.1.1".parse::<IpAddr>().unwrap());
|
assert_eq!(
|
||||||
|
get_remote_ip(&req, None),
|
||||||
|
"192.168.1.1".parse::<IpAddr>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -123,7 +137,10 @@ mod test {
|
|||||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||||
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
assert_eq!(get_remote_ip(&req, Some("192.168.1.2")), "192.168.1.1".parse::<IpAddr>().unwrap());
|
assert_eq!(
|
||||||
|
get_remote_ip(&req, Some("192.168.1.2")),
|
||||||
|
"192.168.1.1".parse::<IpAddr>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -141,7 +158,10 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_ip_v6_address() {
|
fn parse_ip_v6_address() {
|
||||||
let ip = parse_ip("2a00:1450:4007:813::200e").unwrap();
|
let ip = parse_ip("2a00:1450:4007:813::200e").unwrap();
|
||||||
assert_eq!(ip, IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4007, 0x813, 0, 0, 0, 0)));
|
assert_eq!(
|
||||||
|
ip,
|
||||||
|
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4007, 0x813, 0, 0, 0, 0))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -16,7 +16,11 @@ pub fn apply_env_vars(val: &str) -> String {
|
|||||||
let mut val = val.to_string();
|
let mut val = val.to_string();
|
||||||
|
|
||||||
if let Some(varname_with_wrapper) = regex_find!(r#"\$\{[a-zA-Z0-9_-]+\}"#, &val) {
|
if let Some(varname_with_wrapper) = regex_find!(r#"\$\{[a-zA-Z0-9_-]+\}"#, &val) {
|
||||||
let varname = varname_with_wrapper.strip_prefix("${").unwrap().strip_suffix('}').unwrap();
|
let varname = varname_with_wrapper
|
||||||
|
.strip_prefix("${")
|
||||||
|
.unwrap()
|
||||||
|
.strip_suffix('}')
|
||||||
|
.unwrap();
|
||||||
let value = match std::env::var(varname) {
|
let value = match std::env::var(varname) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -34,8 +38,8 @@ pub fn apply_env_vars(val: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::env;
|
|
||||||
use crate::utils::string_utils::apply_env_vars;
|
use crate::utils::string_utils::apply_env_vars;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
const VAR_ONE: &str = "VAR_ONE";
|
const VAR_ONE: &str = "VAR_ONE";
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -4,13 +4,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>You need to validate a second factor to complete your login.</p>
|
<p>You need to validate a second factor to complete your login.</p>
|
||||||
|
|
||||||
{% for factor in factors %}
|
{% for factor in user.get_distinct_factors_types() %}
|
||||||
<p>
|
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%; display: flex;">
|
||||||
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%;">
|
<img src="{{ factor.type_image() }}" alt="Factor icon" style="margin-right: 1em;" />
|
||||||
{{ factor.name }} <br/>
|
<div style="text-align: left;">
|
||||||
<small>{{ factor.type_str() }}</small>
|
{{ factor.type_str() }} <br/>
|
||||||
|
<small style="font-size: 0.7em;">{{ factor.description_str() }}</small>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
<br />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>Please go to your authenticator app <i>{{ factor.name }}</i>, generate a new code and enter it here:</p>
|
<p>Please open one of your registered authenticator app, generate a new code and enter it here:</p>
|
||||||
<form id="totp_form" method="post" action="{{ factor.login_url(_p.redirect_uri) }}">
|
<form id="totp_form" method="post">
|
||||||
<input type="hidden" id="code" name="code"/>
|
<input type="hidden" id="code" name="code"/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div id="otp" class="inputs d-flex flex-row justify-content-center mt-2">
|
<div id="otp" class="inputs d-flex flex-row justify-content-center mt-2">
|
||||||
@ -34,6 +34,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function OTPInput() {
|
function OTPInput() {
|
||||||
|
// Set form destination
|
||||||
|
document.getElementById("totp_form").action = location.href;
|
||||||
|
|
||||||
const inputs = document.querySelectorAll('#otp > *[id]');
|
const inputs = document.querySelectorAll('#otp > *[id]');
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
// Reset form on init
|
// Reset form on init
|
@ -4,7 +4,7 @@
|
|||||||
<p style="color:red" id="err_target"></p>
|
<p style="color:red" id="err_target"></p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>Please insert now your security key <i>{{ factor.name }}</i>, and accept authentication request.</p>
|
<p>Please insert now on of your registered security key, and accept authentication request.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin: 10px 0px;">
|
<div style="margin: 10px 0px;">
|
||||||
|
Loading…
Reference in New Issue
Block a user