Skip to content

Windows client 'get_assertion' windows shows below calling app #274

@RyanHope

Description

@RyanHope

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions