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:
@ -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:
|
api:
|
||||||
insecure: true
|
insecure: true
|
||||||
debug: true
|
debug: true
|
||||||
|
|||||||
97
tests/e2e/test_flows_authenticators_webauthn.py
Normal file
97
tests/e2e/test_flows_authenticators_webauthn.py
Normal 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)
|
||||||
@ -4,34 +4,35 @@ from time import sleep
|
|||||||
|
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.remote.webdriver import WebDriver
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
|
from authentik.core.models import User
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
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):
|
class TestFlowsLoginSFE(SeleniumTestCase):
|
||||||
"""test default login flow"""
|
"""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()
|
@retry()
|
||||||
@apply_blueprint(
|
@apply_blueprint(
|
||||||
"default/flow-default-authentication-flow.yaml",
|
"default/flow-default-authentication-flow.yaml",
|
||||||
@ -46,6 +47,6 @@ class TestFlowsLoginSFE(SeleniumTestCase):
|
|||||||
query={"sfe": True},
|
query={"sfe": True},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.login()
|
login_sfe(self.driver, self.user)
|
||||||
self.wait_for_url(self.if_user_url("/library"))
|
self.wait_for_url(self.if_user_url("/library"))
|
||||||
self.assert_user(self.user)
|
self.assert_user(self.user)
|
||||||
|
|||||||
@ -403,6 +403,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.challenge.deviceChallenges.length === 1) {
|
||||||
|
this.deviceChallenge = this.challenge.deviceChallenges[0];
|
||||||
|
}
|
||||||
if (!this.deviceChallenge) {
|
if (!this.deviceChallenge) {
|
||||||
return this.renderChallengePicker();
|
return this.renderChallengePicker();
|
||||||
}
|
}
|
||||||
@ -431,9 +434,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
${
|
${
|
||||||
challenges.length > 0
|
challenges.length > 0
|
||||||
? "<p>Select an authentication method.</p>"
|
? "<p>Select an authentication method.</p>"
|
||||||
: `
|
: `<p>No compatible authentication method available</p>`
|
||||||
<p>No compatible authentication method available</p>
|
|
||||||
`
|
|
||||||
}
|
}
|
||||||
${challenges
|
${challenges
|
||||||
.map((challenge) => {
|
.map((challenge) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user