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,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BruteForceActor {
|
||||
failed_attempts: HashMap<IpAddr, Vec<u64>>,
|
||||
@ -28,10 +27,7 @@ pub struct BruteForceActor {
|
||||
impl BruteForceActor {
|
||||
pub fn clean_attempts(&mut self) {
|
||||
#[allow(clippy::map_clone)]
|
||||
let keys = self.failed_attempts
|
||||
.keys()
|
||||
.map(|i| *i)
|
||||
.collect::<Vec<_>>();
|
||||
let keys = self.failed_attempts.keys().map(|i| *i).collect::<Vec<_>>();
|
||||
|
||||
for ip in keys {
|
||||
// Remove old attempts
|
||||
@ -102,7 +98,9 @@ mod test {
|
||||
let mut actor = BruteForceActor::default();
|
||||
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_3, vec![time() + 10, time() + 20]);
|
||||
actor
|
||||
.failed_attempts
|
||||
.insert(IP_3, vec![time() + 10, time() + 20]);
|
||||
|
||||
actor.clean_attempts();
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
pub mod users_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::{Actor, AsyncContext, Context, Handler};
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::data::access_token::AccessToken;
|
||||
@ -37,13 +37,16 @@ pub struct Session {
|
||||
|
||||
impl Session {
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn regenerate_access_and_refresh_tokens(&mut self,
|
||||
app_config: &AppConfig,
|
||||
jwt_signer: &JWTSigner) -> Res {
|
||||
pub fn regenerate_access_and_refresh_tokens(
|
||||
&mut self,
|
||||
app_config: &AppConfig,
|
||||
jwt_signer: &JWTSigner,
|
||||
) -> Res {
|
||||
let access_token = AccessToken {
|
||||
issuer: app_config.website_origin.to_string(),
|
||||
subject_identifier: self.user.clone().0,
|
||||
@ -116,7 +119,11 @@ impl Handler<PushNewSession> for OpenIDSessionsActor {
|
||||
impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
|
||||
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
|
||||
.iter()
|
||||
.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 {
|
||||
self.session
|
||||
.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()
|
||||
}
|
||||
}
|
||||
@ -150,9 +162,14 @@ impl Handler<UpdateSession> for OpenIDSessionsActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result {
|
||||
if let Some(r) = self.session.iter().enumerate()
|
||||
.find(|f| f.1.session_id.eq(&msg.0.session_id)).map(|f| f.0) {
|
||||
if let Some(r) = self
|
||||
.session
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|f| f.1.session_id.eq(&msg.0.session_id))
|
||||
.map(|f| f.0)
|
||||
{
|
||||
self.session[r] = msg.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,9 @@ impl Handler<FindUserByUsername> for UsersActor {
|
||||
type Result = MessageResult<FindUserByUsername>;
|
||||
|
||||
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 {
|
||||
let user = match self.manager.find_by_user_id(&msg.0) {
|
||||
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));
|
||||
}
|
||||
Some(s) => s
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
MessageResult(DeleteUserResult(match self.manager.remove(&user) {
|
||||
@ -169,4 +174,4 @@ impl Handler<DeleteUserRequest> for UsersActor {
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,4 +60,4 @@ pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000;
|
||||
|
||||
/// Webauthn constants
|
||||
pub const WEBAUTHN_REGISTER_CHALLENGE_EXPIRE: u64 = 3600;
|
||||
pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600;
|
||||
pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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::data::current_user::CurrentUser;
|
||||
@ -15,10 +15,16 @@ struct FindUserResult {
|
||||
user_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn find_username(req: web::Form<FindUserNameReq>, users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
let res = users.send(FindUserByUsername(req.0.username)).await.unwrap();
|
||||
pub async fn find_username(
|
||||
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 {
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
pub async fn delete_user(user: CurrentUser, req: web::Form<DeleteUserReq>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
pub async fn delete_user(
|
||||
user: CurrentUser,
|
||||
req: web::Form<DeleteUserReq>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
if user.uid == req.user_id {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
|
||||
use crate::actors::users_actor;
|
||||
@ -35,17 +35,15 @@ struct EditUserTemplate {
|
||||
clients: Vec<Client>,
|
||||
}
|
||||
|
||||
|
||||
pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
|
||||
HttpResponse::Ok().body(ClientsListTemplate {
|
||||
_p: BaseSettingsPage::get(
|
||||
"Clients list",
|
||||
&user,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
clients: clients.cloned(),
|
||||
}.render().unwrap())
|
||||
HttpResponse::Ok().body(
|
||||
ClientsListTemplate {
|
||||
_p: BaseSettingsPage::get("Clients list", &user, None, None),
|
||||
clients: clients.cloned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
@ -63,13 +61,20 @@ pub struct UpdateUserQuery {
|
||||
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 success = None;
|
||||
|
||||
if let Some(update) = update_query {
|
||||
let current_user: Option<User> = users.send(users_actor::FindUserByUsername(update.username.to_string()))
|
||||
.await.unwrap().0;
|
||||
let current_user: Option<User> = users
|
||||
.send(users_actor::FindUserByUsername(update.username.to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
let is_creating = current_user.is_none();
|
||||
|
||||
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();
|
||||
|
||||
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() {
|
||||
"all_clients" => None,
|
||||
"custom_clients" => Some(update.0.granted_clients.split(',')
|
||||
.map(|c| ClientID(c.to_string()))
|
||||
.collect::<Vec<_>>()),
|
||||
_ => Some(Vec::new())
|
||||
"custom_clients" => Some(
|
||||
update
|
||||
.0
|
||||
.granted_clients
|
||||
.split(',')
|
||||
.map(|c| ClientID(c.to_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
_ => Some(Vec::new()),
|
||||
};
|
||||
|
||||
let new_password = match update.0.gen_new_password.is_some() {
|
||||
false => None,
|
||||
true => {
|
||||
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
|
||||
user.password = hash_password(&temp_pass)
|
||||
.expect("Failed to hash password");
|
||||
user.password = hash_password(&temp_pass).expect("Failed to hash password");
|
||||
user.need_reset_password = true;
|
||||
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 {
|
||||
danger = Some(match is_creating {
|
||||
true => "Failed to create user!",
|
||||
false => "Failed to update user!"
|
||||
}.to_string())
|
||||
danger = Some(
|
||||
match is_creating {
|
||||
true => "Failed to create user!",
|
||||
false => "Failed to update user!",
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
success = Some(match is_creating {
|
||||
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 {
|
||||
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;
|
||||
|
||||
HttpResponse::Ok().body(UsersListTemplate {
|
||||
_p: BaseSettingsPage::get(
|
||||
"Users list",
|
||||
&user,
|
||||
danger,
|
||||
success,
|
||||
),
|
||||
users,
|
||||
}.render().unwrap())
|
||||
HttpResponse::Ok().body(
|
||||
UsersListTemplate {
|
||||
_p: BaseSettingsPage::get("Users list", &user, danger, success),
|
||||
users,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn create_user(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
|
||||
HttpResponse::Ok().body(EditUserTemplate {
|
||||
_p: BaseSettingsPage::get("Create a new user", user.deref(), None, None),
|
||||
u: Default::default(),
|
||||
clients: clients.cloned(),
|
||||
}.render().unwrap())
|
||||
HttpResponse::Ok().body(
|
||||
EditUserTemplate {
|
||||
_p: BaseSettingsPage::get("Create a new user", user.deref(), None, None),
|
||||
u: Default::default(),
|
||||
clients: clients.cloned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@ -150,26 +172,33 @@ pub struct EditUserQuery {
|
||||
id: UserID,
|
||||
}
|
||||
|
||||
pub async fn edit_user(user: CurrentUser,
|
||||
clients: web::Data<ClientManager>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
query: web::Query<EditUserQuery>,
|
||||
pub async fn edit_user(
|
||||
user: CurrentUser,
|
||||
clients: web::Data<ClientManager>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
query: web::Query<EditUserQuery>,
|
||||
) -> impl Responder {
|
||||
let edited_account = users.send(users_actor::GetUserRequest(query.0.id))
|
||||
.await.unwrap().0;
|
||||
let edited_account = users
|
||||
.send(users_actor::GetUserRequest(query.0.id))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
|
||||
HttpResponse::Ok().body(EditUserTemplate {
|
||||
_p: BaseSettingsPage::get(
|
||||
"Edit user account",
|
||||
user.deref(),
|
||||
match edited_account.is_none() {
|
||||
true => Some("Could not find requested user!".to_string()),
|
||||
false => None
|
||||
},
|
||||
None,
|
||||
),
|
||||
u: edited_account.unwrap_or_default(),
|
||||
clients: clients.cloned(),
|
||||
}.render().unwrap())
|
||||
HttpResponse::Ok().body(
|
||||
EditUserTemplate {
|
||||
_p: BaseSettingsPage::get(
|
||||
"Edit user account",
|
||||
user.deref(),
|
||||
match edited_account.is_none() {
|
||||
true => Some("Could not find requested user!".to_string()),
|
||||
false => None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
u: edited_account.unwrap_or_default(),
|
||||
clients: clients.cloned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use actix_web::{HttpResponse, web};
|
||||
use include_dir::{Dir, include_dir};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use include_dir::{include_dir, Dir};
|
||||
|
||||
/// Assets directory
|
||||
static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets");
|
||||
@ -23,4 +23,4 @@ pub async fn assets_route(path: web::Path<String>) -> HttpResponse {
|
||||
.body(file.contents())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,4 @@ struct FatalErrorPage {
|
||||
|
||||
pub fn build_fatal_error_page(msg: &'static str) -> String {
|
||||
FatalErrorPage { message: msg }.render().unwrap()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
use webauthn_rs::prelude::PublicKeyCredential;
|
||||
|
||||
use crate::data::session_identity::{SessionIdentity, SessionStatus};
|
||||
@ -11,10 +11,12 @@ pub struct AuthWebauthnRequest {
|
||||
credential: PublicKeyCredential,
|
||||
}
|
||||
|
||||
pub async fn auth_webauthn(id: Identity,
|
||||
req: web::Json<AuthWebauthnRequest>,
|
||||
manager: WebAuthManagerReq,
|
||||
http_req: HttpRequest) -> impl Responder {
|
||||
pub async fn auth_webauthn(
|
||||
id: Identity,
|
||||
req: web::Json<AuthWebauthnRequest>,
|
||||
manager: WebAuthManagerReq,
|
||||
http_req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
if !SessionIdentity(Some(&id)).need_2fa_auth() {
|
||||
return HttpResponse::Unauthorized().json("No 2FA required!");
|
||||
}
|
||||
@ -31,4 +33,4 @@ pub async fn auth_webauthn(id: Identity,
|
||||
HttpResponse::InternalServerError().body("Failed to validate security key!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
use actix::Addr;
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
|
||||
use crate::actors::{bruteforce_actor, users_actor};
|
||||
use crate::actors::bruteforce_actor::BruteForceActor;
|
||||
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::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::remote_ip::RemoteIP;
|
||||
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;
|
||||
|
||||
struct BaseLoginPage<'a> {
|
||||
@ -40,26 +42,23 @@ struct PasswordResetTemplate<'a> {
|
||||
#[template(path = "login/choose_second_factor.html")]
|
||||
struct ChooseSecondFactorTemplate<'a> {
|
||||
_p: BaseLoginPage<'a>,
|
||||
factors: &'a [TwoFactor],
|
||||
user: &'a User,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login/opt_input.html")]
|
||||
#[template(path = "login/otp_input.html")]
|
||||
struct LoginWithOTPTemplate<'a> {
|
||||
_p: BaseLoginPage<'a>,
|
||||
factor: &'a TwoFactor,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login/webauthn_input.html")]
|
||||
struct LoginWithWebauthnTemplate<'a> {
|
||||
_p: BaseLoginPage<'a>,
|
||||
factor: &'a TwoFactor,
|
||||
opaque_state: String,
|
||||
challenge_json: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct LoginRequestBody {
|
||||
login: String,
|
||||
@ -87,13 +86,17 @@ pub async fn login_route(
|
||||
let mut success = None;
|
||||
let mut login = String::new();
|
||||
|
||||
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 {
|
||||
return HttpResponse::TooManyRequests().body(
|
||||
build_fatal_error_page("Too many failed login attempts, please try again later!")
|
||||
);
|
||||
return HttpResponse::TooManyRequests().body(build_fatal_error_page(
|
||||
"Too many failed login attempts, please try again later!",
|
||||
));
|
||||
}
|
||||
|
||||
// Check if user session must be closed
|
||||
@ -103,22 +106,24 @@ pub async fn login_route(
|
||||
}
|
||||
success = Some("Goodbye!".to_string());
|
||||
}
|
||||
|
||||
// Check if user is already authenticated
|
||||
else if SessionIdentity(id.as_ref()).is_authenticated() {
|
||||
return redirect_user(query.redirect.get());
|
||||
}
|
||||
|
||||
// Check if the password of the user has to be changed
|
||||
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
|
||||
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
|
||||
else if let Some(req) = &req {
|
||||
login = req.login.clone();
|
||||
@ -150,10 +155,20 @@ pub async fn login_route(
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap();
|
||||
bruteforce
|
||||
.send(bruteforce_actor::RecordFailedAttempt {
|
||||
ip: remote_ip.into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -169,8 +184,8 @@ pub async fn login_route(
|
||||
},
|
||||
login,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -191,10 +206,13 @@ pub struct PasswordResetQuery {
|
||||
}
|
||||
|
||||
/// Reset user password route
|
||||
pub async fn reset_password_route(id: Option<Identity>, query: web::Query<PasswordResetQuery>,
|
||||
req: Option<web::Form<ChangePasswordRequestBody>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
http_req: HttpRequest) -> impl Responder {
|
||||
pub async fn reset_password_route(
|
||||
id: Option<Identity>,
|
||||
query: web::Query<PasswordResetQuery>,
|
||||
req: Option<web::Form<ChangePasswordRequestBody>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
http_req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let mut danger = None;
|
||||
|
||||
if !SessionIdentity(id.as_ref()).need_new_password() {
|
||||
@ -235,8 +253,8 @@ pub async fn reset_password_route(id: Option<Identity>, query: web::Query<Passwo
|
||||
},
|
||||
min_pass_len: MIN_PASS_LEN,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -249,18 +267,27 @@ pub struct ChooseSecondFactorQuery {
|
||||
}
|
||||
|
||||
/// Let the user select the factor to use to authenticate
|
||||
pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSecondFactorQuery>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
pub async fn choose_2fa_method(
|
||||
id: Option<Identity>,
|
||||
query: web::Query<ChooseSecondFactorQuery>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||
log::trace!("User does not require 2fa auth, redirecting");
|
||||
return redirect_user_for_login(query.redirect.get());
|
||||
}
|
||||
|
||||
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
|
||||
.await.unwrap().0.expect("Could not find user!");
|
||||
let user: User = users
|
||||
.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
|
||||
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");
|
||||
return redirect_user(&user.two_factor[0].login_url(&query.redirect));
|
||||
}
|
||||
@ -274,10 +301,10 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
|
||||
app_name: APP_NAME,
|
||||
redirect_uri: &query.redirect,
|
||||
},
|
||||
factors: &user.two_factor,
|
||||
user: &user,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -285,7 +312,6 @@ pub async fn choose_2fa_method(id: Option<Identity>, query: web::Query<ChooseSec
|
||||
pub struct LoginWithOTPQuery {
|
||||
#[serde(default)]
|
||||
redirect: LoginRedirect,
|
||||
id: FactorID,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@ -293,35 +319,39 @@ pub struct LoginWithOTPForm {
|
||||
code: String,
|
||||
}
|
||||
|
||||
|
||||
/// Login with OTP
|
||||
pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTPQuery>,
|
||||
form: Option<web::Form<LoginWithOTPForm>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
http_req: HttpRequest) -> impl Responder {
|
||||
pub async fn login_with_otp(
|
||||
id: Option<Identity>,
|
||||
query: web::Query<LoginWithOTPQuery>,
|
||||
form: Option<web::Form<LoginWithOTPForm>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
http_req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let mut danger = None;
|
||||
|
||||
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||
return redirect_user_for_login(query.redirect.get());
|
||||
}
|
||||
|
||||
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
|
||||
.await.unwrap().0.expect("Could not find user!");
|
||||
let user: User = users
|
||||
.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) {
|
||||
Some(f) => f,
|
||||
None => 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!"));
|
||||
}
|
||||
};
|
||||
let keys = user.get_otp_factors();
|
||||
if keys.is_empty() {
|
||||
return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"));
|
||||
}
|
||||
|
||||
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());
|
||||
} else {
|
||||
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
|
||||
@ -329,56 +359,60 @@ pub async fn login_with_otp(id: Option<Identity>, query: web::Query<LoginWithOTP
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok().body(LoginWithOTPTemplate {
|
||||
_p: BaseLoginPage {
|
||||
danger,
|
||||
success: None,
|
||||
page_title: "Two-Factor Auth",
|
||||
app_name: APP_NAME,
|
||||
redirect_uri: &query.redirect,
|
||||
},
|
||||
factor,
|
||||
}.render().unwrap())
|
||||
HttpResponse::Ok().body(
|
||||
LoginWithOTPTemplate {
|
||||
_p: BaseLoginPage {
|
||||
danger,
|
||||
success: None,
|
||||
page_title: "Two-Factor Auth",
|
||||
app_name: APP_NAME,
|
||||
redirect_uri: &query.redirect,
|
||||
},
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct LoginWithWebauthnQuery {
|
||||
#[serde(default)]
|
||||
redirect: LoginRedirect,
|
||||
id: FactorID,
|
||||
}
|
||||
|
||||
|
||||
/// Login with Webauthn
|
||||
pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWithWebauthnQuery>,
|
||||
manager: WebAuthManagerReq,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
pub async fn login_with_webauthn(
|
||||
id: Option<Identity>,
|
||||
query: web::Query<LoginWithWebauthnQuery>,
|
||||
manager: WebAuthManagerReq,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
if !SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||
return redirect_user_for_login(query.redirect.get());
|
||||
}
|
||||
|
||||
let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(id.as_ref()).user_id()))
|
||||
.await.unwrap().0.expect("Could not find user!");
|
||||
let user: User = users
|
||||
.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) {
|
||||
Some(f) => f,
|
||||
None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!"))
|
||||
};
|
||||
let pub_keys = user.get_webauthn_pub_keys();
|
||||
if pub_keys.is_empty() {
|
||||
return HttpResponse::Ok()
|
||||
.body(build_fatal_error_page("No Webauthn public key registered!"));
|
||||
}
|
||||
|
||||
let key = match &factor.kind {
|
||||
TwoFactorType::WEBAUTHN(key) => key,
|
||||
_ => {
|
||||
return HttpResponse::Ok()
|
||||
.body(build_fatal_error_page("Factor is not a Webauthn key!"));
|
||||
}
|
||||
};
|
||||
|
||||
let challenge = match manager.start_authentication(&user.uid, key) {
|
||||
let challenge = match manager.start_authentication(&user.uid, &pub_keys) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!("Failed to generate webauthn challenge! {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.body(build_fatal_error_page("Failed to generate webauthn challenge"));
|
||||
return HttpResponse::InternalServerError().body(build_fatal_error_page(
|
||||
"Failed to generate webauthn challenge",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@ -390,16 +424,19 @@ pub async fn login_with_webauthn(id: Option<Identity>, query: web::Query<LoginWi
|
||||
}
|
||||
};
|
||||
|
||||
HttpResponse::Ok().body(LoginWithWebauthnTemplate {
|
||||
_p: BaseLoginPage {
|
||||
danger: None,
|
||||
success: None,
|
||||
page_title: "Two-Factor Auth",
|
||||
app_name: APP_NAME,
|
||||
redirect_uri: &query.redirect,
|
||||
},
|
||||
factor,
|
||||
opaque_state: challenge.opaque_state,
|
||||
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
||||
}.render().unwrap())
|
||||
}
|
||||
HttpResponse::Ok().body(
|
||||
LoginWithWebauthnTemplate {
|
||||
_p: BaseLoginPage {
|
||||
danger: None,
|
||||
success: None,
|
||||
page_title: "Two-Factor Auth",
|
||||
app_name: APP_NAME,
|
||||
redirect_uri: &query.redirect,
|
||||
},
|
||||
opaque_state: challenge.opaque_state,
|
||||
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
pub mod admin_api;
|
||||
pub mod admin_controller;
|
||||
pub mod assets_controller;
|
||||
pub mod base_controller;
|
||||
pub mod login_controller;
|
||||
pub mod login_api;
|
||||
pub mod settings_controller;
|
||||
pub mod admin_controller;
|
||||
pub mod admin_api;
|
||||
pub mod login_controller;
|
||||
pub mod openid_controller;
|
||||
pub mod settings_controller;
|
||||
pub mod two_factor_api;
|
||||
pub mod two_factors_controller;
|
||||
pub mod two_factor_api;
|
@ -2,12 +2,12 @@ use std::fmt::Debug;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
||||
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::users_actor::UsersActor;
|
||||
use crate::actors::{openid_sessions_actor, users_actor};
|
||||
use crate::constants::*;
|
||||
use crate::controllers::base_controller::build_fatal_error_page;
|
||||
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::current_user::CurrentUser;
|
||||
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::openid_config::OpenIDConfig;
|
||||
use crate::data::session_identity::SessionIdentity;
|
||||
@ -24,7 +24,9 @@ use crate::utils::string_utils::rand_str;
|
||||
use crate::utils::time::time;
|
||||
|
||||
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"))
|
||||
.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(),
|
||||
};
|
||||
|
||||
let curr_origin = format!("{}://{}", match is_secure_request {
|
||||
true => "https",
|
||||
false => "http"
|
||||
}, host);
|
||||
let curr_origin = format!(
|
||||
"{}://{}",
|
||||
match is_secure_request {
|
||||
true => "https",
|
||||
false => "http",
|
||||
},
|
||||
host
|
||||
);
|
||||
|
||||
HttpResponse::Ok().json(OpenIDConfig {
|
||||
issuer: app_conf.website_origin.clone(),
|
||||
@ -80,35 +86,43 @@ pub struct AuthorizeQuery {
|
||||
}
|
||||
|
||||
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()
|
||||
.append_header(
|
||||
("Location", format!(
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}?error={}?error_description={}&state={}",
|
||||
query.redirect_uri,
|
||||
urlencoding::encode(error),
|
||||
urlencoding::encode(description),
|
||||
urlencoding::encode(&query.state)
|
||||
))
|
||||
)
|
||||
),
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<AuthorizeQuery>,
|
||||
clients: web::Data<ClientManager>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> impl Responder {
|
||||
pub async fn authorize(
|
||||
user: CurrentUser,
|
||||
id: Identity,
|
||||
query: web::Query<AuthorizeQuery>,
|
||||
clients: web::Data<ClientManager>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
) -> impl Responder {
|
||||
let client = match clients.find_by_id(&query.client_id) {
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.body(build_fatal_error_page("Client is invalid!"));
|
||||
return HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!"));
|
||||
}
|
||||
Some(c) => c
|
||||
Some(c) => c,
|
||||
};
|
||||
|
||||
let redirect_uri = query.redirect_uri.trim().to_string();
|
||||
if !redirect_uri.starts_with(&client.redirect_uri) {
|
||||
return HttpResponse::BadRequest()
|
||||
.body(build_fatal_error_page("Redirect URI is invalid!"));
|
||||
return HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!"));
|
||||
}
|
||||
|
||||
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") {
|
||||
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() {
|
||||
@ -127,18 +145,27 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
|
||||
Some(chal) => {
|
||||
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
|
||||
if !meth.eq("S256") && !meth.eq("plain") {
|
||||
return error_redirect(&query, "invalid_request",
|
||||
"Only S256 and plain code challenge methods are supported!");
|
||||
return error_redirect(
|
||||
&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
|
||||
if !user.can_access_app(&client.id) {
|
||||
return error_redirect(&query, "invalid_request",
|
||||
"User is not authorized to access this application!");
|
||||
return error_redirect(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"User is not authorized to access this application!",
|
||||
);
|
||||
}
|
||||
|
||||
// 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,
|
||||
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);
|
||||
|
||||
HttpResponse::Found()
|
||||
.append_header(("Location", format!(
|
||||
"{}?state={}&session_state={}&code={}",
|
||||
session.redirect_uri,
|
||||
urlencoding::encode(&query.0.state),
|
||||
urlencoding::encode(&session.session_id.0),
|
||||
urlencoding::encode(&session.authorization_code)
|
||||
))).finish()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}?state={}&session_state={}&code={}",
|
||||
session.redirect_uri,
|
||||
urlencoding::encode(&query.0.state),
|
||||
urlencoding::encode(&session.session_id.0),
|
||||
urlencoding::encode(&session.authorization_code)
|
||||
),
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@ -178,12 +212,16 @@ struct ErrorResponse {
|
||||
}
|
||||
|
||||
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
|
||||
log::warn!("request failed: {} - {} => '{:#?}'", error, description, query);
|
||||
HttpResponse::BadRequest()
|
||||
.json(ErrorResponse {
|
||||
error: error.to_string(),
|
||||
error_description: description.to_string(),
|
||||
})
|
||||
log::warn!(
|
||||
"request failed: {} - {} => '{:#?}'",
|
||||
error,
|
||||
description,
|
||||
query
|
||||
);
|
||||
HttpResponse::BadRequest().json(ErrorResponse {
|
||||
error: error.to_string(),
|
||||
error_description: description.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
@ -198,7 +236,6 @@ pub struct TokenRefreshTokenQuery {
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TokenQuery {
|
||||
grant_type: String,
|
||||
@ -222,115 +259,175 @@ pub struct TokenResponse {
|
||||
id_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn token(req: HttpRequest,
|
||||
query: web::Form<TokenQuery>,
|
||||
clients: web::Data<ClientManager>,
|
||||
app_config: web::Data<AppConfig>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> {
|
||||
|
||||
pub async fn token(
|
||||
req: HttpRequest,
|
||||
query: web::Form<TokenQuery>,
|
||||
clients: web::Data<ClientManager>,
|
||||
app_config: web::Data<AppConfig>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
jwt_signer: web::Data<JWTSigner>,
|
||||
) -> actix_web::Result<HttpResponse> {
|
||||
// Extraction authentication information
|
||||
let authorization_header = req.headers().get("authorization");
|
||||
let (client_id, client_secret) = match (&query.client_id, &query.client_secret, authorization_header) {
|
||||
// post authentication
|
||||
(Some(client_id), Some(client_secret), None) => {
|
||||
(client_id.clone(), client_secret.to_string())
|
||||
}
|
||||
|
||||
// Basic authentication
|
||||
(None, None, Some(v)) => {
|
||||
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
|
||||
None => {
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
|
||||
));
|
||||
}
|
||||
Some(v) => v
|
||||
};
|
||||
|
||||
let decode = String::from_utf8_lossy(&match base64::decode(token) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::error!("Failed to decode authorization header: {:?}", e);
|
||||
return Ok(error_response(&query, "invalid_request", "Failed to decode authorization header!"));
|
||||
}
|
||||
}).to_string();
|
||||
|
||||
match decode.split_once(':') {
|
||||
None => (ClientID(decode), "".to_string()),
|
||||
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string())
|
||||
let (client_id, client_secret) =
|
||||
match (&query.client_id, &query.client_secret, authorization_header) {
|
||||
// post authentication
|
||||
(Some(client_id), Some(client_secret), None) => {
|
||||
(client_id.clone(), client_secret.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(error_response(&query, "invalid_request", "Authentication method unknown!"));
|
||||
}
|
||||
};
|
||||
// Basic authentication
|
||||
(None, None, Some(v)) => {
|
||||
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
|
||||
None => {
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
&format!(
|
||||
"Authorization header does not start with 'Basic ', got '{:#?}'",
|
||||
v
|
||||
),
|
||||
));
|
||||
}
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
let decode = String::from_utf8_lossy(&match base64::decode(token) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::error!("Failed to decode authorization header: {:?}", e);
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Failed to decode authorization header!",
|
||||
));
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
|
||||
match decode.split_once(':') {
|
||||
None => (ClientID(decode), "".to_string()),
|
||||
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Authentication method unknown!",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let client = clients
|
||||
.find_by_id(&client_id)
|
||||
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
|
||||
|
||||
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(),
|
||||
&query.authorization_code_query,
|
||||
&query.refresh_token_query) {
|
||||
let token_response = match (
|
||||
query.grant_type.as_str(),
|
||||
&query.authorization_code_query,
|
||||
&query.refresh_token_query,
|
||||
) {
|
||||
("authorization_code", Some(q), _) => {
|
||||
let mut session: Session = match sessions
|
||||
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
|
||||
.await.unwrap()
|
||||
.send(openid_sessions_actor::FindSessionByAuthorizationCode(
|
||||
q.code.clone(),
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
None => {
|
||||
return Ok(error_response(&query, "invalid_request", "Session not found!"));
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Session not found!",
|
||||
));
|
||||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
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 {
|
||||
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() {
|
||||
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
|
||||
if let Some(chall) = &session.code_challenge {
|
||||
let code_verifier = match &q.code_verifier {
|
||||
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) {
|
||||
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() {
|
||||
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() {
|
||||
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)?;
|
||||
|
||||
sessions.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||
.await.unwrap();
|
||||
sessions
|
||||
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user.clone()))
|
||||
.await.unwrap().0;
|
||||
let user: Option<User> = users
|
||||
.send(users_actor::GetUserRequest(session.user.clone()))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
let user = match user {
|
||||
None => return Ok(error_response(&query, "invalid_request", "User not found!")),
|
||||
Some(u) => u,
|
||||
@ -359,28 +456,44 @@ pub async fn token(req: HttpRequest,
|
||||
|
||||
("refresh_token", _, Some(q)) => {
|
||||
let mut session: Session = match sessions
|
||||
.send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone()))
|
||||
.await.unwrap()
|
||||
.send(openid_sessions_actor::FindSessionByRefreshToken(
|
||||
q.refresh_token.clone(),
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
None => {
|
||||
return Ok(error_response(&query, "invalid_request", "Session not found!"));
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Session not found!",
|
||||
));
|
||||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
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() {
|
||||
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)?;
|
||||
|
||||
sessions
|
||||
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
TokenResponse {
|
||||
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 {
|
||||
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 {
|
||||
HttpResponse::Unauthorized()
|
||||
.insert_header(("WWW-Authenticate", format!(
|
||||
"Bearer error=\"{}\", error_description=\"{}\"",
|
||||
err,
|
||||
description
|
||||
)))
|
||||
.insert_header((
|
||||
"WWW-Authenticate",
|
||||
format!(
|
||||
"Bearer error=\"{}\", error_description=\"{}\"",
|
||||
err, description
|
||||
),
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@ -426,37 +547,46 @@ pub struct UserInfoQuery {
|
||||
access_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn user_info_post(req: HttpRequest,
|
||||
form: Option<web::Form<UserInfoQuery>>,
|
||||
query: web::Query<UserInfoQuery>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
user_info(req,
|
||||
form
|
||||
.map(|f| f.0.access_token)
|
||||
.unwrap_or_default()
|
||||
.or(query.0.access_token),
|
||||
sessions,
|
||||
users,
|
||||
).await
|
||||
pub async fn user_info_post(
|
||||
req: HttpRequest,
|
||||
form: Option<web::Form<UserInfoQuery>>,
|
||||
query: web::Query<UserInfoQuery>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
user_info(
|
||||
req,
|
||||
form.map(|f| f.0.access_token)
|
||||
.unwrap_or_default()
|
||||
.or(query.0.access_token),
|
||||
sessions,
|
||||
users,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn user_info_get(req: HttpRequest, query: web::Query<UserInfoQuery>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
pub async fn user_info_get(
|
||||
req: HttpRequest,
|
||||
query: web::Query<UserInfoQuery>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
user_info(req, query.0.access_token, sessions, users).await
|
||||
}
|
||||
|
||||
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
|
||||
async fn user_info(req: HttpRequest, token: Option<String>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
async fn user_info(
|
||||
req: HttpRequest,
|
||||
token: Option<String>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
let token = match token {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
let token = match req.headers().get("Authorization") {
|
||||
None => return user_info_error("invalid_request", "Missing access token!"),
|
||||
Some(t) => t
|
||||
Some(t) => t,
|
||||
};
|
||||
|
||||
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 ") {
|
||||
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,
|
||||
};
|
||||
|
||||
@ -474,7 +609,9 @@ async fn user_info(req: HttpRequest, token: Option<String>,
|
||||
};
|
||||
|
||||
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 {
|
||||
None => {
|
||||
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!");
|
||||
}
|
||||
|
||||
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 {
|
||||
None => {
|
||||
return user_info_error("invalid_request", "Failed to extract user information!");
|
||||
@ -494,14 +635,13 @@ async fn user_info(req: HttpRequest, token: Option<String>,
|
||||
Some(u) => u,
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.json(OpenIDUserInfo {
|
||||
name: user.full_name(),
|
||||
sub: user.uid.0,
|
||||
given_name: user.first_name,
|
||||
family_name: user.last_name,
|
||||
preferred_username: user.username,
|
||||
email: user.email,
|
||||
email_verified: true,
|
||||
})
|
||||
}
|
||||
HttpResponse::Ok().json(OpenIDUserInfo {
|
||||
name: user.full_name(),
|
||||
sub: user.uid.0,
|
||||
given_name: user.first_name,
|
||||
family_name: user.last_name,
|
||||
preferred_username: user.username,
|
||||
email: user.email,
|
||||
email_verified: true,
|
||||
})
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
use actix::Addr;
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
|
||||
use crate::actors::{bruteforce_actor, users_actor};
|
||||
use crate::actors::bruteforce_actor::BruteForceActor;
|
||||
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::data::current_user::CurrentUser;
|
||||
use crate::data::remote_ip::RemoteIP;
|
||||
@ -21,8 +21,12 @@ pub(crate) struct BaseSettingsPage {
|
||||
}
|
||||
|
||||
impl BaseSettingsPage {
|
||||
pub fn get(page_title: &'static str, user: &User,
|
||||
danger_message: Option<String>, success_message: Option<String>) -> BaseSettingsPage {
|
||||
pub fn get(
|
||||
page_title: &'static str,
|
||||
user: &User,
|
||||
danger_message: Option<String>,
|
||||
success_message: Option<String>,
|
||||
) -> BaseSettingsPage {
|
||||
Self {
|
||||
danger_message,
|
||||
success_message,
|
||||
@ -52,11 +56,14 @@ struct ChangePasswordPage {
|
||||
/// Account details page
|
||||
pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder {
|
||||
let user = user.into();
|
||||
HttpResponse::Ok()
|
||||
.body(AccountDetailsPage {
|
||||
HttpResponse::Ok().body(
|
||||
AccountDetailsPage {
|
||||
_p: BaseSettingsPage::get("Account details", &user, None, None),
|
||||
u: user,
|
||||
}.render().unwrap())
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@ -66,53 +73,71 @@ pub struct PassChangeRequest {
|
||||
}
|
||||
|
||||
/// Change password route
|
||||
pub async fn change_password_route(user: CurrentUser,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
req: Option<web::Form<PassChangeRequest>>,
|
||||
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||
remote_ip: RemoteIP) -> impl Responder {
|
||||
pub async fn change_password_route(
|
||||
user: CurrentUser,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
req: Option<web::Form<PassChangeRequest>>,
|
||||
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||
remote_ip: RemoteIP,
|
||||
) -> impl Responder {
|
||||
let mut danger = None;
|
||||
let mut success = None;
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
||||
// Invalid password
|
||||
if !user.verify_password(&req.old_pass) {
|
||||
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
|
||||
else if req.new_pass.len() < MIN_PASS_LEN {
|
||||
danger = Some("New password is too short!".to_string());
|
||||
}
|
||||
|
||||
// Change password
|
||||
else {
|
||||
let res = users.send(
|
||||
users_actor::ChangePasswordRequest {
|
||||
let res = users
|
||||
.send(users_actor::ChangePasswordRequest {
|
||||
user_id: user.uid.clone(),
|
||||
new_password: req.new_pass.to_string(),
|
||||
temporary: false,
|
||||
}
|
||||
).await.unwrap().0;
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
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 {
|
||||
success = Some("Your password was successfully changed!".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.body(ChangePasswordPage {
|
||||
HttpResponse::Ok().body(
|
||||
ChangePasswordPage {
|
||||
_p: BaseSettingsPage::get("Change password", &user, danger, success),
|
||||
min_pwd_len: MIN_PASS_LEN,
|
||||
}.render().unwrap())
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use actix::Addr;
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::RegisterPublicKeyCredential;
|
||||
|
||||
@ -17,15 +17,19 @@ pub struct AddTOTPRequest {
|
||||
first_code: String,
|
||||
}
|
||||
|
||||
pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
pub async fn save_totp_factor(
|
||||
user: CurrentUser,
|
||||
form: web::Json<AddTOTPRequest>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
let key = TotpKey::from_encoded_secret(&form.secret);
|
||||
|
||||
if !key.check_code(&form.first_code).unwrap_or(false) {
|
||||
return HttpResponse::BadRequest()
|
||||
.body(format!("Given code is invalid (expected {} or {})!",
|
||||
key.current_code().unwrap_or_default(),
|
||||
key.previous_code().unwrap_or_default()));
|
||||
return HttpResponse::BadRequest().body(format!(
|
||||
"Given code is invalid (expected {} or {})!",
|
||||
key.current_code().unwrap_or_default(),
|
||||
key.previous_code().unwrap_or_default()
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
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 {
|
||||
HttpResponse::InternalServerError().body("Failed to update user information!")
|
||||
@ -54,14 +62,13 @@ pub struct AddWebauthnRequest {
|
||||
credential: RegisterPublicKeyCredential,
|
||||
}
|
||||
|
||||
pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json<AddWebauthnRequest>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
manager: WebAuthManagerReq) -> impl Responder {
|
||||
let key = match manager.finish_registration(
|
||||
&user,
|
||||
&form.0.opaque_state,
|
||||
form.0.credential,
|
||||
) {
|
||||
pub async fn save_webauthn_factor(
|
||||
user: CurrentUser,
|
||||
form: web::Json<AddWebauthnRequest>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
manager: WebAuthManagerReq,
|
||||
) -> impl Responder {
|
||||
let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) {
|
||||
Ok(k) => k,
|
||||
Err(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,
|
||||
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 {
|
||||
HttpResponse::InternalServerError().body("Failed to update user information!")
|
||||
@ -89,16 +100,23 @@ pub struct DeleteFactorRequest {
|
||||
id: FactorID,
|
||||
}
|
||||
|
||||
pub async fn delete_factor(user: CurrentUser, form: web::Json<DeleteFactorRequest>,
|
||||
users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||
pub async fn delete_factor(
|
||||
user: CurrentUser,
|
||||
form: web::Json<DeleteFactorRequest>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
) -> impl Responder {
|
||||
let mut user = User::from(user);
|
||||
user.remove_factor(form.0.id);
|
||||
|
||||
let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0;
|
||||
let res = users
|
||||
.send(users_actor::UpdateUserRequest(user))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
if !res {
|
||||
HttpResponse::InternalServerError().body("Failed to update user information!")
|
||||
} else {
|
||||
HttpResponse::Ok().body("Removed factor!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
use qrcode_generator::QrCodeEcc;
|
||||
|
||||
@ -37,27 +37,25 @@ struct AddWebauhtnPage {
|
||||
|
||||
/// Manage two factors authentication methods route
|
||||
pub async fn two_factors_route(user: CurrentUser) -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.body(TwoFactorsPage {
|
||||
_p: BaseSettingsPage::get(
|
||||
"Two factor auth",
|
||||
&user,
|
||||
None,
|
||||
None),
|
||||
HttpResponse::Ok().body(
|
||||
TwoFactorsPage {
|
||||
_p: BaseSettingsPage::get("Two factor auth", &user, None, None),
|
||||
user: user.deref(),
|
||||
}.render().unwrap())
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// 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 qr_code = qrcode_generator::to_png_to_vec(
|
||||
key.url_for_user(&user, &app_conf),
|
||||
QrCodeEcc::Low,
|
||||
1024,
|
||||
);
|
||||
let qr_code =
|
||||
qrcode_generator::to_png_to_vec(key.url_for_user(&user, &app_conf), QrCodeEcc::Low, 1024);
|
||||
let qr_code = match qr_code {
|
||||
Ok(q) => q,
|
||||
Err(e) => {
|
||||
@ -66,26 +64,29 @@ pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppCon
|
||||
}
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.body(AddTotpPage {
|
||||
_p: BaseSettingsPage::get(
|
||||
"New authenticator app",
|
||||
&user,
|
||||
None,
|
||||
None),
|
||||
HttpResponse::Ok().body(
|
||||
AddTotpPage {
|
||||
_p: BaseSettingsPage::get("New authenticator app", &user, None, None),
|
||||
qr_code: base64::encode(qr_code),
|
||||
account_name: key.account_name(&user, &app_conf),
|
||||
secret_key: key.get_secret(),
|
||||
}.render().unwrap())
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
Ok(r) => r,
|
||||
Err(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()
|
||||
.body(AddWebauhtnPage {
|
||||
_p: BaseSettingsPage::get(
|
||||
"New security key",
|
||||
&user,
|
||||
None,
|
||||
None),
|
||||
HttpResponse::Ok().body(
|
||||
AddWebauhtnPage {
|
||||
_p: BaseSettingsPage::get("New security key", &user, None, None),
|
||||
|
||||
opaque_state: registration_request.opaque_state,
|
||||
challenge_json: urlencoding::encode(&challenge_json).to_string(),
|
||||
}.render().unwrap())
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ impl AccessToken {
|
||||
jwt_id: None,
|
||||
nonce: self.nonce,
|
||||
custom: CustomAccessTokenClaims {
|
||||
rand_val: self.rand_val
|
||||
rand_val: self.rand_val,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,4 +42,4 @@ impl EntityManager<Client> {
|
||||
c.redirect_uri = apply_env_vars(&c.redirect_uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,8 @@ impl CodeChallenge {
|
||||
match self.code_challenge_method.as_str() {
|
||||
"plain" => code_verifer.eq(&self.code_challenge),
|
||||
"S256" => {
|
||||
let encoded = base64::encode_config(
|
||||
sha256(code_verifer.as_bytes()),
|
||||
URL_SAFE_NO_PAD,
|
||||
);
|
||||
let encoded =
|
||||
base64::encode_config(sha256(code_verifer.as_bytes()), URL_SAFE_NO_PAD);
|
||||
|
||||
encoded.eq(&self.code_challenge)
|
||||
}
|
||||
@ -64,7 +62,10 @@ mod test {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
||||
use aes_gcm::aead::{Aead, OsRng};
|
||||
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
||||
use rand::Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
@ -17,7 +17,9 @@ pub struct CryptoWrapper {
|
||||
impl CryptoWrapper {
|
||||
/// Generate a new memory wrapper
|
||||
pub fn new_random() -> Self {
|
||||
Self { key: Aes256Gcm::generate_key(&mut OsRng) }
|
||||
Self {
|
||||
key: Aes256Gcm::generate_key(&mut OsRng),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt some data
|
||||
@ -27,11 +29,11 @@ impl CryptoWrapper {
|
||||
|
||||
let serialized_data = bincode::serialize(data)?;
|
||||
|
||||
let mut enc = aes_key.encrypt(Nonce::from_slice(&nonce_bytes),
|
||||
serialized_data.as_slice()).unwrap();
|
||||
let mut enc = aes_key
|
||||
.encrypt(Nonce::from_slice(&nonce_bytes), serialized_data.as_slice())
|
||||
.unwrap();
|
||||
enc.extend_from_slice(&nonce_bytes);
|
||||
|
||||
|
||||
Ok(base64::encode(enc))
|
||||
}
|
||||
|
||||
@ -40,8 +42,10 @@ impl CryptoWrapper {
|
||||
let bytes = base64::decode(input)?;
|
||||
|
||||
if bytes.len() < NONCE_LEN {
|
||||
return Err(Box::new(std::io::Error::new(ErrorKind::Other,
|
||||
"Input string is smaller than nonce!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Input string is smaller than nonce!",
|
||||
)));
|
||||
}
|
||||
|
||||
let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN);
|
||||
@ -53,8 +57,10 @@ impl CryptoWrapper {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::error!("Failed to decrypt wrapped data! {:#?}", e);
|
||||
return Err(Box::new(std::io::Error::new(ErrorKind::Other,
|
||||
"Failed to decrypt wrapped data!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Failed to decrypt wrapped data!",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@ -87,4 +93,4 @@ mod test {
|
||||
let enc = wrapper_1.encrypt(&msg).unwrap();
|
||||
wrapper_2.decrypt::<Message>(&enc).unwrap_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ use std::pin::Pin;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{Error, FromRequest, HttpRequest, web};
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::error::ErrorInternalServerError;
|
||||
use actix_web::{web, Error, FromRequest, HttpRequest};
|
||||
|
||||
use crate::actors::users_actor;
|
||||
use crate::actors::users_actor::UsersActor;
|
||||
@ -31,27 +31,33 @@ impl Deref for CurrentUser {
|
||||
|
||||
impl FromRequest for CurrentUser {
|
||||
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 {
|
||||
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 identity: Identity = Identity::from_request(req, payload).into_inner()
|
||||
let identity: Identity = Identity::from_request(req, payload)
|
||||
.into_inner()
|
||||
.expect("Failed to get identity!");
|
||||
let user_id = SessionIdentity(Some(&identity)).user_id();
|
||||
|
||||
|
||||
Box::pin(async move {
|
||||
let user = match user_actor.send(
|
||||
users_actor::GetUserRequest(user_id)
|
||||
).await.unwrap().0 {
|
||||
let user = match user_actor
|
||||
.send(users_actor::GetUserRequest(user_id))
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
{
|
||||
Some(u) => u,
|
||||
None => {
|
||||
return Err(ErrorInternalServerError("Could not extract user information!"));
|
||||
return Err(ErrorInternalServerError(
|
||||
"Could not extract user information!",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CurrentUser(user))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ use std::slice::{Iter, IterMut};
|
||||
|
||||
use crate::utils::err::Res;
|
||||
|
||||
enum FileFormat { Json, Yaml }
|
||||
enum FileFormat {
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
pub struct EntityManager<E> {
|
||||
file_path: PathBuf,
|
||||
@ -11,8 +14,8 @@ pub struct EntityManager<E> {
|
||||
}
|
||||
|
||||
impl<E> EntityManager<E>
|
||||
where
|
||||
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
|
||||
where
|
||||
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
|
||||
{
|
||||
/// Open entity
|
||||
pub fn open_or_create<A: AsRef<Path>>(path: A) -> Res<Self> {
|
||||
@ -30,7 +33,7 @@ impl<E> EntityManager<E>
|
||||
file_path: path.as_ref().to_path_buf(),
|
||||
list: match Self::file_format(path.as_ref()) {
|
||||
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 {
|
||||
match p.to_string_lossy().ends_with(".json") {
|
||||
true => FileFormat::Json,
|
||||
false => FileFormat::Yaml
|
||||
false => FileFormat::Yaml,
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,8 +73,8 @@ impl<E> EntityManager<E>
|
||||
|
||||
/// Replace entries in the list that matches a criteria
|
||||
pub fn replace_entries<F>(&mut self, filter: F, el: &E) -> Res
|
||||
where
|
||||
F: Fn(&E) -> bool,
|
||||
where
|
||||
F: Fn(&E) -> bool,
|
||||
{
|
||||
for i in 0..self.list.len() {
|
||||
if filter(&self.list[i]) {
|
||||
|
@ -49,4 +49,4 @@ impl IdToken {
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,9 @@ pub struct JWTSigner(RS256KeyPair);
|
||||
|
||||
impl JWTSigner {
|
||||
pub fn gen_from_memory() -> Res<Self> {
|
||||
Ok(Self(RS256KeyPair::generate(2048)?
|
||||
.with_key_id(&format!("key-{}", rand_str(15)))))
|
||||
Ok(Self(
|
||||
RS256KeyPair::generate(2048)?.with_key_id(&format!("key-{}", rand_str(15))),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_json_web_key(&self) -> JsonWebKey {
|
||||
@ -45,4 +46,4 @@ impl JWTSigner {
|
||||
pub fn sign_token<E: Serialize + DeserializeOwned>(&self, c: JWTClaims<E>) -> Res<String> {
|
||||
Ok(self.0.sign(c)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,4 +18,4 @@ impl Default for LoginRedirect {
|
||||
fn default() -> Self {
|
||||
Self("/".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 totp_key;
|
||||
pub mod app_config;
|
||||
pub mod client;
|
||||
pub mod code_challenge;
|
||||
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;
|
||||
pub mod crypto_wrapper;
|
@ -21,4 +21,4 @@ pub struct OpenIDUserInfo {
|
||||
|
||||
/// True if the End-User's e-mail address has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this e-mail address was controlled by the End-User at the time the verification was performed. The means by which an e-mail address is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating.
|
||||
pub email_verified: bool,
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,4 @@ pub struct OpenIDConfig {
|
||||
pub claims_supported: Vec<&'static str>,
|
||||
|
||||
pub code_challenge_methods_supported: Vec<&'static str>,
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use actix_web::{Error, FromRequest, HttpRequest, web};
|
||||
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::utils::network_utils::get_remote_ip;
|
||||
@ -25,4 +25,4 @@ impl FromRequest for RemoteIP {
|
||||
let config: &web::Data<AppConfig> = req.app_data().expect("AppData undefined!");
|
||||
ready(Ok(RemoteIP(get_remote_ip(req, config.proxy_ip.as_deref()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,7 @@ impl<'a> SessionIdentity<'a> {
|
||||
fn get_session_data(&self) -> Option<SessionIdentityData> {
|
||||
if let Some(id) = self.0 {
|
||||
Self::deserialize_session_data(id.id().ok())
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@ -71,12 +70,15 @@ impl<'a> SessionIdentity<'a> {
|
||||
}
|
||||
|
||||
pub fn set_user(&self, req: &HttpRequest, user: &User, status: SessionStatus) {
|
||||
self.set_session_data(req, &SessionIdentityData {
|
||||
id: Some(user.uid.clone()),
|
||||
is_admin: user.admin,
|
||||
auth_time: time(),
|
||||
status,
|
||||
});
|
||||
self.set_session_data(
|
||||
req,
|
||||
&SessionIdentityData {
|
||||
id: Some(user.uid.clone()),
|
||||
is_admin: user.admin,
|
||||
auth_time: time(),
|
||||
status,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_status(&self, req: &HttpRequest, status: SessionStatus) {
|
||||
@ -108,7 +110,9 @@ impl<'a> SessionIdentity<'a> {
|
||||
}
|
||||
|
||||
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!")
|
||||
}
|
||||
|
||||
|
@ -23,13 +23,15 @@ impl TotpKey {
|
||||
pub fn new_random() -> Self {
|
||||
let random_bytes = rand::thread_rng().gen::<[u8; 10]>();
|
||||
Self {
|
||||
encoded: base32::encode(BASE32_ALPHABET, &random_bytes)
|
||||
encoded: base32::encode(BASE32_ALPHABET, &random_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a key from an encoded secret
|
||||
pub fn from_encoded_secret(s: &str) -> Self {
|
||||
Self { encoded: s.to_string() }
|
||||
Self {
|
||||
encoded: s.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get QrCode URL for user
|
||||
@ -74,15 +76,19 @@ impl TotpKey {
|
||||
/// Get the code at a specific time
|
||||
fn get_code_at<F: Fn() -> u64>(&self, get_time: F) -> Res<String> {
|
||||
let gen = TotpGenerator::new()
|
||||
.set_digit(NUM_DIGITS).unwrap()
|
||||
.set_step(PERIOD).unwrap()
|
||||
.set_digit(NUM_DIGITS)
|
||||
.unwrap()
|
||||
.set_step(PERIOD)
|
||||
.unwrap()
|
||||
.set_hash_algorithm(HashAlgorithm::SHA1)
|
||||
.build();
|
||||
|
||||
let key = match base32::decode(BASE32_ALPHABET, &self.encoded) {
|
||||
None => {
|
||||
return Err(Box::new(
|
||||
std::io::Error::new(ErrorKind::Other, "Failed to decode base32 secret!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Failed to decode base32 secret!",
|
||||
)));
|
||||
}
|
||||
Some(k) => k,
|
||||
};
|
||||
@ -113,4 +119,4 @@ mod test {
|
||||
let key = TotpKey::from_encoded_secret("JBSWY3DPEHPK3PXP");
|
||||
assert_eq!("124851", key.get_code_at(|| 1650470683).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
match self.kind {
|
||||
TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}",
|
||||
self.id.0, redirect_uri.get_encoded()),
|
||||
TwoFactorType::WEBAUTHN(_) => format!("/2fa_webauthn?id={}&redirect={}",
|
||||
self.id.0, redirect_uri.get_encoded()),
|
||||
TwoFactorType::TOTP(_) => format!("/2fa_otp?redirect={}", redirect_uri.get_encoded()),
|
||||
TwoFactorType::WEBAUTHN(_) => {
|
||||
format!("/2fa_webauthn?redirect={}", redirect_uri.get_encoded())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_webauthn(&self) -> bool {
|
||||
matches!(self.kind, TwoFactorType::WEBAUTHN(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
@ -71,7 +89,7 @@ impl User {
|
||||
pub fn can_access_app(&self, id: &ClientID) -> bool {
|
||||
match &self.authorized_clients {
|
||||
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> {
|
||||
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 {
|
||||
@ -157,8 +218,8 @@ impl EntityManager<User> {
|
||||
|
||||
/// Update user information
|
||||
fn update_user<F>(&mut self, id: &UserID, update: F) -> bool
|
||||
where
|
||||
F: FnOnce(User) -> User,
|
||||
where
|
||||
F: FnOnce(User) -> User,
|
||||
{
|
||||
let user = match self.find_by_user_id(id) {
|
||||
None => return false,
|
||||
|
@ -3,10 +3,15 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::{
|
||||
CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||
RequestChallengeResponse,
|
||||
};
|
||||
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::crypto_wrapper::CryptoWrapper;
|
||||
use crate::data::user::{User, UserID};
|
||||
@ -42,7 +47,6 @@ struct AuthStateOpaqueData {
|
||||
expire: u64,
|
||||
}
|
||||
|
||||
|
||||
pub type WebAuthManagerReq = web::Data<Arc<WebAuthManager>>;
|
||||
|
||||
pub struct WebAuthManager {
|
||||
@ -54,24 +58,23 @@ impl WebAuthManager {
|
||||
pub fn init(conf: &AppConfig) -> Self {
|
||||
Self {
|
||||
core: WebauthnBuilder::new(
|
||||
conf.domain_name().split_once(':')
|
||||
conf.domain_name()
|
||||
.split_once(':')
|
||||
.map(|s| s.0)
|
||||
.unwrap_or_else(|| conf.domain_name()),
|
||||
&url::Url::parse(&conf.website_origin)
|
||||
.expect("Failed to parse configuration origin!"))
|
||||
.expect("Invalid Webauthn configuration")
|
||||
.rp_name(APP_NAME)
|
||||
.build()
|
||||
.expect("Failed to build webauthn")
|
||||
|
||||
,
|
||||
.expect("Failed to parse configuration origin!"),
|
||||
)
|
||||
.expect("Invalid Webauthn configuration")
|
||||
.rp_name(APP_NAME)
|
||||
.build()
|
||||
.expect("Failed to build webauthn"),
|
||||
crypto_wrapper: CryptoWrapper::new_random(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_register(&self, user: &User) -> Res<RegisterKeyRequest> {
|
||||
let (creation_challenge, registration_state)
|
||||
= self.core.start_passkey_registration(
|
||||
let (creation_challenge, registration_state) = self.core.start_passkey_registration(
|
||||
Uuid::parse_str(&user.uid.0).expect("Failed to parse user id"),
|
||||
&user.username,
|
||||
&user.full_name(),
|
||||
@ -88,29 +91,43 @@ impl WebAuthManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finish_registration(&self, user: &User, opaque_state: &str,
|
||||
pub_cred: RegisterPublicKeyCredential) -> Res<WebauthnPubKey> {
|
||||
pub fn finish_registration(
|
||||
&self,
|
||||
user: &User,
|
||||
opaque_state: &str,
|
||||
pub_cred: RegisterPublicKeyCredential,
|
||||
) -> Res<WebauthnPubKey> {
|
||||
let state: RegisterKeyOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
|
||||
if state.user_id != user.uid {
|
||||
return Err(Box::new(
|
||||
std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Invalid user for pubkey!",
|
||||
)));
|
||||
}
|
||||
|
||||
if state.expire < time() {
|
||||
return Err(Box::new(
|
||||
std::io::Error::new(ErrorKind::Other, "Challenge has expired!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Challenge has expired!",
|
||||
)));
|
||||
}
|
||||
|
||||
let res = self.core
|
||||
.finish_passkey_registration(&pub_cred, &serde_json::from_str(&state.registration_state)?)?;
|
||||
let res = self.core.finish_passkey_registration(
|
||||
&pub_cred,
|
||||
&serde_json::from_str(&state.registration_state)?,
|
||||
)?;
|
||||
|
||||
Ok(WebauthnPubKey { creds: res })
|
||||
}
|
||||
|
||||
pub fn start_authentication(&self, user_id: &UserID, key: &WebauthnPubKey) -> Res<AuthRequest> {
|
||||
let (login_challenge, authentication_state) = self.core.start_passkey_authentication(&vec![
|
||||
key.creds.clone()
|
||||
])?;
|
||||
pub fn start_authentication(
|
||||
&self,
|
||||
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 {
|
||||
opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData {
|
||||
@ -122,22 +139,32 @@ impl WebAuthManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finish_authentication(&self, user_id: &UserID, opaque_state: &str,
|
||||
pub_cred: &PublicKeyCredential) -> Res {
|
||||
pub fn finish_authentication(
|
||||
&self,
|
||||
user_id: &UserID,
|
||||
opaque_state: &str,
|
||||
pub_cred: &PublicKeyCredential,
|
||||
) -> Res {
|
||||
let state: AuthStateOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?;
|
||||
if &state.user_id != user_id {
|
||||
return Err(Box::new(
|
||||
std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Invalid user for pubkey!",
|
||||
)));
|
||||
}
|
||||
|
||||
if state.expire < time() {
|
||||
return Err(Box::new(
|
||||
std::io::Error::new(ErrorKind::Other, "Challenge has expired!")));
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Challenge has expired!",
|
||||
)));
|
||||
}
|
||||
|
||||
self.core.finish_passkey_authentication(pub_cred,
|
||||
&serde_json::from_str(&state.authentication_state)?)?;
|
||||
self.core.finish_passkey_authentication(
|
||||
pub_cred,
|
||||
&serde_json::from_str(&state.authentication_state)?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
181
src/main.rs
181
src/main.rs
@ -4,19 +4,19 @@ use std::sync::Arc;
|
||||
use actix::Actor;
|
||||
use actix_identity::config::LogoutBehaviour;
|
||||
use actix_identity::IdentityMiddleware;
|
||||
use actix_session::SessionMiddleware;
|
||||
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::middleware::Logger;
|
||||
use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
|
||||
use clap::Parser;
|
||||
|
||||
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
|
||||
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
|
||||
use basic_oidc::actors::users_actor::UsersActor;
|
||||
use basic_oidc::constants::*;
|
||||
use basic_oidc::controllers::*;
|
||||
use basic_oidc::controllers::assets_controller::assets_route;
|
||||
use basic_oidc::controllers::*;
|
||||
use basic_oidc::data::app_config::AppConfig;
|
||||
use basic_oidc::data::client::ClientManager;
|
||||
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 bruteforce_actor = BruteForceActor::default().start();
|
||||
let openid_sessions_actor = OpenIDSessionsActor::default().start();
|
||||
let jwt_signer = JWTSigner::gen_from_memory()
|
||||
.expect("Failed to generate JWKS key");
|
||||
let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
|
||||
let webauthn_manager = Arc::new(WebAuthManager::init(&config));
|
||||
|
||||
log::info!("Server will listen on {}", config.listen_address);
|
||||
@ -84,13 +83,14 @@ async fn main() -> std::io::Result<()> {
|
||||
.expect("Failed to load clients list!");
|
||||
clients.apply_environment_variables();
|
||||
|
||||
let session_mw =
|
||||
SessionMiddleware::builder(CookieSessionStore::default(),
|
||||
Key::from(config.token_key.as_bytes()))
|
||||
.cookie_name(SESSION_COOKIE_NAME.to_string())
|
||||
.cookie_secure(config.secure_cookie())
|
||||
.cookie_same_site(SameSite::Lax)
|
||||
.build();
|
||||
let session_mw = SessionMiddleware::builder(
|
||||
CookieSessionStore::default(),
|
||||
Key::from(config.token_key.as_bytes()),
|
||||
)
|
||||
.cookie_name(SESSION_COOKIE_NAME.to_string())
|
||||
.cookie_secure(config.secure_cookie())
|
||||
.cookie_same_site(SameSite::Lax)
|
||||
.build();
|
||||
|
||||
let identity_middleware = IdentityMiddleware::builder()
|
||||
.logout_behaviour(LogoutBehaviour::PurgeSession)
|
||||
@ -106,74 +106,145 @@ async fn main() -> std::io::Result<()> {
|
||||
.app_data(web::Data::new(clients))
|
||||
.app_data(web::Data::new(jwt_signer.clone()))
|
||||
.app_data(web::Data::new(webauthn_manager.clone()))
|
||||
|
||||
.wrap(middleware::DefaultHeaders::new()
|
||||
.add(("Permissions-Policy", "interest-cohort=()")))
|
||||
.wrap(
|
||||
middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")),
|
||||
)
|
||||
.wrap(Logger::default())
|
||||
.wrap(AuthMiddleware {})
|
||||
.wrap(identity_middleware)
|
||||
.wrap(session_mw)
|
||||
|
||||
// main route
|
||||
.route("/", web::get()
|
||||
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
|
||||
.route(
|
||||
"/",
|
||||
web::get().to(|| async {
|
||||
HttpResponse::Found()
|
||||
.append_header(("Location", "/settings"))
|
||||
.finish()
|
||||
}),
|
||||
)
|
||||
.route("/robots.txt", web::get().to(assets_controller::robots_txt))
|
||||
|
||||
// health route
|
||||
.service(health)
|
||||
|
||||
// Assets serving
|
||||
.route("/assets/{path:.*}", web::get().to(assets_route))
|
||||
|
||||
// Login pages
|
||||
.route("/logout", web::get().to(login_controller::logout_route))
|
||||
.route("/login", web::get().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("/reset_password", web::post().to(login_controller::reset_password_route))
|
||||
.route("/2fa_auth", web::get().to(login_controller::choose_2fa_method))
|
||||
.route(
|
||||
"/reset_password",
|
||||
web::get().to(login_controller::reset_password_route),
|
||||
)
|
||||
.route(
|
||||
"/reset_password",
|
||||
web::post().to(login_controller::reset_password_route),
|
||||
)
|
||||
.route(
|
||||
"/2fa_auth",
|
||||
web::get().to(login_controller::choose_2fa_method),
|
||||
)
|
||||
.route("/2fa_otp", web::get().to(login_controller::login_with_otp))
|
||||
.route("/2fa_otp", web::post().to(login_controller::login_with_otp))
|
||||
.route("/2fa_webauthn", web::get().to(login_controller::login_with_webauthn))
|
||||
|
||||
.route(
|
||||
"/2fa_webauthn",
|
||||
web::get().to(login_controller::login_with_webauthn),
|
||||
)
|
||||
// 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
|
||||
.route("/settings", web::get().to(settings_controller::account_settings_details_route))
|
||||
.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))
|
||||
|
||||
.route(
|
||||
"/settings",
|
||||
web::get().to(settings_controller::account_settings_details_route),
|
||||
)
|
||||
.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
|
||||
.route("/settings/api/two_factor/save_totp_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))
|
||||
|
||||
.route(
|
||||
"/settings/api/two_factor/save_totp_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
|
||||
.route("/admin", web::get()
|
||||
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
|
||||
.route("/admin/clients", web::get().to(admin_controller::clients_route))
|
||||
.route(
|
||||
"/admin",
|
||||
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::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))
|
||||
|
||||
.route(
|
||||
"/admin/users",
|
||||
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
|
||||
.route("/admin/api/find_username", web::post().to(admin_api::find_username))
|
||||
.route("/admin/api/delete_user", web::post().to(admin_api::delete_user))
|
||||
|
||||
.route(
|
||||
"/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
|
||||
.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(TOKEN_URI, web::post().to(openid_controller::token))
|
||||
.route(CERT_URI, web::get().to(openid_controller::cert_uri))
|
||||
.route(USERINFO_URI, web::post().to(openid_controller::user_info_post))
|
||||
.route(USERINFO_URI, web::get().to(openid_controller::user_info_get))
|
||||
.route(
|
||||
USERINFO_URI,
|
||||
web::post().to(openid_controller::user_info_post),
|
||||
)
|
||||
.route(
|
||||
USERINFO_URI,
|
||||
web::get().to(openid_controller::user_info_get),
|
||||
)
|
||||
})
|
||||
.bind(listen_address)?
|
||||
.run()
|
||||
.await
|
||||
.bind(listen_address)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
//! # Authentication middleware
|
||||
|
||||
use std::future::{Future, ready, Ready};
|
||||
use std::future::{ready, Future, Ready};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
|
||||
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::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::data::app_config::AppConfig;
|
||||
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
|
||||
@ -27,10 +29,10 @@ pub struct AuthMiddleware;
|
||||
// `S` - type of the next service
|
||||
// `B` - type of response's body
|
||||
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
|
||||
where
|
||||
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Error = Error;
|
||||
@ -62,22 +64,21 @@ impl ConnStatus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct AuthInnerMiddleware<S> {
|
||||
service: Rc<S>,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response=ServiceResponse<B>, Error=Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Error = Error;
|
||||
|
||||
#[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);
|
||||
|
||||
@ -90,7 +91,8 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||
|
||||
// Check if POST request comes from another website (block invalid origins)
|
||||
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 !o.to_str().unwrap_or("bad").eq(&config.website_origin) {
|
||||
log::warn!(
|
||||
@ -118,14 +120,14 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||
let session_data = SessionIdentity::deserialize_session_data(id);
|
||||
let session = match session_data {
|
||||
Some(SessionIdentityData {
|
||||
status: SessionStatus::SignedIn,
|
||||
is_admin: true,
|
||||
..
|
||||
}) => ConnStatus::Admin,
|
||||
status: SessionStatus::SignedIn,
|
||||
is_admin: true,
|
||||
..
|
||||
}) => ConnStatus::Admin,
|
||||
Some(SessionIdentityData {
|
||||
status: SessionStatus::SignedIn,
|
||||
..
|
||||
}) => ConnStatus::RegularUser,
|
||||
status: SessionStatus::SignedIn,
|
||||
..
|
||||
}) => ConnStatus::RegularUser,
|
||||
_ => ConnStatus::SignedOut,
|
||||
};
|
||||
|
||||
@ -135,10 +137,13 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||
// Redirect user to login page
|
||||
if !session.is_auth()
|
||||
&& (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.",
|
||||
req.path());
|
||||
log::debug!(
|
||||
"Redirect unauthenticated user from {} to authorization route.",
|
||||
req.path()
|
||||
);
|
||||
|
||||
let path = req.uri().to_string();
|
||||
return Ok(req
|
||||
@ -149,10 +154,9 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||
// Restrict access to admin pages
|
||||
if !session.is_admin() && req.path().starts_with(ADMIN_ROUTES) {
|
||||
return Ok(req
|
||||
.into_response(
|
||||
HttpResponse::Unauthorized().body(
|
||||
build_fatal_error_page("You are not allowed to access this resource.")),
|
||||
)
|
||||
.into_response(HttpResponse::Unauthorized().body(build_fatal_error_page(
|
||||
"You are not allowed to access this resource.",
|
||||
)))
|
||||
.map_into_right_body());
|
||||
}
|
||||
|
||||
|
@ -3,4 +3,4 @@ use digest::Digest;
|
||||
#[inline]
|
||||
pub fn sha256(input: &[u8]) -> Vec<u8> {
|
||||
sha2::Sha256::digest(input).to_vec()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
pub mod crypt_utils;
|
||||
pub mod err;
|
||||
pub mod time;
|
||||
pub mod network_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
|
||||
}
|
||||
|
||||
|
||||
/// Get the remote IP address
|
||||
pub fn get_remote_ip(req: &HttpRequest, proxy_ip: Option<&str>) -> IpAddr {
|
||||
let mut ip = req.peer_addr().unwrap().ip();
|
||||
@ -78,7 +77,10 @@ mod test {
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||
.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]
|
||||
@ -87,7 +89,10 @@ mod test {
|
||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||
.insert_header(("X-Forwarded-For", "1.1.1.1"))
|
||||
.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]
|
||||
@ -96,7 +101,10 @@ mod test {
|
||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
||||
.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]
|
||||
@ -105,7 +113,10 @@ mod test {
|
||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||
.insert_header(("X-Forwarded-For", "10::1, 1.2.2.2"))
|
||||
.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]
|
||||
@ -114,7 +125,10 @@ mod test {
|
||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
||||
.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]
|
||||
@ -123,7 +137,10 @@ mod test {
|
||||
.peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
|
||||
.insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
|
||||
.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]
|
||||
@ -141,7 +158,10 @@ mod test {
|
||||
#[test]
|
||||
fn parse_ip_v6_address() {
|
||||
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]
|
||||
@ -155,4 +175,4 @@ mod test {
|
||||
let ip = parse_ip("a::1").unwrap();
|
||||
assert_eq!(ip, IpAddr::V6(Ipv6Addr::new(0xa, 0, 0, 0, 0, 0, 0, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,11 @@ pub fn apply_env_vars(val: &str) -> String {
|
||||
let mut val = val.to_string();
|
||||
|
||||
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) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@ -34,8 +38,8 @@ pub fn apply_env_vars(val: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::env;
|
||||
use crate::utils::string_utils::apply_env_vars;
|
||||
use std::env;
|
||||
|
||||
const VAR_ONE: &str = "VAR_ONE";
|
||||
#[test]
|
||||
@ -52,4 +56,4 @@ mod test {
|
||||
let src = format!("This is ${{{}}}", VAR_INVALID);
|
||||
assert_eq!(src, apply_env_vars(&src));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,15 @@
|
||||
<div>
|
||||
<p>You need to validate a second factor to complete your login.</p>
|
||||
|
||||
{% for factor in factors %}
|
||||
<p>
|
||||
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%;">
|
||||
{{ factor.name }} <br/>
|
||||
<small>{{ factor.type_str() }}</small>
|
||||
</a>
|
||||
</p>
|
||||
{% for factor in user.get_distinct_factors_types() %}
|
||||
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%; display: flex;">
|
||||
<img src="{{ factor.type_image() }}" alt="Factor icon" style="margin-right: 1em;" />
|
||||
<div style="text-align: left;">
|
||||
{{ factor.type_str() }} <br/>
|
||||
<small style="font-size: 0.7em;">{{ factor.description_str() }}</small>
|
||||
</div>
|
||||
</a>
|
||||
<br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
@ -10,8 +10,8 @@
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<p>Please go to your authenticator app <i>{{ factor.name }}</i>, generate a new code and enter it here:</p>
|
||||
<form id="totp_form" method="post" action="{{ factor.login_url(_p.redirect_uri) }}">
|
||||
<p>Please open one of your registered authenticator app, generate a new code and enter it here:</p>
|
||||
<form id="totp_form" method="post">
|
||||
<input type="hidden" id="code" name="code"/>
|
||||
<div class="form-group">
|
||||
<div id="otp" class="inputs d-flex flex-row justify-content-center mt-2">
|
||||
@ -34,6 +34,9 @@
|
||||
|
||||
<script>
|
||||
function OTPInput() {
|
||||
// Set form destination
|
||||
document.getElementById("totp_form").action = location.href;
|
||||
|
||||
const inputs = document.querySelectorAll('#otp > *[id]');
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
// Reset form on init
|
@ -4,7 +4,7 @@
|
||||
<p style="color:red" id="err_target"></p>
|
||||
|
||||
<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 style="margin: 10px 0px;">
|
||||
|
Loading…
Reference in New Issue
Block a user