Skip to content
14 changes: 12 additions & 2 deletions .github/workflows/test-web.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
on: []

on:
push:
branches:
- main
- "release/**"
pull_request:
branches:
- main
- "release/**"

permissions:
contents: read
Expand All @@ -19,6 +26,9 @@ jobs:
run: |
npm i -g npm pnpm
pnpm i --frozen-lockfile
- name: Generate paraglide messages
working-directory: ./web
run: pnpm exec paraglide-js compile --project ./project.inlang --outdir ./src/paraglide
- name: Run tests
working-directory: ./web
run: pnpm run test
1 change: 1 addition & 0 deletions crates/defguard_core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub enum ApiEventType {
mfa_method: MFAMethod,
message: String,
},
RecoveryCodeLoginFailed,
RecoveryCodeUsed,
PasswordChangedByAdmin {
user: User<Id>,
Expand Down
5 changes: 5 additions & 0 deletions crates/defguard_core/src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,11 @@ pub async fn recovery_code(
),
));
}

appstate.emit_event(ApiEvent {
context: ApiRequestContext::new(user.id, username, insecure_ip, user_agent.to_string()),
event: Box::new(ApiEventType::RecoveryCodeLoginFailed),
})?;
}
Err(WebError::Http(StatusCode::UNAUTHORIZED))
}
12 changes: 11 additions & 1 deletion crates/defguard_core/tests/integration/api/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ fn totp_code(auth_totp: &AuthTotp) -> AuthCode {
async fn test_totp(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;

let client = make_client(pool).await;
let mut client = make_client(pool).await;

// login
let auth = Auth::new("hpotter", "pass123");
Expand Down Expand Up @@ -274,6 +274,8 @@ async fn test_totp(_: PgPoolOptions, options: PgConnectOptions) {
let response = client.get("/api/v1/me").send().await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

client.drain_all_events();

// provide wrong TOTP code
let code = AuthCode::new("0");
let response = client
Expand All @@ -282,6 +284,10 @@ async fn test_totp(_: PgPoolOptions, options: PgConnectOptions) {
.send()
.await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
client.verify_api_events(&[ApiEventType::UserMfaLoginFailed {
mfa_method: MFAMethod::OneTimePassword,
message: "TOTP code verification failed".into(),
}]);

// provide recovery code
let code = recovery_codes.codes.unwrap().first().unwrap().clone();
Expand All @@ -291,6 +297,7 @@ async fn test_totp(_: PgPoolOptions, options: PgConnectOptions) {
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);
client.verify_api_events(&[ApiEventType::RecoveryCodeUsed]);

assert_eq!(
response.json::<AuthResponse>().await.user.username,
Expand All @@ -309,13 +316,16 @@ async fn test_totp(_: PgPoolOptions, options: PgConnectOptions) {
let response = client.post("/api/v1/auth").json(&auth).send().await;
assert_eq!(response.status(), StatusCode::CREATED);

client.drain_all_events();

// reuse the same recovery code - shouldn't work
let response = client
.post("/api/v1/auth/recovery")
.json(&json!({ "code": code }))
.send()
.await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
client.verify_api_events(&[ApiEventType::RecoveryCodeLoginFailed]);

// logout
let response = client.post("/api/v1/auth/logout").send().await;
Expand Down
3 changes: 3 additions & 0 deletions crates/defguard_event_logger/src/description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub fn get_defguard_event_description(event: &DefguardEvent) -> Option<String> {
} => Some(format!(
"User login using {mfa_method} failed with: {message}"
)),
DefguardEvent::RecoveryCodeLoginFailed => {
Some("User login with recovery code failed".to_string())
}
DefguardEvent::UserLogout => None,
DefguardEvent::RecoveryCodeUsed => None,
DefguardEvent::PasswordChanged => None,
Expand Down
7 changes: 7 additions & 0 deletions crates/defguard_event_logger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ async fn process_batch(
})
.ok(),
),
DefguardEvent::RecoveryCodeLoginFailed => (
EventType::UserMfaLoginFailed,
serde_json::to_value(LoginFailedMetadata {
message: "Recovery code verification failed".to_string(),
})
.ok(),
),
DefguardEvent::UserLogout => (EventType::UserLogout, None),
DefguardEvent::UserDeviceAdded { owner, device } => (
EventType::DeviceAdded,
Expand Down
1 change: 1 addition & 0 deletions crates/defguard_event_logger/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ pub enum DefguardEvent {
mfa_method: MFAMethod,
message: String,
},
RecoveryCodeLoginFailed,
RecoveryCodeUsed,
PasswordChangedByAdmin {
user: User<Id>,
Expand Down
4 changes: 4 additions & 0 deletions crates/defguard_event_router/src/handlers/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ impl EventRouter {
})),
None,
),
ApiEventType::RecoveryCodeLoginFailed => (
LoggerEvent::Defguard(Box::new(DefguardEvent::RecoveryCodeLoginFailed)),
None,
),
ApiEventType::RecoveryCodeUsed => (
LoggerEvent::Defguard(Box::new(DefguardEvent::RecoveryCodeUsed)),
None,
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/LocationsPage/components/LocationsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const LocationsTable = () => {
onClick: () => {
if (
license?.limits &&
license.limits.locations.current === license.limits.locations.limit
license.limits.locations.current >= license.limits.locations.limit
) {
openModal(ModalName.LimitReached);
} else {
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/UsersOverviewPage/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const UsersTable = () => {
onClick: () => {
if (
license?.limits &&
license.limits.users.current === license.limits.users.limit
license.limits.users.current >= license.limits.users.limit
) {
openModal(ModalName.LimitReached);
} else {
Expand Down
Loading