[GH-ISSUE #6788] AuthRequestResponse notification incorrectly sent to approving device, causing duplicate notification on Android #2537

Open
opened 2026-03-03 02:19:17 +03:00 by kerem · 1 comment
Owner

Originally created by @ivulit on GitHub (Feb 4, 2026).
Original GitHub issue: https://github.com/dani-garcia/vaultwarden/issues/6788

Prerequisites

Vaultwarden Support String

Your environment (Generated via diagnostics page)

  • Vaultwarden version: v1.35.2
  • Web-vault version: v2025.12.1+build.3
  • OS/Arch: linux/x86_64
  • Running within a container: true (Base: Alpine)
  • Database type: SQLite
  • Database version: 3.51.1
  • Uses config.json: false
  • Uses a reverse proxy: true
  • IP Header check: true (X-Real-IP)
  • Internet access: true
  • Internet access via a proxy: false
  • DNS Check: true
  • TZ environment: Europe/Moscow
  • Browser/Server Time Check: true
  • Server/NTP Time Check: true
  • Domain Configuration Check: true
  • HTTPS Check: true
  • Websocket Check: true
  • HTTP Response Checks: true

Config & Details (Generated via diagnostics page)

Show Config & Details

Config:

{
  "_duo_akey": null,
  "_enable_duo": true,
  "_enable_email_2fa": true,
  "_enable_smtp": true,
  "_enable_yubico": true,
  "_icon_service_csp": "",
  "_icon_service_url": "",
  "_ip_header_enabled": true,
  "_max_note_size": 10000,
  "_smtp_img_src": "***:",
  "admin_ratelimit_max_burst": 3,
  "admin_ratelimit_seconds": 300,
  "admin_session_lifetime": 20,
  "admin_token": "***",
  "allowed_connect_src": "",
  "allowed_iframe_ancestors": "",
  "attachments_folder": "data/attachments",
  "auth_request_purge_schedule": "30 * * * * *",
  "authenticator_disable_time_drift": false,
  "data_folder": "data",
  "database_conn_init": "",
  "database_idle_timeout": 600,
  "database_max_conns": 10,
  "database_min_conns": 2,
  "database_timeout": 30,
  "database_url": "***************",
  "db_connection_retries": 15,
  "disable_2fa_remember": false,
  "disable_admin_token": false,
  "disable_icon_download": false,
  "dns_prefer_ipv6": false,
  "domain": "*****://***************",
  "domain_origin": "*****://***************",
  "domain_path": "",
  "domain_set": true,
  "duo_context_purge_schedule": "30 * * * * *",
  "duo_host": null,
  "duo_ikey": null,
  "duo_skey": null,
  "duo_use_iframe": false,
  "email_2fa_auto_fallback": false,
  "email_2fa_enforce_on_verified_invite": false,
  "email_attempts_limit": 3,
  "email_change_allowed": true,
  "email_expiration_time": 600,
  "email_token_size": 6,
  "emergency_access_allowed": true,
  "emergency_notification_reminder_schedule": "0 3 * * * *",
  "emergency_request_timeout_schedule": "0 7 * * * *",
  "enable_db_wal": true,
  "enable_websocket": true,
  "enforce_single_org_with_reset_pw_policy": false,
  "event_cleanup_schedule": "0 10 0 * * *",
  "events_days_retain": null,
  "experimental_client_feature_flags": "ssh-key-vault-item,ssh-agent,fido2-vault-credentials,inline-menu-positioning-improvements,inline-menu-totp,export-attachments,pm-25373-windows-biometrics-v2",
  "extended_logging": true,
  "helo_name": null,
  "hibp_api_key": null,
  "http_request_block_non_global_ips": true,
  "http_request_block_regex": "'\\.(local|arpa)$'",
  "icon_blacklist_non_global_ips": true,
  "icon_blacklist_regex": null,
  "icon_cache_folder": "data/icon_cache",
  "icon_cache_negttl": 21600,
  "icon_cache_ttl": 2592000,
  "icon_download_timeout": 30,
  "icon_redirect_code": 302,
  "icon_service": "internal",
  "incomplete_2fa_schedule": "30 * * * * *",
  "incomplete_2fa_time_limit": 3,
  "increase_note_size_limit": false,
  "invitation_expiration_hours": 120,
  "invitation_org_name": "Vaultwarden",
  "invitations_allowed": true,
  "ip_header": "X-Real-IP",
  "job_poll_interval_ms": 30000,
  "log_file": null,
  "log_level": "error",
  "log_timestamp_format": "%Y-%m-%d %H:%M:%S.%3f",
  "login_ratelimit_max_burst": 10,
  "login_ratelimit_seconds": 60,
  "org_attachment_limit": null,
  "org_creation_users": "",
  "org_events_enabled": false,
  "org_groups_enabled": false,
  "password_hints_allowed": true,
  "password_iterations": 600000,
  "purge_incomplete_sso_auth": "0 20 0 * * *",
  "push_enabled": true,
  "push_identity_uri": "https://identity.bitwarden.com",
  "push_installation_id": "***",
  "push_installation_key": "***",
  "push_relay_uri": "https://push.bitwarden.com",
  "reload_templates": false,
  "require_device_email": false,
  "rsa_key_filename": "data/rsa_key",
  "send_purge_schedule": "0 5 * * * *",
  "sendmail_command": null,
  "sends_allowed": true,
  "sends_folder": "data/sends",
  "show_password_hint": false,
  "signups_allowed": false,
  "signups_domains_whitelist": "",
  "signups_verify": true,
  "signups_verify_resend_limit": 6,
  "signups_verify_resend_time": 3600,
  "smtp_accept_invalid_certs": false,
  "smtp_accept_invalid_hostnames": false,
  "smtp_auth_mechanism": "Login",
  "smtp_debug": false,
  "smtp_embed_images": true,
  "smtp_explicit_tls": null,
  "smtp_from": "*********************",
  "smtp_from_name": "***********",
  "smtp_host": "**************",
  "smtp_password": "***",
  "smtp_port": 465,
  "smtp_security": "force_tls",
  "smtp_ssl": null,
  "smtp_timeout": 15,
  "smtp_username": "***********",
  "sso_allow_unknown_email_verification": false,
  "sso_audience_trusted": null,
  "sso_auth_only_not_session": false,
  "sso_authority": "",
  "sso_authorize_extra_params": "",
  "sso_callback_path": "*****://********************************************",
  "sso_client_cache_expiration": 0,
  "sso_client_id": "",
  "sso_client_secret": "***",
  "sso_debug_tokens": false,
  "sso_enabled": false,
  "sso_master_password_policy": null,
  "sso_only": false,
  "sso_pkce": true,
  "sso_scopes": "email profile",
  "sso_signups_match_email": true,
  "templates_folder": "data/templates",
  "tmp_folder": "data/tmp",
  "trash_auto_delete_days": null,
  "trash_purge_schedule": "0 5 0 * * *",
  "use_sendmail": false,
  "use_syslog": false,
  "user_attachment_limit": null,
  "user_send_limit": null,
  "web_vault_enabled": true,
  "web_vault_folder": "web-vault/",
  "yubico_client_id": null,
  "yubico_secret_key": null,
  "yubico_server": null
}

Vaultwarden Build Version

1.35.2

Deployment method

Official Container Image

Custom deployment method

No response

Reverse Proxy

nginx 1.28.0

Host/Server Operating System

Linux

Operating System Version

Alpine Linux v3.23

Clients

Android

Client Version

Android 2026.1.0 (21141)

Steps To Reproduce

  1. Enable push notifications on the vaultwarden instance
  2. On Device A (e.g. browser), choose "Log in with device
  3. On Device B (Android), receive the AuthRequest push notification
  4. Tap the notification on Device B and approve the login request

Expected Result

Device B shows no further notifications after approval. Device A receives the approval and completes login

Actual Result

Device B receives an AuthRequestResponse (type 16) push notification immediately after approving, which appears as a duplicate auth request notification on Android.

Logs

[2026-01-24 11:02:32.978][response][INFO] (put_auth_request) PUT /api/auth-requests/<auth_request_id> => 200 OK
[2026-01-24 11:02:32.978][vaultwarden::api::push][DEBUG] Auth Push token still valid, no need for a new one
[2026-01-24 11:02:32.978][hyper_util::client::legacy::pool][TRACE] take? ("https", push.bitwarden.com): expiration = Some(90s)
[2026-01-24 11:02:32.978][hyper_util::client::legacy::pool][DEBUG] reuse idle connection for ("https", push.bitwarden.com)

Screenshots or Videos

No response

Additional Context

Analysis

In put_auth_request (src/api/core/accounts.rs:1591-1592), two notification calls are made after approval:

ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await;

ant.send_auth_response() sends through the anonymous WebSocket hub, keyed by auth_request_id — this correctly reaches only Device A (the requesting device).

nt.send_auth_response() does two things:

  1. Sends through the authenticated WebSocket hub to all devices of the user (notifications.rs:523-524: self.send_update(user_id, &data))
  2. Sends a push notification via relay (notifications.rs:527-528)

Both reach Device B (the approving device), which should not receive AuthRequestResponse at all.

Official Bitwarden server behavior

In the official server, PushAuthRequestResponseAsync (IPushNotificationService.cs:387-399) creates a single PushNotification with ExcludeCurrentContext = true.
This notification is routed through all push engines:

  • SignalR (HubHelpers.cs:109-120): AuthRequestResponse is sent only through _anonymousHubContext to Group(AuthRequest.Id). It is not sent through the
    authenticated _hubContext. This is the key difference from AuthRequest (type 15), which is sent through _hubContext.Clients.User() (line 130).
  • Push relay (RelayPushEngine.cs:100): Identifier is set to the current device's identifier when ExcludeCurrentContext = true, and NotificationHubPushEngine
    (BuildTag, line 94) builds an exclusion tag !deviceIdentifier:{identifier}, preventing the push from reaching the approving device.

As a result, Device B receives nothing on the official server.


This analysis was conducted with the help of Claude (Anthropic).

Originally created by @ivulit on GitHub (Feb 4, 2026). Original GitHub issue: https://github.com/dani-garcia/vaultwarden/issues/6788 ### Prerequisites - [x] I have searched the existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=) - [x] I have searched and read the [documentation](https://github.com/dani-garcia/vaultwarden/wiki/) ### Vaultwarden Support String ### Your environment (Generated via diagnostics page) * Vaultwarden version: v1.35.2 * Web-vault version: v2025.12.1+build.3 * OS/Arch: linux/x86_64 * Running within a container: true (Base: Alpine) * Database type: SQLite * Database version: 3.51.1 * Uses config.json: false * Uses a reverse proxy: true * IP Header check: true (X-Real-IP) * Internet access: true * Internet access via a proxy: false * DNS Check: true * TZ environment: Europe/Moscow * Browser/Server Time Check: true * Server/NTP Time Check: true * Domain Configuration Check: true * HTTPS Check: true * Websocket Check: true * HTTP Response Checks: true ### Config & Details (Generated via diagnostics page) <details><summary>Show Config & Details</summary> **Config:** ```json { "_duo_akey": null, "_enable_duo": true, "_enable_email_2fa": true, "_enable_smtp": true, "_enable_yubico": true, "_icon_service_csp": "", "_icon_service_url": "", "_ip_header_enabled": true, "_max_note_size": 10000, "_smtp_img_src": "***:", "admin_ratelimit_max_burst": 3, "admin_ratelimit_seconds": 300, "admin_session_lifetime": 20, "admin_token": "***", "allowed_connect_src": "", "allowed_iframe_ancestors": "", "attachments_folder": "data/attachments", "auth_request_purge_schedule": "30 * * * * *", "authenticator_disable_time_drift": false, "data_folder": "data", "database_conn_init": "", "database_idle_timeout": 600, "database_max_conns": 10, "database_min_conns": 2, "database_timeout": 30, "database_url": "***************", "db_connection_retries": 15, "disable_2fa_remember": false, "disable_admin_token": false, "disable_icon_download": false, "dns_prefer_ipv6": false, "domain": "*****://***************", "domain_origin": "*****://***************", "domain_path": "", "domain_set": true, "duo_context_purge_schedule": "30 * * * * *", "duo_host": null, "duo_ikey": null, "duo_skey": null, "duo_use_iframe": false, "email_2fa_auto_fallback": false, "email_2fa_enforce_on_verified_invite": false, "email_attempts_limit": 3, "email_change_allowed": true, "email_expiration_time": 600, "email_token_size": 6, "emergency_access_allowed": true, "emergency_notification_reminder_schedule": "0 3 * * * *", "emergency_request_timeout_schedule": "0 7 * * * *", "enable_db_wal": true, "enable_websocket": true, "enforce_single_org_with_reset_pw_policy": false, "event_cleanup_schedule": "0 10 0 * * *", "events_days_retain": null, "experimental_client_feature_flags": "ssh-key-vault-item,ssh-agent,fido2-vault-credentials,inline-menu-positioning-improvements,inline-menu-totp,export-attachments,pm-25373-windows-biometrics-v2", "extended_logging": true, "helo_name": null, "hibp_api_key": null, "http_request_block_non_global_ips": true, "http_request_block_regex": "'\\.(local|arpa)$'", "icon_blacklist_non_global_ips": true, "icon_blacklist_regex": null, "icon_cache_folder": "data/icon_cache", "icon_cache_negttl": 21600, "icon_cache_ttl": 2592000, "icon_download_timeout": 30, "icon_redirect_code": 302, "icon_service": "internal", "incomplete_2fa_schedule": "30 * * * * *", "incomplete_2fa_time_limit": 3, "increase_note_size_limit": false, "invitation_expiration_hours": 120, "invitation_org_name": "Vaultwarden", "invitations_allowed": true, "ip_header": "X-Real-IP", "job_poll_interval_ms": 30000, "log_file": null, "log_level": "error", "log_timestamp_format": "%Y-%m-%d %H:%M:%S.%3f", "login_ratelimit_max_burst": 10, "login_ratelimit_seconds": 60, "org_attachment_limit": null, "org_creation_users": "", "org_events_enabled": false, "org_groups_enabled": false, "password_hints_allowed": true, "password_iterations": 600000, "purge_incomplete_sso_auth": "0 20 0 * * *", "push_enabled": true, "push_identity_uri": "https://identity.bitwarden.com", "push_installation_id": "***", "push_installation_key": "***", "push_relay_uri": "https://push.bitwarden.com", "reload_templates": false, "require_device_email": false, "rsa_key_filename": "data/rsa_key", "send_purge_schedule": "0 5 * * * *", "sendmail_command": null, "sends_allowed": true, "sends_folder": "data/sends", "show_password_hint": false, "signups_allowed": false, "signups_domains_whitelist": "", "signups_verify": true, "signups_verify_resend_limit": 6, "signups_verify_resend_time": 3600, "smtp_accept_invalid_certs": false, "smtp_accept_invalid_hostnames": false, "smtp_auth_mechanism": "Login", "smtp_debug": false, "smtp_embed_images": true, "smtp_explicit_tls": null, "smtp_from": "*********************", "smtp_from_name": "***********", "smtp_host": "**************", "smtp_password": "***", "smtp_port": 465, "smtp_security": "force_tls", "smtp_ssl": null, "smtp_timeout": 15, "smtp_username": "***********", "sso_allow_unknown_email_verification": false, "sso_audience_trusted": null, "sso_auth_only_not_session": false, "sso_authority": "", "sso_authorize_extra_params": "", "sso_callback_path": "*****://********************************************", "sso_client_cache_expiration": 0, "sso_client_id": "", "sso_client_secret": "***", "sso_debug_tokens": false, "sso_enabled": false, "sso_master_password_policy": null, "sso_only": false, "sso_pkce": true, "sso_scopes": "email profile", "sso_signups_match_email": true, "templates_folder": "data/templates", "tmp_folder": "data/tmp", "trash_auto_delete_days": null, "trash_purge_schedule": "0 5 0 * * *", "use_sendmail": false, "use_syslog": false, "user_attachment_limit": null, "user_send_limit": null, "web_vault_enabled": true, "web_vault_folder": "web-vault/", "yubico_client_id": null, "yubico_secret_key": null, "yubico_server": null } ``` </details> ### Vaultwarden Build Version 1.35.2 ### Deployment method Official Container Image ### Custom deployment method _No response_ ### Reverse Proxy nginx 1.28.0 ### Host/Server Operating System Linux ### Operating System Version Alpine Linux v3.23 ### Clients Android ### Client Version Android 2026.1.0 (21141) ### Steps To Reproduce 1. Enable push notifications on the vaultwarden instance 2. On Device A (e.g. browser), choose "Log in with device 3. On Device B (Android), receive the AuthRequest push notification 4. Tap the notification on Device B and approve the login request ### Expected Result Device B shows no further notifications after approval. Device A receives the approval and completes login ### Actual Result Device B receives an AuthRequestResponse (type 16) push notification immediately after approving, which appears as a duplicate auth request notification on Android. ### Logs ```text [2026-01-24 11:02:32.978][response][INFO] (put_auth_request) PUT /api/auth-requests/<auth_request_id> => 200 OK [2026-01-24 11:02:32.978][vaultwarden::api::push][DEBUG] Auth Push token still valid, no need for a new one [2026-01-24 11:02:32.978][hyper_util::client::legacy::pool][TRACE] take? ("https", push.bitwarden.com): expiration = Some(90s) [2026-01-24 11:02:32.978][hyper_util::client::legacy::pool][DEBUG] reuse idle connection for ("https", push.bitwarden.com) ``` ### Screenshots or Videos _No response_ ### Additional Context **Analysis** In put_auth_request (src/api/core/accounts.rs:1591-1592), two notification calls are made after approval: ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await; nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await; ant.send_auth_response() sends through the anonymous WebSocket hub, keyed by auth_request_id — this correctly reaches only Device A (the requesting device). nt.send_auth_response() does two things: 1. Sends through the authenticated WebSocket hub to all devices of the user (notifications.rs:523-524: self.send_update(user_id, &data)) 2. Sends a push notification via relay (notifications.rs:527-528) Both reach Device B (the approving device), which should not receive AuthRequestResponse at all. **Official Bitwarden server behavior** In the official server, PushAuthRequestResponseAsync (IPushNotificationService.cs:387-399) creates a single PushNotification with ExcludeCurrentContext = true. This notification is routed through all push engines: - SignalR (HubHelpers.cs:109-120): AuthRequestResponse is sent only through _anonymousHubContext to Group(AuthRequest.Id). It is not sent through the authenticated _hubContext. This is the key difference from AuthRequest (type 15), which is sent through _hubContext.Clients.User() (line 130). - Push relay (RelayPushEngine.cs:100): Identifier is set to the current device's identifier when ExcludeCurrentContext = true, and NotificationHubPushEngine (BuildTag, line 94) builds an exclusion tag !deviceIdentifier:{identifier}, preventing the push from reaching the approving device. As a result, Device B receives nothing on the official server. --- This analysis was conducted with the help of Claude (Anthropic).
Author
Owner

@rafaelfariasbsb commented on GitHub (Feb 26, 2026):

Root Cause Analysis

I traced the notification flow after a device approves an auth request in put_auth_request (src/api/core/accounts.rs:1584-1585):

ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await;

Call 1 — ant.send_auth_response() (anonymous WebSocket hub):
Sends to the anonymous hub keyed by auth_request_id (notifications.rs:556). Only Device A (the requesting device) is subscribed to this token, so this is correct.

Call 2 — nt.send_auth_response() (authenticated hub + push relay):
This does two things:

  1. self.send_update(user_id, &data) (notifications.rs:524) — sends via the authenticated WebSocket hub to all devices of the user, including Device B (the approving device).
  2. push_auth_response() (notifications.rs:528push.rs:322) — sends a push notification to all devices of the user via the Bitwarden relay, which also reaches Device B.

Both paths in call 2 reach Device B, causing the duplicate notification.

How the official Bitwarden server handles this

Looking at the official server code:

  • SignalR (HubHelpers.cs:109-120): AuthRequestResponse (type 16) is sent only through _anonymousHubContext to Group(AuthRequest.Id). It is not sent through the authenticated _hubContext. This is the key difference from AuthRequest (type 15), which uses _hubContext.Clients.User().

  • Push relay (RelayPushEngine.cs:100): When ExcludeCurrentContext = true, the Identifier is set to the acting device's identifier, and NotificationHubPushEngine builds an exclusion tag !deviceIdentifier:{identifier} to prevent the push from reaching the approving device.

Proposed fix

The simplest fix aligned with the official server behavior would be to remove the nt.send_auth_response() call entirely, since AuthRequestResponse should only be delivered through the anonymous hub:

  ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
- nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await;

However, there's a consideration: if Device A is not connected via WebSocket (e.g., a mobile app in background), it won't receive the response through the anonymous hub alone. The push relay would be the fallback for that scenario. A more conservative approach would be to:

  1. Remove the authenticated WebSocket send (which causes the duplicate)
  2. Keep the push relay call but target it at the requesting device rather than broadcasting to all devices

I'd appreciate feedback on which approach the maintainers prefer before submitting a PR.

<!-- gh-comment-id:3969238576 --> @rafaelfariasbsb commented on GitHub (Feb 26, 2026): ## Root Cause Analysis I traced the notification flow after a device approves an auth request in `put_auth_request` (`src/api/core/accounts.rs:1584-1585`): ```rust ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await; nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await; ``` **Call 1 — `ant.send_auth_response()` (anonymous WebSocket hub):** Sends to the anonymous hub keyed by `auth_request_id` (`notifications.rs:556`). Only Device A (the requesting device) is subscribed to this token, so this is correct. **Call 2 — `nt.send_auth_response()` (authenticated hub + push relay):** This does two things: 1. `self.send_update(user_id, &data)` (`notifications.rs:524`) — sends via the **authenticated** WebSocket hub to **all** devices of the user, including Device B (the approving device). 2. `push_auth_response()` (`notifications.rs:528` → `push.rs:322`) — sends a push notification to all devices of the user via the Bitwarden relay, which also reaches Device B. Both paths in call 2 reach Device B, causing the duplicate notification. ### How the official Bitwarden server handles this Looking at the official server code: - **SignalR** (`HubHelpers.cs:109-120`): `AuthRequestResponse` (type 16) is sent **only** through `_anonymousHubContext` to `Group(AuthRequest.Id)`. It is **not** sent through the authenticated `_hubContext`. This is the key difference from `AuthRequest` (type 15), which uses `_hubContext.Clients.User()`. - **Push relay** (`RelayPushEngine.cs:100`): When `ExcludeCurrentContext = true`, the `Identifier` is set to the acting device's identifier, and `NotificationHubPushEngine` builds an exclusion tag `!deviceIdentifier:{identifier}` to prevent the push from reaching the approving device. ### Proposed fix The simplest fix aligned with the official server behavior would be to remove the `nt.send_auth_response()` call entirely, since `AuthRequestResponse` should only be delivered through the anonymous hub: ```diff ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await; - nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await; ``` However, there's a consideration: if Device A is not connected via WebSocket (e.g., a mobile app in background), it won't receive the response through the anonymous hub alone. The push relay would be the fallback for that scenario. A more conservative approach would be to: 1. Remove the authenticated WebSocket send (which causes the duplicate) 2. Keep the push relay call but target it at the **requesting device** rather than broadcasting to all devices I'd appreciate feedback on which approach the maintainers prefer before submitting a PR.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vaultwarden#2537
No description provided.