Can force 2FA authent

This commit is contained in:
Pierre HUBERT 2024-03-26 21:07:29 +01:00
parent 4bb515366d
commit dfb277d636
8 changed files with 63 additions and 16 deletions

View File

@ -16,6 +16,7 @@ pub struct AuthWebauthnRequest {
credential: PublicKeyCredential, credential: PublicKeyCredential,
} }
#[allow(clippy::too_many_arguments)]
pub async fn auth_webauthn( pub async fn auth_webauthn(
id: Identity, id: Identity,
req: web::Json<AuthWebauthnRequest>, req: web::Json<AuthWebauthnRequest>,
@ -25,10 +26,6 @@ pub async fn auth_webauthn(
users: web::Data<Addr<UsersActor>>, users: web::Data<Addr<UsersActor>>,
logger: ActionLogger, logger: ActionLogger,
) -> impl Responder { ) -> impl Responder {
if !SessionIdentity(Some(&id)).need_2fa_auth() {
return HttpResponse::Unauthorized().json("No 2FA required!");
}
let user_id = SessionIdentity(Some(&id)).user_id(); let user_id = SessionIdentity(Some(&id)).user_id();
match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) { match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) {

View File

@ -13,6 +13,7 @@ use crate::controllers::base_controller::{
build_fatal_error_page, redirect_user, redirect_user_for_login, build_fatal_error_page, redirect_user, redirect_user_for_login,
}; };
use crate::data::action_logger::{Action, ActionLogger}; use crate::data::action_logger::{Action, ActionLogger};
use crate::data::force_2fa_auth::Force2FAAuth;
use crate::data::login_redirect::LoginRedirect; use crate::data::login_redirect::LoginRedirect;
use crate::data::provider::{Provider, ProvidersManager}; use crate::data::provider::{Provider, ProvidersManager};
use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::session_identity::{SessionIdentity, SessionStatus};
@ -311,8 +312,9 @@ pub async fn choose_2fa_method(
id: Option<Identity>, id: Option<Identity>,
query: web::Query<ChooseSecondFactorQuery>, query: web::Query<ChooseSecondFactorQuery>,
users: web::Data<Addr<UsersActor>>, users: web::Data<Addr<UsersActor>>,
force2faauth: Force2FAAuth,
) -> impl Responder { ) -> impl Responder {
if !SessionIdentity(id.as_ref()).need_2fa_auth() { if !SessionIdentity(id.as_ref()).need_2fa_auth() && !force2faauth.force {
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());
} }
@ -329,7 +331,7 @@ pub async fn choose_2fa_method(
// Automatically choose factor if there is only one factor // Automatically choose factor if there is only one factor
if user.get_distinct_factors_types().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, true));
} }
HttpResponse::Ok().content_type("text/html").body( HttpResponse::Ok().content_type("text/html").body(
@ -360,6 +362,7 @@ pub struct LoginWithOTPForm {
} }
/// Login with OTP /// Login with OTP
#[allow(clippy::too_many_arguments)]
pub async fn login_with_otp( pub async fn login_with_otp(
id: Option<Identity>, id: Option<Identity>,
query: web::Query<LoginWithOTPQuery>, query: web::Query<LoginWithOTPQuery>,
@ -368,10 +371,11 @@ pub async fn login_with_otp(
http_req: HttpRequest, http_req: HttpRequest,
remote_ip: RemoteIP, remote_ip: RemoteIP,
logger: ActionLogger, logger: ActionLogger,
force2faauth: Force2FAAuth,
) -> impl Responder { ) -> 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() && !force2faauth.force {
return redirect_user_for_login(query.redirect.get()); return redirect_user_for_login(query.redirect.get());
} }
@ -446,8 +450,9 @@ pub async fn login_with_webauthn(
query: web::Query<LoginWithWebauthnQuery>, query: web::Query<LoginWithWebauthnQuery>,
manager: WebAuthManagerReq, manager: WebAuthManagerReq,
users: web::Data<Addr<UsersActor>>, users: web::Data<Addr<UsersActor>>,
force2faauth: Force2FAAuth,
) -> impl Responder { ) -> impl Responder {
if !SessionIdentity(id.as_ref()).need_2fa_auth() { if !SessionIdentity(id.as_ref()).need_2fa_auth() && !force2faauth.force {
return redirect_user_for_login(query.redirect.get()); return redirect_user_for_login(query.redirect.get());
} }

View File

@ -0,0 +1,36 @@
use crate::data::current_user::CurrentUser;
use actix_web::dev::Payload;
use actix_web::{web, Error, FromRequest, HttpRequest};
use std::future::Future;
use std::pin::Pin;
#[derive(serde::Deserialize)]
pub struct Force2FAAuthQuery {
#[serde(default)]
force_2fa: bool,
}
pub struct Force2FAAuth {
pub force: bool,
}
impl FromRequest for Force2FAAuth {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let req = req.clone();
let query = web::Query::<Force2FAAuthQuery>::from_request(&req, payload)
.into_inner()
.unwrap();
Box::pin(async move {
let user = CurrentUser::from_request(&req, &mut Payload::None).await?;
Ok(Self {
force: query.force_2fa && user.has_two_factor(),
})
})
}
}

View File

@ -5,6 +5,7 @@ pub mod client;
pub mod code_challenge; pub mod code_challenge;
pub mod current_user; pub mod current_user;
pub mod entity_manager; pub mod entity_manager;
pub mod force_2fa_auth;
pub mod id_token; pub mod id_token;
pub mod jwt_signer; pub mod jwt_signer;
pub mod login_redirect; pub mod login_redirect;

View File

@ -90,11 +90,17 @@ impl TwoFactor {
} }
} }
pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String { pub fn login_url(&self, redirect_uri: &LoginRedirect, force_2fa: bool) -> String {
match self.kind { match self.kind {
TwoFactorType::TOTP(_) => format!("/2fa_otp?redirect={}", redirect_uri.get_encoded()), TwoFactorType::TOTP(_) => format!(
"/2fa_otp?redirect={}&force_2fa={force_2fa}",
redirect_uri.get_encoded()
),
TwoFactorType::WEBAUTHN(_) => { TwoFactorType::WEBAUTHN(_) => {
format!("/2fa_webauthn?redirect={}", redirect_uri.get_encoded()) format!(
"/2fa_webauthn?redirect={}&force_2fa={force_2fa}",
redirect_uri.get_encoded()
)
} }
} }
} }

View File

@ -5,7 +5,9 @@
<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 user.get_distinct_factors_types() %} {% for factor in user.get_distinct_factors_types() %}
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(p.redirect_uri) }}" style="width: 100%; display: flex;"> <!-- We can ask to force 2FA, because once we are here, it means 2FA is required anyway... -->
<a class="btn btn-primary btn-lg" href="{{ factor.login_url(p.redirect_uri, true) }}"
style="width: 100%; display: flex;">
<img src="{{ factor.type_image() }}" alt="Factor icon" style="margin-right: 1em;"/> <img src="{{ factor.type_image() }}" alt="Factor icon" style="margin-right: 1em;"/>
<div style="text-align: left;"> <div style="text-align: left;">
{{ factor.type_str() }} <br/> {{ factor.type_str() }} <br/>

View File

@ -28,7 +28,7 @@
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<a href="/2fa_auth?force_display=true&redirect={{ p.redirect_uri.get_encoded() }}">Sign in using another factor</a><br/> <a href="/2fa_auth?force_display=true&redirect={{ p.redirect_uri.get_encoded() }}&force_2fa=true">Sign in using another factor</a><br/>
<a href="/logout">Sign out</a> <a href="/logout">Sign out</a>
</div> </div>

View File

@ -12,7 +12,7 @@
</div> </div>
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<a href="/2fa_auth?force_display=true&redirect={{ p.redirect_uri.get_encoded() }}">Sign in using another factor</a><br/> <a href="/2fa_auth?force_display=true&redirect={{ p.redirect_uri.get_encoded() }}&force_2fa=true">Sign in using another factor</a><br/>
<a href="/logout">Sign out</a> <a href="/logout">Sign out</a>
</div> </div>