Metadata-Version: 2.4
Name: duo-pass
Version: 0.0.1
Summary: Duo second-factor handling for Playwright logins: push approval and passcodes behind one interface
Author: Jei Blanchard
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/doctorjei/duo-pass
Project-URL: Source, https://github.com/doctorjei/duo-pass
Project-URL: Issues, https://github.com/doctorjei/duo-pass/issues
Keywords: duo,mfa,playwright,authentication
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.40
Dynamic: license-file

# duo-pass

Duo second-factor handling for Playwright logins — push approval with Verified
Push number matching, or a typed passcode — behind one `Approver` interface.
Spun out of [canvasser](https://github.com/doctorjei/canvasser), where it was
proved against a live Duo tenant. GPL-3.0-or-later.

This module never asks how the Duo page was reached and never leaves the Duo
host except by Duo handing control back. The login flow that got here is the
host application's business.

```python
from playwright.sync_api import sync_playwright
from duo_pass import complete_duo, PushApprover, configure

configure(
    snapshot=lambda page, label: save_somewhere(page, label),
    announce=lambda number: show_somehow(number),
)

with sync_playwright() as p:
    ...  # reach the Duo challenge on `page`
    complete_duo(page, PushApprover())  # or PasscodeApprover()
```

## Hooks

A library must not invent paths or terminal chrome, so two hooks connect this
module to its host. Both have working defaults; both should be overridden by
any host with its own state directory or display layer.

| hook | signature | default |
|---|---|---|
| `snapshot` | `(page, label) -> Path` | PNG under `./snapshots/`, mode 700/600 |
| `announce` | `(number: str \| None) -> None` | framed box on stderr |

`snapshot` fires as `duo-exit-error` (Duo refused the session) and
`duo-timeout` (still on Duo when the wait expired). `announce` fires with
Duo's Verified Push number, or `None` when the tenant has Verified Push off —
absence is normal, not an error, and the frame is drawn either way.

## Notes

- The wait watches for *leaving the Duo host*, never for Duo markup — markup
  changes, leaving does not.
- The trust prompt ("remember this device") is clicked *during* the wait, not
  after it: waiting to leave Duo before handling the prompt deadlocks, because
  the missing click is what would let us leave.
- The Verified Push number is re-read every few seconds while waiting. A push
  can expire and be re-sent with a different number, and a stale number on
  screen is worse than none.
- A Duo `error=` exit is reported as Duo refusing the browser, not as the user
  failing to approve. A timeout says what it knows: approval may have worked
  while Duo never handed control back.
- Nothing here retries a credential. Repeatedly submitting a bad passcode can
  lock the account; a wrong code raises `ApprovalError` instead.
