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