-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add browser automation for registering passkeys
- Loading branch information
Showing
6 changed files
with
454 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/bin/sh | ||
set -e | ||
|
||
# This script is used to install and run the e2e testsuite. | ||
# Usage: | ||
# | ||
# Run the testsuite: | ||
# ./run_e2e_testsuite.sh | ||
# | ||
# Run a specific test: | ||
# ./run_e2e_testsuite.sh -k test_my_test_name | ||
# | ||
# Run all tests with tracing enabled: | ||
# ./run_e2e_testsuite.sh --tracing on | ||
|
||
playwright install chromium | ||
DJANGO_ALLOW_ASYNC_UNSAFE=1 python -m pytest --browser chromium --headed tests/e2e/ $@ |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import random | ||
from concurrent.futures import Future | ||
|
||
import pytest | ||
from django.test.client import Client as DjangoTestClient | ||
from playwright.sync_api import CDPSession | ||
|
||
from tests.e2e.fixtures import VirtualAuthenticator | ||
|
||
|
||
class FutureWrapper: | ||
def __init__(self): | ||
self.future = Future() | ||
|
||
def get_result(self): | ||
return self.future.result(timeout=5) | ||
|
||
def set_result(self, value): | ||
self.future.set_result(value) | ||
|
||
|
||
@pytest.fixture | ||
def event_waiter(): | ||
def _event_waiter(): | ||
future = FutureWrapper() | ||
return future.get_result, future.set_result | ||
|
||
return _event_waiter | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def cdpsession(page) -> CDPSession: | ||
return page.context.new_cdp_session(page) | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def configure_otp_webauthn(settings, live_server): | ||
settings.OTP_WEBAUTHN_RP_NAME = "OTP WebAuthn Testsuite" | ||
settings.OTP_WEBAUTHN_RP_ID = "localhost" | ||
settings.OTP_WEBAUTHN_ALLOWED_ORIGINS = [live_server.url] | ||
|
||
|
||
@pytest.fixture | ||
def wait_for_javascript_event(page): | ||
"""Returns a function that blocks until a JavaScript event has been fired.""" | ||
|
||
def _wait_for_javascript_event(event_name: str): | ||
# Hot mess - why is there no Playwright API for this? | ||
|
||
# We need to generate a unique name for the nook to store the event result in | ||
nook = "".join([chr(97 + random.randint(0, 25)) for _ in range(25)]) | ||
|
||
# Setup the listener, that stores the event detail in the nook | ||
page.evaluate(f""" | ||
window._event_{nook} = undefined; | ||
document.addEventListener("{event_name}", event => {{ | ||
console.log("Event fired", event); | ||
window._event_{nook} = event.detail; | ||
}}); | ||
""") | ||
|
||
# Return a function that will block until the event has been fired and | ||
# the detail has been stored in the nook. It is indirect like this | ||
# because we circumvent the Content Security Policy that is active for | ||
# the sandbox. | ||
def _return(): | ||
page.wait_for_function( | ||
"payload => payload !== undefined", | ||
arg="window._event_" + nook, | ||
timeout=5000, | ||
) | ||
return page.evaluate("window._event_" + nook) | ||
|
||
return _return | ||
|
||
return _wait_for_javascript_event | ||
|
||
|
||
@pytest.fixture | ||
def virtual_authenticator(cdpsession): | ||
def _get_authenticator(authenticator: VirtualAuthenticator): | ||
cdpsession.send("WebAuthn.enable") | ||
resp = cdpsession.send( | ||
"WebAuthn.addVirtualAuthenticator", | ||
{ | ||
"options": authenticator.as_cdp_options(), | ||
}, | ||
) | ||
return resp | ||
|
||
return _get_authenticator | ||
|
||
|
||
@pytest.fixture | ||
def playwright_force_login(live_server, context): | ||
"""Fixture that forces the given user to be logged in by manipulating the session cookie.""" | ||
|
||
def _playwright_force_login(user): | ||
login_helper = DjangoTestClient() | ||
login_helper.force_login(user) | ||
|
||
context.add_cookies( | ||
[ | ||
{ | ||
"url": live_server.url, | ||
"name": "sessionid", | ||
"value": login_helper.cookies["sessionid"].value, | ||
} | ||
] | ||
) | ||
return user | ||
|
||
return _playwright_force_login |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import enum | ||
from dataclasses import dataclass | ||
|
||
|
||
# Matches StatusEnum in types.ts | ||
class StatusEnum(enum.StrEnum): | ||
UNKNOWN_ERROR = "unknown-error" | ||
STATE_ERROR = "state-error" | ||
SECURITY_ERROR = "security-error" | ||
GET_OPTIONS_FAILED = ("get-options-failed",) | ||
ABORTED = "aborted" | ||
NOT_ALLOWED_OR_ABORTED = "not-allowed-or-aborted" | ||
SERVER_ERROR = "server-error" | ||
SUCCESS = "success" | ||
BUSY = "busy" | ||
|
||
|
||
@dataclass(frozen=True) | ||
class VirtualAuthenticator: | ||
transport: str | ||
protocol: str | ||
has_resident_key: bool | ||
has_user_verification: bool | ||
is_user_verified: bool | ||
default_backup_eligibility: bool | ||
default_backup_state: bool | ||
automaticPresenceSimulation: bool | ||
|
||
def as_cdp_options(self) -> dict: | ||
return { | ||
"transport": self.transport, | ||
"protocol": self.protocol, | ||
"hasResidentKey": self.has_resident_key, | ||
"hasUserVerification": self.has_user_verification, | ||
"isUserVerified": self.is_user_verified, | ||
"defaultBackupEligibility": self.default_backup_eligibility, | ||
"defaultBackupState": self.default_backup_state, | ||
"automaticPresenceSimulation": self.automaticPresenceSimulation, | ||
} | ||
|
||
@classmethod | ||
def internal(cls): | ||
return cls( | ||
transport="internal", | ||
protocol="ctap2", | ||
has_resident_key=True, | ||
has_user_verification=True, | ||
is_user_verified=True, | ||
default_backup_eligibility=True, | ||
default_backup_state=False, | ||
automaticPresenceSimulation=True, | ||
) | ||
|
||
@classmethod | ||
def u2f(cls): | ||
return cls( | ||
transport="usb", | ||
protocol="u2f", | ||
has_resident_key=False, | ||
has_user_verification=False, | ||
is_user_verified=False, | ||
default_backup_eligibility=False, | ||
default_backup_state=False, | ||
automaticPresenceSimulation=True, | ||
) |
Oops, something went wrong.