-
Notifications
You must be signed in to change notification settings - Fork 127
Open
Description
I've been wresting with a pyqt5 app where I am trying to use this fido2 package for a webauthn login. I can't for the life of my get the assertion window to show above my application, it always shows up below. I've tried lots of way to get and pass the window handle to the windows client but nothing seems to work. I've included a little test application that demonstrates the issue. I am not sure if this is a bug in the fido2 package or if its a bug with my code.
import os
import sys
import traceback
from PyQt5.QtCore import Qt, QT_VERSION_STR
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QPushButton, QTextEdit, QLabel, QMessageBox
)
# python-fido2
try:
# Most common import path in python-fido2
from fido2.client.windows import WindowsClient
from fido2.client import DefaultClientDataCollector
except Exception:
WindowsClient = None
DefaultClientDataCollector = None
# Optional: Win32 helpers (only used for HWND / z-order experiments)
try:
import win32con
import win32gui
except Exception:
win32con = None
win32gui = None
def build_request_options():
# WebAuthn PublicKeyCredentialRequestOptions (Python types, not JSON strings)
# NOTE: rpId must match what the credential was created with, otherwise you'll likely get "No credentials"
return {
"rpId": "example.com", # change to your RP ID if you have a real credential
"challenge": os.urandom(32), # bytes
"timeout": 60_000, # ms
"userVerification": "preferred",
# "allowCredentials": [], # optional; leave unset or empty to let Windows choose
}
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt5 + python-fido2 WindowsClient.get_assertion() Repro")
self.setMinimumSize(700, 450)
central = QWidget(self)
self.setCentralWidget(central)
layout = QVBoxLayout(central)
self.info = QLabel(
"Press the button to call WindowsClient.get_assertion().\n"
"This reproducer logs HWND and any exceptions/output below.\n"
"If the Windows Security/WebAuthn UI appears behind the app, that's the bug symptom."
)
self.info.setWordWrap(True)
layout.addWidget(self.info)
self.btn = QPushButton("Get assertion (WebAuthn)")
self.btn.setCursor(Qt.PointingHandCursor)
self.btn.clicked.connect(self.on_get_assertion_clicked)
layout.addWidget(self.btn)
self.log = QTextEdit()
self.log.setReadOnly(True)
layout.addWidget(self.log)
self.append_log(f"Python: {sys.version}")
self.append_log(f"PyQt5: {QT_VERSION_STR}")
self.append_log(f"fido2 WindowsClient available: {bool(WindowsClient)}")
def append_log(self, s: str):
self.log.append(s)
self.log.ensureCursorVisible()
def active_hwnd(self) -> int:
# Prefer effectiveWinId() if present (gives the native HWND)
w = QApplication.activeWindow() or self
try:
hwnd = int(w.effectiveWinId()) # PyQt5 supports this on QWidget
except Exception:
w.winId()
hwnd = int(w.winId())
return hwnd
def win32_foreground_debug(self, hwnd: int):
if not (win32gui and win32con):
self.append_log("pywin32 not installed; skipping Win32 foreground debug.")
return
try:
# Helpful logging for bug reports
fg = win32gui.GetForegroundWindow()
self.append_log(f"Foreground HWND before: {fg}, title={win32gui.GetWindowText(fg)!r}")
self.append_log(f"Our HWND: {hwnd}, title={win32gui.GetWindowText(hwnd)!r}")
# NOTE: These calls are often *not sufficient* due to Windows foreground rules,
# but are useful to demonstrate attempted mitigation in a bug report.
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(hwnd)
fg2 = win32gui.GetForegroundWindow()
self.append_log(f"Foreground HWND after: {fg2}, title={win32gui.GetWindowText(fg2)!r}")
except Exception:
self.append_log("Win32 foreground debug failed:\n" + traceback.format_exc())
def on_get_assertion_clicked(self):
self.append_log("\n=== Button pressed: get_assertion ===")
if WindowsClient is None or DefaultClientDataCollector is None:
QMessageBox.critical(
self, "Missing dependency",
"Could not import fido2.client.WindowsClient / DefaultClientDataCollector.\n"
"Install: pip install fido2"
)
return
try:
if not WindowsClient.is_available():
QMessageBox.warning(self, "Not available", "WindowsClient.is_available() returned False.")
self.append_log("WindowsClient.is_available() == False")
return
# Make sure our window is visible/active
self.raise_()
self.activateWindow()
hwnd = self.active_hwnd()
self.append_log(f"Using HWND: {hwnd}")
# Optional foreground debugging
self.win32_foreground_debug(hwnd)
# ClientDataCollector origin must match your RP; for repro, example.com is fine
origin = "https://example.com"
collector = DefaultClientDataCollector(origin)
# Create WindowsClient with handle to try to parent/associate UI with this window
client = WindowsClient(collector, handle=hwnd)
request_options = build_request_options()
self.append_log(f"Request rpId={request_options['rpId']!r}, challenge_len={len(request_options['challenge'])}")
self.append_log("Calling client.get_assertion(...) now. If UI appears behind app, capture that.")
assertions = client.get_assertion(request_options)
# If it returns, dump a small summary
try:
resp0 = assertions.get_response(0)
self.append_log("Got assertion response[0].")
self.append_log(f"Credential id len: {len(resp0.credential_id) if getattr(resp0, 'credential_id', None) else 'unknown'}")
except Exception:
self.append_log("Got assertions object, but parsing response[0] failed:\n" + traceback.format_exc())
except Exception as e:
self.append_log("Exception:\n" + traceback.format_exc())
QMessageBox.critical(self, "Exception", str(e))
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Metadata
Metadata
Assignees
Labels
No labels