Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions crates/defguard_core/src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ pub(crate) async fn authenticate(
) -> Result<(CookieJar, PrivateCookieJar, ApiResponse), WebError> {
let username_or_email = data.username;
debug!("Authenticating user {username_or_email}");

// check if user can proceed with login
check_failed_logins(&appstate.failed_logins, &username_or_email)?;

let settings = Settings::get_current_settings();

// attempt to find user first by username and then by email
Expand All @@ -161,7 +163,7 @@ pub(crate) async fn authenticate(
"Failed to authenticate user {username_or_email} internally and through LDAP. Internal error: {err}, LDAP error: {ldap_err}"
);

log_failed_login_attempt(&appstate.failed_logins, &username_or_email);
log_failed_login_attempt(&appstate.failed_logins, &user.username);
appstate.emit_event(ApiEvent {
context: ApiRequestContext::new(
user.id,
Expand All @@ -180,7 +182,7 @@ pub(crate) async fn authenticate(
}
} else {
warn!("Failed to authenticate user {username_or_email}: {err}");
log_failed_login_attempt(&appstate.failed_logins, &username_or_email);
log_failed_login_attempt(&appstate.failed_logins, &user.username);
appstate.emit_event(ApiEvent {
context: ApiRequestContext::new(
user.id,
Expand Down Expand Up @@ -690,6 +692,9 @@ pub async fn totp_code(
) -> Result<(PrivateCookieJar, ApiResponse), WebError> {
if let Some(user) = User::find_by_id(&appstate.pool, session.user_id).await? {
let username = user.username.clone();
// check if user can proceed with login
check_failed_logins(&appstate.failed_logins, &username)?;

debug!("Verifying TOTP for user {}", username);
if user.totp_enabled && user.verify_totp_code(&data.code) {
session
Expand Down Expand Up @@ -744,6 +749,8 @@ pub async fn totp_code(
format!("TOTP authentication is disabled for {username}")
};

log_failed_login_attempt(&appstate.failed_logins, &username);

appstate.emit_event(ApiEvent {
// User may not be fully authenticated so we can't use
// context extractor in this handler since it requires
Expand Down Expand Up @@ -872,6 +879,10 @@ pub async fn email_mfa_code(
) -> Result<(PrivateCookieJar, ApiResponse), WebError> {
if let Some(user) = User::find_by_id(&appstate.pool, session.user_id).await? {
let username = user.username.clone();

// check if user can proceed with login
check_failed_logins(&appstate.failed_logins, &username)?;

debug!("Verifying email MFA code for user {}", username);
if user.email_mfa_enabled && user.verify_email_mfa_code(&data.code) {
session
Expand All @@ -894,7 +905,7 @@ pub async fn email_mfa_code(
}),
})?;
if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) {
debug!("Found openid session cookie.");
debug!("Found OpenID session cookie.");
let redirect_url = openid_cookie.value().to_string();
let private_cookies = private_cookies.remove(openid_cookie);
Ok((
Expand Down Expand Up @@ -926,6 +937,8 @@ pub async fn email_mfa_code(
format!("Email code authentication is disabled for {username}")
};

log_failed_login_attempt(&appstate.failed_logins, &username);

appstate.emit_event(ApiEvent {
// User may not be fully authenticated so we can't use
// context extractor in this handler since it requires
Expand Down
108 changes: 108 additions & 0 deletions crates/defguard_core/tests/integration/api/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,51 @@ async fn test_totp(_: PgPoolOptions, options: PgConnectOptions) {
assert_eq!(response.status(), StatusCode::OK);
}

#[sqlx::test]
async fn dg25_15_test_totp_brute_force(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;

let client = make_client(pool).await;

// login
let auth = Auth::new("hpotter", "pass123");
let response = client.post("/api/v1/auth").json(&auth).send().await;
assert_eq!(response.status(), StatusCode::OK);

// new TOTP secret
let response = client.post("/api/v1/auth/totp/init").send().await;
assert_eq!(response.status(), StatusCode::OK);
let auth_totp: AuthTotp = response.json().await;

// enable TOTP
let code = totp_code(&auth_totp);
let response = client.post("/api/v1/auth/totp").json(&code).send().await;
assert_eq!(response.status(), StatusCode::OK);

// enable MFA
let response = client.put("/api/v1/auth/mfa").send().await;
assert_eq!(response.status(), StatusCode::OK);

// login again, this time a different status code is returned
let response = client.post("/api/v1/auth").json(&auth).send().await;
assert_eq!(response.status(), StatusCode::CREATED);

// provide wrong TOTP code more than 5 times in a row
let code = AuthCode::new("0");
for i in 0..10 {
let response = client
.post("/api/v1/auth/totp/verify")
.json(&code)
.send()
.await;
if i >= 5 {
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
} else {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}
}

static EMAIL_CODE_REGEX: &str = r"<b>(?<code>\d{6})</b>";
fn extract_email_code(content: &str) -> &str {
let re = regex::Regex::new(EMAIL_CODE_REGEX).unwrap();
Expand Down Expand Up @@ -436,6 +481,69 @@ async fn test_email_mfa(_: PgPoolOptions, options: PgConnectOptions) {
assert_eq!(response.status(), StatusCode::OK);
}

#[sqlx::test]
async fn dg25_15_test_email_mfa_brute_force(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;

let (client, state) = make_client_with_state(pool).await;
let pool = state.pool;
let mut mail_rx = state.mail_rx;

// try to initialize email MFA setup before logging in
let response = client.post("/api/v1/auth/email/init").send().await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

// login
let auth = Auth::new("hpotter", "pass123");
let response = client.post("/api/v1/auth").json(&auth).send().await;
assert_eq!(response.status(), StatusCode::OK);
// remove login confirmation email from queue
let _mail = mail_rx.try_recv().unwrap();

// add dummy SMTP settings
let mut settings = Settings::get_current_settings();
settings.smtp_server = Some("smtp_server".into());
settings.smtp_port = Some(587);
settings.smtp_sender = Some("smtp@sender.pl".into());
update_current_settings(&pool, settings).await.unwrap();

// initialize email MFA setup
let response = client.post("/api/v1/auth/email/init").send().await;
assert_eq!(response.status(), StatusCode::OK);
let mail = mail_rx.try_recv().unwrap();
assert_eq!(mail.to, "h.potter@hogwart.edu.uk");
assert_eq!(mail.subject, "Your Multi-Factor Authentication Activation");
let code = extract_email_code(&mail.content);

// finish setup
let code = AuthCode::new(code);
let response = client.post("/api/v1/auth/email").json(&code).send().await;
assert_eq!(response.status(), StatusCode::OK);

// enable MFA
let response = client.put("/api/v1/auth/mfa").send().await;
assert_eq!(response.status(), StatusCode::OK);

// login again, this time a different status code is returned
let response = client.post("/api/v1/auth").json(&auth).send().await;
assert_eq!(response.status(), StatusCode::CREATED);

// provide wrong code more than 5 times in a row
let code = AuthCode::new("0");
for i in 0..10 {
let response = client
.post("/api/v1/auth/email/verify")
.json(&code)
.send()
.await;
if i >= 5 {
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
} else {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}
}

#[sqlx::test]
async fn test_webauthn(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;
Expand Down