tests/e2e: WebAuthn E2E tests (#14461)

* a start of webauthn testing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* separate file, just do it via localhost

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove unneeded stuff

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add auth and sfe tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* auto select device challenge if only 1

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* revert a thing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-06-07 09:31:16 +02:00
committed by GitHub
parent b7417e77c7
commit baa4deda99
4 changed files with 124 additions and 25 deletions

View File

@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://json.schemastore.org/traefik-v2.json
# yaml-language-server: $schema=https://json.schemastore.org/traefik-v3.json
api:
insecure: true
debug: true

View File

@ -0,0 +1,97 @@
"""test flow with WebAuthn Stage"""
from selenium.webdriver.common.virtual_authenticator import (
Protocol,
Transport,
VirtualAuthenticatorOptions,
)
from authentik.blueprints.tests import apply_blueprint
from authentik.stages.authenticator_webauthn.models import (
AuthenticatorWebAuthnStage,
WebAuthnDevice,
)
from tests.e2e.test_flows_login_sfe import login_sfe
from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsAuthenticatorWebAuthn(SeleniumTestCase):
"""test flow with WebAuthn Stage"""
host = "localhost"
def register(self):
options = VirtualAuthenticatorOptions(
protocol=Protocol.CTAP2,
transport=Transport.INTERNAL,
has_resident_key=True,
has_user_verification=True,
is_user_verified=True,
)
self.driver.add_virtual_authenticator(options)
self.driver.get(self.url("authentik_core:if-flow", flow_slug="default-authentication-flow"))
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)
self.driver.get(
self.url(
"authentik_flows:configure",
stage_uuid=AuthenticatorWebAuthnStage.objects.first().stage_uuid,
)
)
self.wait_for_url(self.if_user_url("/library"))
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user, confirmed=True).exists())
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-authenticator-webauthn-setup.yaml")
def test_webauthn_setup(self):
"""Test WebAuthn setup"""
self.register()
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-authenticator-webauthn-setup.yaml")
def test_webauthn_authenticate(self):
"""Test WebAuthn authentication"""
self.register()
self.driver.delete_all_cookies()
self.driver.get(self.url("authentik_core:if-flow", flow_slug="default-authentication-flow"))
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-authenticator-webauthn-setup.yaml")
def test_webauthn_authenticate_sfe(self):
"""Test WebAuthn authentication (SFE)"""
self.register()
self.driver.delete_all_cookies()
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
query={"sfe": True},
)
)
login_sfe(self.driver, self.user)
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -4,34 +4,35 @@ from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webdriver import WebDriver
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import User
from tests.e2e.utils import SeleniumTestCase, retry
def login_sfe(driver: WebDriver, user: User):
"""Do entire login flow adjusted for SFE"""
flow_executor = driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(user.username)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
class TestFlowsLoginSFE(SeleniumTestCase):
"""test default login flow"""
def login(self):
"""Do entire login flow adjusted for SFE"""
flow_executor = self.driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
self.user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username
)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
@ -46,6 +47,6 @@ class TestFlowsLoginSFE(SeleniumTestCase):
query={"sfe": True},
)
)
self.login()
login_sfe(self.driver, self.user)
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -403,6 +403,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
render() {
if (this.challenge.deviceChallenges.length === 1) {
this.deviceChallenge = this.challenge.deviceChallenges[0];
}
if (!this.deviceChallenge) {
return this.renderChallengePicker();
}
@ -431,9 +434,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
${
challenges.length > 0
? "<p>Select an authentication method.</p>"
: `
<p>No compatible authentication method available</p>
`
: `<p>No compatible authentication method available</p>`
}
${challenges
.map((challenge) => {