diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py
index 6d34fad746..3cbdebddbf 100644
--- a/authentik/flows/stage.py
+++ b/authentik/flows/stage.py
@@ -108,6 +108,7 @@ class ChallengeStageView(StageView):
def post(self, request: Request, *args, **kwargs) -> HttpResponse:
"""Handle challenge response"""
+ print(request.data)
valid = False
try:
challenge: ChallengeResponse = self.get_response_instance(data=request.data)
diff --git a/authentik/flows/views/interface.py b/authentik/flows/views/interface.py
index d8b1e1cebc..082b97601d 100644
--- a/authentik/flows/views/interface.py
+++ b/authentik/flows/views/interface.py
@@ -38,6 +38,6 @@ class FlowInterfaceView(InterfaceView):
return False
def get_template_names(self) -> list[str]:
- if self.compat_needs_sfe() or "sfe" in self.request.GET:
- return ["if/flow-sfe.html"]
- return ["if/flow.html"]
+ # if self.compat_needs_sfe() or "sfe" in self.request.GET:
+ return ["if/flow-sfe.html"]
+ # return ["if/flow.html"]
diff --git a/tests/manual/openid-conformance/test-config.json b/tests/manual/openid-conformance/test-config.json
deleted file mode 100644
index 2eed023ffa..0000000000
--- a/tests/manual/openid-conformance/test-config.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "alias": "authentik",
- "description": "authentik",
- "server": {
- "discoveryUrl": "http://host.docker.internal:9000/application/o/conformance/.well-known/openid-configuration"
- },
- "client": {
- "client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
- "client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867"
- },
- "client_secret_post": {
- "client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
- "client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867"
- },
- "client2": {
- "client_id": "ad64aeaf1efe388ecf4d28fcc537e8de08bcae26",
- "client_secret": "ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789"
- },
- "consent": {}
-}
diff --git a/tests/manual/openid-conformance/README.md b/tests/openid-conformance/README.md
similarity index 81%
rename from tests/manual/openid-conformance/README.md
rename to tests/openid-conformance/README.md
index 41b6fe7f9c..e352c96c8c 100644
--- a/tests/manual/openid-conformance/README.md
+++ b/tests/openid-conformance/README.md
@@ -1,7 +1,6 @@
# #Test files for OpenID Conformance testing.
-These config files assume testing is being done using the [OpenID Conformance Suite
-](https://openid.net/certification/about-conformance-suite/), locally.
+These config files assume testing is being done using the [OpenID Conformance Suite](https://openid.net/certification/about-conformance-suite/), locally.
See https://gitlab.com/openid/conformance-suite/-/wikis/Developers/Build-&-Run for running the conformance suite locally.
diff --git a/tests/openid-conformance/compose.yml b/tests/openid-conformance/compose.yml
new file mode 100644
index 0000000000..8f785506b9
--- /dev/null
+++ b/tests/openid-conformance/compose.yml
@@ -0,0 +1,32 @@
+services:
+ mongodb:
+ image: mongo:6.0.13
+ volumes:
+ - mongo:/data/db
+ httpd:
+ image: ghcr.io/beryju/oidc-conformance-suite-httpd:v5.1.32
+ ports:
+ - "8443:8443"
+ - "8444:8444"
+ depends_on:
+ - server
+ server:
+ image: ghcr.io/beryju/oidc-conformance-suite-server:v5.1.32
+ ports:
+ - "9999:9999"
+ command: >
+ java
+ -Xdebug -Xrunjdwp:transport=dt_socket,address=*:9999,server=y,suspend=n
+ -jar /server/fapi-test-suite.jar
+ -Djdk.tls.maxHandshakeMessageSize=65536
+ --fintechlabs.base_url=https://localhost.emobix.co.uk:8443
+ --fintechlabs.base_mtls_url=https://localhost.emobix.co.uk:8444
+ --fintechlabs.devmode=true
+ --fintechlabs.startredir=true
+ links:
+ - mongodb:mongodb
+ depends_on:
+ - mongodb
+
+volumes:
+ mongo:
diff --git a/tests/openid-conformance/conformance.py b/tests/openid-conformance/conformance.py
new file mode 100644
index 0000000000..7f64f9ce87
--- /dev/null
+++ b/tests/openid-conformance/conformance.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+#
+# python wrapper for conformance suite API
+
+
+import asyncio
+import json
+import os
+import re
+import time
+import traceback
+import zipfile
+
+import httpx
+
+
+class RetryTransport(httpx.HTTPTransport):
+ def handle_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ retry = 0
+ resp = None
+ while retry < 5:
+ retry += 1
+ if retry > 2:
+ time.sleep(1)
+ try:
+ if resp is not None:
+ resp.close()
+ resp = super().handle_request(request)
+ except Exception as e:
+ print(f"httpx {request.url} exception {e} caught - retrying")
+ continue
+ if resp.status_code >= 500 and resp.status_code < 600:
+ print(f"httpx {request.url} 5xx response - retrying")
+ continue
+ content_type = resp.headers.get("Content-Type")
+ if content_type is not None:
+ mime_type, _, _ = content_type.partition(";")
+ if mime_type == "application/json":
+ try:
+ resp.read()
+ resp.json()
+ except Exception as e:
+ traceback.print_exc()
+ print(
+ f"httpx {request.url} response not decodable as json '{e}' - retrying"
+ )
+ continue
+ break
+ return resp
+
+
+class Conformance:
+ def __init__(self, api_url_base, api_token, verify_ssl):
+ if not api_url_base.endswith("/"):
+ api_url_base += "/"
+ self.api_url_base = api_url_base
+ transport = RetryTransport(verify=verify_ssl)
+ self.httpclient = httpx.Client(verify=verify_ssl, transport=transport, timeout=20)
+ headers = {"Content-Type": "application/json"}
+ if api_token is not None:
+ headers["Authorization"] = f"Bearer {api_token}"
+ self.httpclient.headers = headers
+
+ async def get_all_test_modules(self):
+ """Returns an array containing a dictionary per test module"""
+ api_url = f"{self.api_url_base}api/runner/available"
+ response = self.httpclient.get(api_url)
+
+ if response.status_code != 200:
+ raise Exception(
+ f"get_all_test_modules failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def exporthtml(self, plan_id, path):
+ for i in range(5):
+ api_url = f"{self.api_url_base}api/plan/exporthtml/{plan_id}"
+ try:
+ with self.httpclient.stream("GET", api_url) as response:
+ if response.status_code != 200:
+ raise Exception(
+ f"exporthtml failed - HTTP {response.status_code:d} {response.content}"
+ )
+ d = response.headers["content-disposition"]
+ local_filename = re.findall('filename="(.+)"', d)[0]
+ full_path = os.path.join(path, local_filename)
+ with open(full_path, "wb") as f:
+ for chunk in response.iter_bytes():
+ f.write(chunk)
+ zip_file = zipfile.ZipFile(full_path)
+ ret = zip_file.testzip()
+ if ret is not None:
+ raise Exception(
+ f"exporthtml for {plan_id} downloaded corrupt zip file {full_path} - {str(ret)}"
+ )
+ return full_path
+ except Exception as e:
+ print(f"httpx {api_url} exception {e} caught - retrying")
+ await asyncio.sleep(1)
+ raise Exception(f"exporthtml for {plan_id} failed even after retries")
+
+ async def create_certification_package(
+ self, plan_id, conformance_pdf_path, rp_logs_zip_path=None, output_zip_directory="./"
+ ):
+ """
+ Create a complete certification package zip file which is written
+ to the directory specified by the 'output_zip_directory' parameter.
+ Calling this function will additionally publish and mark the test plan as immutable.
+
+ :param plan_id: The plan id for which to create the package.
+ :conformance_pdf_path: The path to the signed Certification of Conformance PDF document.
+ :rp_logs_zip_path: Required for RP tests and is the path to the client logs zip file.
+ :output_zip_directory: The (already existing) directory to which the certification package zip file is written.
+ """
+ certificationOfConformancePdf = open(conformance_pdf_path, "rb")
+ clientSideData = (
+ open(rp_logs_zip_path, "rb") if rp_logs_zip_path is not None else open(os.devnull, "rb")
+ )
+ files = {
+ "certificationOfConformancePdf": certificationOfConformancePdf,
+ "clientSideData": clientSideData,
+ }
+ try:
+ with httpx.Client() as multipartClient:
+ multipartClient.headers = self.httpclient.headers.copy()
+ multipartClient.headers.pop("content-type")
+ api_url = f"{self.api_url_base}api/plan/{plan_id}/certificationpackage"
+
+ response = multipartClient.post(api_url, files=files)
+ if response.status_code != 200:
+ raise Exception(
+ f"certificationpackage failed - HTTP {response.status_code:d} {response.content}"
+ )
+
+ d = response.headers["content-disposition"]
+ local_filename = re.findall('filename="(.+)"', d)[0]
+ full_path = os.path.join(output_zip_directory, local_filename)
+ with open(full_path, "wb") as f:
+ for chunk in response.iter_bytes():
+ f.write(chunk)
+ print(
+ f"Certification package zip for plan id {plan_id} written to {full_path}"
+ )
+ finally:
+ certificationOfConformancePdf.close()
+ clientSideData.close()
+
+ async def create_test_plan(self, name, configuration, variant=None):
+ api_url = f"{self.api_url_base}api/plan"
+ payload = {"planName": name}
+ if variant != None:
+ payload["variant"] = json.dumps(variant)
+ response = self.httpclient.post(api_url, params=payload, data=configuration)
+
+ if response.status_code != 201:
+ raise Exception(
+ f"create_test_plan failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def create_test(self, test_name, configuration):
+ api_url = f"{self.api_url_base}api/runner"
+ payload = {"test": test_name}
+ response = self.httpclient.post(api_url, params=payload, data=configuration)
+
+ if response.status_code != 201:
+ raise Exception(
+ f"create_test failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def create_test_from_plan(self, plan_id, test_name):
+ api_url = f"{self.api_url_base}api/runner"
+ payload = {"test": test_name, "plan": plan_id}
+ response = self.httpclient.post(api_url, params=payload)
+
+ if response.status_code != 201:
+ raise Exception(
+ f"create_test_from_plan failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def create_test_from_plan_with_variant(self, plan_id, test_name, variant):
+ api_url = f"{self.api_url_base}api/runner"
+ payload = {"test": test_name, "plan": plan_id}
+ if variant != None:
+ payload["variant"] = json.dumps(variant)
+ response = self.httpclient.post(api_url, params=payload)
+
+ if response.status_code != 201:
+ raise Exception(
+ f"create_test_from_plan failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def get_module_info(self, module_id):
+ api_url = f"{self.api_url_base}api/info/{module_id}"
+ response = self.httpclient.get(api_url)
+
+ if response.status_code != 200:
+ raise Exception(
+ f"get_module_info failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def get_test_log(self, module_id):
+ api_url = f"{self.api_url_base}api/log/{module_id}"
+ response = self.httpclient.get(api_url)
+
+ if response.status_code != 200:
+ raise Exception(
+ f"get_test_log failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def start_test(self, module_id):
+ api_url = f"{self.api_url_base}api/runner/{module_id}"
+ response = self.httpclient.post(api_url)
+
+ if response.status_code != 200:
+ raise Exception(
+ f"start_test failed - HTTP {response.status_code:d} {response.content}"
+ )
+ return response.json()
+
+ async def wait_for_state(self, module_id, required_states, timeout=240):
+ timeout_at = time.time() + timeout
+ while True:
+ if time.time() > timeout_at:
+ raise Exception(
+ f"Timed out waiting for test module {module_id} to be in one of states: {required_states}"
+ )
+
+ info = await self.get_module_info(module_id)
+
+ status = info["status"]
+ print(f"module id {module_id} status is {status}")
+ if status in required_states:
+ return status
+ if status == "INTERRUPTED":
+ raise Exception(f"Test module {module_id} has moved to INTERRUPTED")
+
+ await asyncio.sleep(1)
+
+ async def close_client(self):
+ self.httpclient.close()
diff --git a/tests/manual/openid-conformance/oidc-conformance.yaml b/tests/openid-conformance/oidc-conformance.yaml
similarity index 82%
rename from tests/manual/openid-conformance/oidc-conformance.yaml
rename to tests/openid-conformance/oidc-conformance.yaml
index 54f3da1ff8..f39c8e68d1 100644
--- a/tests/manual/openid-conformance/oidc-conformance.yaml
+++ b/tests/openid-conformance/oidc-conformance.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: OIDC conformance testing
@@ -34,12 +35,15 @@ entries:
name: provider
attrs:
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
+ invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
issuer_mode: global
client_id: 4054d882aff59755f2f279968b97ce8806a926e1
client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867
- redirect_uris: |
- https://localhost:8443/test/a/authentik/callback
- https://localhost.emobix.co.uk:8443/test/a/authentik/callback
+ redirect_uris:
+ - matching_mode: strict
+ url: https://localhost:8443/test/a/authentik/callback
+ - matching_mode: strict
+ url: https://localhost.emobix.co.uk:8443/test/a/authentik/callback
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
@@ -60,12 +64,15 @@ entries:
name: oidc-conformance-2
attrs:
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
+ invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
issuer_mode: global
client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26
client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789
- redirect_uris: |
- https://localhost:8443/test/a/authentik/callback
- https://localhost.emobix.co.uk:8443/test/a/authentik/callback
+ redirect_uris:
+ - matching_mode: strict
+ url: https://localhost:8443/test/a/authentik/callback
+ - matching_mode: strict
+ url: https://localhost.emobix.co.uk:8443/test/a/authentik/callback
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
diff --git a/tests/openid-conformance/run.py b/tests/openid-conformance/run.py
new file mode 100644
index 0000000000..20317c96eb
--- /dev/null
+++ b/tests/openid-conformance/run.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+
+import asyncio
+import json
+import os
+
+from conformance import Conformance
+
+CONFORMANCE_SERVER = "https://localhost:8443/"
+
+# This is the name of the Basic OP test plan:
+test_plan_name = "oidcc-basic-certification-test-plan"
+
+# This is the variant configuration of the test,
+# i.e. static or dynamic metadata location and client registration:
+test_variant_config = {"server_metadata": "discovery", "client_registration": "static_client"}
+
+# This is the required configuration for the test run:
+test_plan_config = {
+ "alias": "authentik",
+ "description": "authentik",
+ "server": {
+ "discoveryUrl": "http://10.120.20.76:9000/application/o/conformance/.well-known/openid-configuration"
+ },
+ "client": {
+ "client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
+ "client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867",
+ },
+ "client_secret_post": {
+ "client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
+ "client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867",
+ },
+ "client2": {
+ "client_id": "ad64aeaf1efe388ecf4d28fcc537e8de08bcae26",
+ "client_secret": "ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789",
+ },
+ "consent": {},
+ "browser": [
+ {
+ "match": "http://10.120.20.76:9000/application/o/authorize*",
+ "tasks": [
+ {
+ "task": "Login",
+ "optional": True,
+ "match": "http://10.120.20.76:9000/if/flow/default-authentication-flow*",
+ "commands": [
+ ["wait", "css", "[name=uid_field]", 10],
+ ["text", "css", "[name=uid_field]", "akadmin"],
+ ["wait", "css", "button[type=submit]", 10],
+ ["click", "css", "button[type=submit]"],
+ ["wait", "css", "[name=password]", 10],
+ ["text", "css", "[name=password]", "foo"],
+ ["click", "css", "button[type=submit]"],
+ ["wait", "css", "#loading-text", 10],
+ ["wait", "contains", "application/o/authorize", 10],
+ ],
+ },
+ {
+ "task": "Authorize",
+ "match": "http://10.120.20.76:9000/application/o/authorize*",
+ },
+ {
+ "task": "Authorize 2",
+ "match": "http://10.120.20.76:9000/if/flow/default-provider-authorization-implicit-consent*",
+ },
+ ],
+ }
+ ],
+}
+
+
+# Create a Conformance instance...
+conformance = Conformance(CONFORMANCE_SERVER, None, verify_ssl=False)
+
+# Create a test plan instance and print the id of it
+test_plan = asyncio.run(
+ conformance.create_test_plan(test_plan_name, json.dumps(test_plan_config), test_variant_config)
+)
+plan_id = test_plan["id"]
+
+print(f"----------------\nBegin {test_plan_name}.")
+print(f"Plan URL: {CONFORMANCE_SERVER}plan-detail.html?plan={plan_id}\n")
+
+# Iterate over the tests in the plan and run them one by one
+for test in test_plan["modules"]:
+
+ # Fetch name and variant of the next test to run
+ module_name = test["testModule"]
+ variant = test["variant"]
+ print(f"Module name: {module_name}")
+ print(f"Variant: {json.dumps(variant)}")
+
+ # Create an instance of that test
+ module_instance = asyncio.run(
+ conformance.create_test_from_plan_with_variant(plan_id, module_name, variant)
+ )
+ module_id = module_instance["id"]
+ print(f"Test URL: {CONFORMANCE_SERVER}log-detail.html?log={module_id}")
+
+ # Run the test and wait for it to finish
+ state = asyncio.run(conformance.wait_for_state(module_id, ["FINISHED"]))
+ print("")
+
+print(f"Plan URL: {CONFORMANCE_SERVER}plan-detail.html?plan={plan_id}\n")
+print(f"\nEnd {test_plan_name}\n----------------")
+
+print("Creating certification package")
+asyncio.run(
+ conformance.create_certification_package(
+ plan_id=plan_id,
+ conformance_pdf_path="OpenID-Certification-of-Conformance.pdf",
+ output_zip_directory="./zips/",
+ )
+)
diff --git a/web/packages/sfe/src/index.ts b/web/packages/sfe/src/index.ts
index 6d6372fe11..311718180d 100644
--- a/web/packages/sfe/src/index.ts
+++ b/web/packages/sfe/src/index.ts
@@ -1,20 +1,12 @@
import { fromByteArray } from "base64-js";
-import "formdata-polyfill";
+// import "formdata-polyfill";
import $ from "jquery";
import "weakmap-polyfill";
-import {
- type AuthenticatorValidationChallenge,
- type AutosubmitChallenge,
- type ChallengeTypes,
- ChallengeTypesFromJSON,
- type ContextualFlowInfo,
- type DeviceChallenge,
- type ErrorDetail,
- type IdentificationChallenge,
- type PasswordChallenge,
- type RedirectChallenge,
-} from "@goauthentik/api";
+
+
+import { type AuthenticatorValidationChallenge, type AutosubmitChallenge, type ChallengeTypes, ChallengeTypesFromJSON, type ContextualFlowInfo, type DeviceChallenge, type ErrorDetail, type IdentificationChallenge, type PasswordChallenge, type RedirectChallenge } from "@goauthentik/api";
+
interface GlobalAuthentik {
brand: {
@@ -67,15 +59,17 @@ class SimpleFlowExecutor {
});
}
- submit(data: { [key: string]: unknown } | FormData) {
+ submit(data: { [key: string]: unknown } | HTMLFormElement) {
$("button[type=submit]").addClass("disabled")
.html(`
- Loading...`);
+ Loading...`);
let finalData: { [key: string]: unknown } = {};
- if (data instanceof FormData) {
- finalData = {};
- data.forEach((value, key) => {
- finalData[key] = value;
+ if (data.tagName === "FORM") {
+ // @ts-expect-error
+ const rawData = $(data as unknown as string).serializeArray();
+
+ rawData.forEach((entry) => {
+ finalData[entry.name] = entry.value;
});
} else {
finalData = data;
@@ -198,8 +192,7 @@ class IdentificationStage extends Stage {
$("#ident-form input[name=uid_field]").trigger("focus");
$("#ident-form").on("submit", (ev) => {
ev.preventDefault();
- const data = new FormData(ev.target as HTMLFormElement);
- this.executor.submit(data);
+ this.executor.submit(ev.target as HTMLFormElement);
});
}
}
@@ -222,8 +215,7 @@ class PasswordStage extends Stage {
$("#password-form input").trigger("focus");
$("#password-form").on("submit", (ev) => {
ev.preventDefault();
- const data = new FormData(ev.target as HTMLFormElement);
- this.executor.submit(data);
+ this.executor.submit(ev.target as HTMLFormElement);
});
}
}
@@ -485,8 +477,7 @@ class AuthenticatorValidateStage extends Stage
$("#totp-form input").trigger("focus");
$("#totp-form").on("submit", (ev) => {
ev.preventDefault();
- const data = new FormData(ev.target as HTMLFormElement);
- this.executor.submit(data);
+ this.executor.submit(ev.target as HTMLFormElement);
});
}