Compare commits

..

3 Commits

Author SHA1 Message Date
Teffen Ellis
f959d14e00 ci/web: clarify Rust binary build-step label
Step was labeled "Build authentik worker (Rust)" but the binary is
literally named `authentik` (matching lifecycle/ak's PATH probe) and
backs both `ak worker` and `ak allinone`. Renaming the label + adding
a comment so the naming isn't read as a typo.

Co-Authored-By: Agent (authentik-i21994-better-mobile-tangelo) <279763771+playpen-agent@users.noreply.github.com>
2026-05-14 02:34:42 +02:00
Teffen Ellis
52538a5961 ci/web: cache uploaded Playwright reports for 30 days
S3 key already includes run_id + attempt-N, so each path is write-once
— bumping cache-control to max-age=2592000 + immutable avoids hammering
S3 on report reloads. Matches the GHA artifact retention.

Co-Authored-By: Agent (authentik-i21994-better-mobile-tangelo) <279763771+playpen-agent@users.noreply.github.com>
2026-05-14 02:34:24 +02:00
Teffen Ellis
47349e6354 ci/web: run Playwright e2e suite on every PR
Boots the full authentik stack (postgres + Go server + Rust worker)
inside the existing ci-web workflow, applies migrations and the
test-admin user blueprint, then runs `corepack npm run --prefix web
test:e2e` against http://localhost:9000. Uploads the HTML report,
traces/videos, and authentik logs as artifacts on failure so reviewers
can debug without rerunning locally.

Also enables the HTML reporter and screenshot/video capture on CI in
playwright.config.js, and updates the full dev-environment docs to
point at the same npm scripts CI uses so local and CI runs stay in
lockstep.

Closes #21994

Co-Authored-By: Agent (authentik-i21994-better-mobile-tangelo) <279763771+playpen-agent@users.noreply.github.com>

ci/web: make test-admin blueprint self-contained

The previous blueprint used !Find to look up the authentik Admins group,
which raced against system/bootstrap.yaml and resolved to None when the
explicit apply_blueprint step ran before the worker had applied bootstrap.
The serializer rejected groups: [None] with Invalid pk "None".

Define the group in the same blueprint with state: present and reference
it via !KeyOf, so the test admin setup does not depend on any pre-existing
data. If bootstrap has already created the group, state: present is a
no-op on the identifiers; otherwise the group is created here.

Co-Authored-By: Agent (authentik-i21994-better-mobile-tangelo) <279763771+playpen-agent@users.noreply.github.com>

ci/web: format test-admin-user.yaml with prettier

Pick up the 4-space indent that web/'s prettier config enforces. The
file was added under issue #21994 with 2-space indent and tripped the
ci-web format check on push.

Co-Authored-By: Agent (authentik-i21994-better-mobile-tangelo) <279763771+playpen-agent@users.noreply.github.com>

Use parallelism.

Remove guard.

Reorder tests.

Ignore playwright-traces.

Update expected path.

Always parallel.

Flesh out types.

ci/web: post Playwright result comment + gated S3 upload + !cancelled() guards

Three reviewer-facing improvements to the e2e job:

1. Idempotent PR comment summarising Playwright pass/fail/flaky/skipped
   counts. Marker `<!-- playwright-result -->` lets re-runs edit the
   same comment instead of piling up. Skipped on fork PRs where the
   default GITHUB_TOKEN is read-only.

2. Optional S3 publish of the HTML report to
   `s3://authentik-playwright-artifacts/pr-<n>/run-<id>/attempt-<n>/`,
   gated behind `vars.PLAYWRIGHT_S3_ENABLED == 'true'`. The bucket is
   pending infra provisioning; the public URL pattern is already wired
   into the comment so flipping the variable on later requires no
   workflow changes. Borrows the OIDC + IAM role plumbing from
   `.github/workflows/release-publish.yml`.

3. Switch the failure-guarded reporting/upload steps to `!cancelled()`
   so a superseded (cancelled) run no longer emits failure-shaped noise,
   and so successful runs still produce the artifact bundle reviewers
   expect.

Adds the Playwright JSON reporter so the parse step can pull pass/fail
counts from `playwright-report/results.json` for the comment body.

Co-Authored-By: Agent (authentik-i21996-internal-achievable-raisin) <279763771+playpen-agent@users.noreply.github.com>

web/e2e: fix three regressions blocking the parallel suite

Locally and in CI the new `e2e (playwright)` job appeared to "hang"
under `fullyParallel: true` + `workers: "50%"`. The hang was actually
five tests sharing two unrelated bugs that all manifest as 30s test
timeouts; the cluster only *looks* like a parallelism issue because
multiple workers stall on the same wall-clock window. With these three
fixes the full suite is green in 1m48s on `--workers=2` (was: 5 failed
/ 17 passed in 5m30s).

1. `web/test/browser/600-providers.test.ts`
   PR #21647 dropped the `to:` argument on the `session.login()` call
   in this file's `beforeEach`. Without it, `SessionFixture.login()`
   waits for the auth-flow URL pattern to re-appear — which it does
   immediately, since we just navigated there — so the helper returns
   *before* the post-login redirect lands. The wizard buttons probed
   afterward live on `/if/admin/#/core/providers`, which the user never
   actually reaches; every test in the file then hits the 30s
   `beforeEach` timeout. Pin the destination explicitly, matching the
   shape of every other test file.

2. `web/src/admin/roles/ak-role-list.ts`
   The role-list row anchor had no aria-label, so its accessible name
   was the (random, generated) role name. `500-roles.test.ts` searches
   for that anchor with `getByRole("link", { name: "view details" })`
   — the same selector `400-groups.test.ts` uses against the group
   list, where `GroupListPage.row()` *does* set
   `aria-label="View details of group ..."`. Bring the role row to
   parity with groups; the test wasn't wrong, the UI was missing the
   accessibility hook.

3. `web/test/browser/500-roles.test.ts` ("Edit role from view page")
   The post-edit verification used `page.getByText(updatedName)`, but
   on the role view page the new name renders in two places (the
   "Role <name>" page-navbar heading and the description-list value),
   so the bare text match resolves to two elements and trips
   strict-mode. Add `{ exact: true }` so we assert the canonical value
   the edit wrote rather than the heading template.

Co-Authored-By: Agent (authentik-i21996-internal-achievable-raisin) <279763771+playpen-agent@users.noreply.github.com>

Use headless.
2026-05-14 00:37:19 +02:00
95 changed files with 657 additions and 1341 deletions

View File

@@ -64,7 +64,7 @@ runs:
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2
uses: taiki-e/install-action@ec28e287910af896fd98e04056d31fa68607e7ad # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (root, web)

View File

@@ -71,7 +71,7 @@ jobs:
with:
working-directory: web
dependencies: "monorepo"
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version-file: "go.mod"
- name: Generate API Clients

View File

@@ -12,6 +12,20 @@ on:
- main
- version-*
env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
AUTHENTIK_BLUEPRINTS_DIR: "./blueprints"
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST: "true"
# Drives the system/bootstrap.yaml blueprint at startup: creates akadmin with
# these credentials and flips the Setup flag (Setup.set(True)) so the SPA's
# post-login redirect to "/" doesn't bounce through /setup, which would 500
# because the OOBE policy refuses to run once akadmin already has a usable
# password. See authentik/core/setup/signals.py and blueprints/default/flow-oobe.yaml.
AUTHENTIK_BOOTSTRAP_EMAIL: "test-admin@goauthentik.io"
AUTHENTIK_BOOTSTRAP_PASSWORD: "test-runner"
jobs:
lint:
runs-on: ubuntu-latest
@@ -43,6 +57,246 @@ jobs:
NODE_ENV: "production"
working-directory: web/
run: corepack npm run build
e2e:
name: e2e (playwright)
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
# Required so the "Comment Playwright result on PR" step can update its
# marker comment via the gh CLI / REST API.
pull-requests: write
# Required so the optional "Upload HTML report to S3" step can mint OIDC
# credentials with aws-actions/configure-aws-credentials. Harmless when
# the upload is gated off.
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Setup authentik env
uses: ./.github/actions/setup
with:
dependencies: system,python,node,go,rust,runtime
- name: Build web UI
run: corepack npm run --prefix web build
- name: Build authentik server (Go)
run: | # shell
go build -o ./bin/authentik-server ./cmd/server
sudo install -m 0755 ./bin/authentik-server /usr/local/bin/authentik-server
# The Rust binary is named `authentik` (not `authentik-worker`) to match
# `lifecycle/ak`, which probes `command -v authentik` and falls back to
# `cargo run --` if the prebuilt binary isn't on PATH. It serves both
# `ak worker` and `ak allinone`.
- name: Build authentik Rust binary (worker / allinone)
run: | # shell
cargo build --release --bin authentik
sudo install -m 0755 ./target/release/authentik /usr/local/bin/authentik
- name: Apply migrations
run: uv run python -m lifecycle.migrate
- name: Resolve Playwright version
id: playwright-version
working-directory: web
run: | # shell
version=$(node -p "require('@playwright/test/package.json').version")
if [ -z "$version" ]; then
echo "Failed to resolve @playwright/test version" >&2
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
working-directory: web
run: | # shell
if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then
corepack npm exec -- playwright install-deps chromium
else
corepack npm exec -- playwright install --with-deps chromium
fi
- name: Start authentik server and worker
run: | # shell
set -euo pipefail
mkdir -p /tmp/ak-logs
# The Go server (authentik-server) spawns gunicorn as a child via PATH lookup
# and inherits the env that `uv run` set up for it. Verify gunicorn resolves
# under the same launcher so we fail fast here instead of waiting for an
# empty 200 from the proxy fallback later.
uv run --frozen sh -c 'command -v gunicorn' \
|| { echo "gunicorn not resolvable from uv run"; exit 1; }
uv run ak server > /tmp/ak-logs/server.log 2>&1 &
echo $! > /tmp/ak-logs/server.pid
# The Rust worker also opens an HTTP/metrics server on listen.http /
# listen.metrics (default :9000 / :9300). On a single CI host that races the
# Go server's binds and silently steals :9000, leaving Playwright talking to
# a healthcheck-only axum router that returns 200/empty for /if/* paths.
# Pin the worker to disjoint ports so the Go server keeps the public 9000.
AUTHENTIK_LISTEN__HTTP="[::]:9001" \
AUTHENTIK_LISTEN__METRICS="[::]:9301" \
uv run ak worker > /tmp/ak-logs/worker.log 2>&1 &
echo $! > /tmp/ak-logs/worker.pid
- name: Wait for authentik to be ready
run: | # shell
set -euo pipefail
# Readiness probes must verify the Go server is actually serving the request,
# not just that *something* on :9000 returned 200. The Go proxy stamps
# `X-authentik-version` on its static responses and the rendered flow page
# contains the <ak-flow-executor> custom element — both are absent from the
# worker's axum healthcheck router, so checking either rules out the
# port-collision failure mode.
timeout 240 bash -c '
until curl -fsS -o /dev/null http://localhost:9000/-/health/ready/; do
sleep 2
done'
timeout 300 bash -c '
until curl -fsS http://localhost:9000/if/flow/default-authentication-flow/ \
| grep -q "ak-flow-executor"; do
sleep 3
done'
- name: Run Playwright tests
working-directory: web
env:
AK_TEST_RUNNER_PAGE_URL: http://localhost:9000
run: corepack npm run test:e2e
# Reporting / upload steps below intentionally use `!cancelled()` rather
# than `failure()`: a cancelled run (e.g. superseded by a newer push) is
# not a real result and shouldn't produce reviewer-facing artifacts or
# comments. `if-no-files-found: ignore` keeps the "passed" case quiet.
- name: Upload Playwright HTML report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: web/playwright-report/
retention-days: 14
if-no-files-found: ignore
- name: Upload Playwright traces and videos
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-traces
path: web/test-results/
retention-days: 14
if-no-files-found: ignore
- name: Upload authentik server and worker logs
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: authentik-logs
path: /tmp/ak-logs/
retention-days: 14
if-no-files-found: ignore
- name: Parse Playwright results
id: playwright-results
if: ${{ !cancelled() }}
run: | # shell
set -euo pipefail
report=web/playwright-report/results.json
if [ ! -f "$report" ]; then
{
echo "available=false"
echo "passed=0"
echo "failed=0"
echo "flaky=0"
echo "skipped=0"
} >> "$GITHUB_OUTPUT"
exit 0
fi
{
echo "available=true"
echo "passed=$(jq -r '.stats.expected // 0' "$report")"
echo "failed=$(jq -r '.stats.unexpected // 0' "$report")"
echo "flaky=$(jq -r '.stats.flaky // 0' "$report")"
echo "skipped=$(jq -r '.stats.skipped // 0' "$report")"
} >> "$GITHUB_OUTPUT"
# The S3 publishing pair below is intentionally gated off until the
# `authentik-playwright-artifacts` bucket is provisioned by infra. Flip
# the repo variable `PLAYWRIGHT_S3_ENABLED=true` to turn it on; the URL
# baked into the PR comment below already points at the eventual key.
- name: Configure AWS credentials for HTML report upload
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && vars.PLAYWRIGHT_S3_ENABLED == 'true' }}
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
aws-region: eu-central-1
- name: Upload HTML report to S3
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && vars.PLAYWRIGHT_S3_ENABLED == 'true' }}
env:
S3_BUCKET: authentik-playwright-artifacts
S3_KEY_PREFIX: pr-${{ github.event.pull_request.number }}/run-${{ github.run_id }}/attempt-${{ github.run_attempt }}
run: | # shell
set -euo pipefail
if [ ! -d web/playwright-report ]; then
echo "No playwright-report/ produced; skipping S3 upload"
exit 0
fi
# Reports are stamped with run_id + attempt in S3_KEY_PREFIX, so a given
# key is immutable once written — safe to cache aggressively. 30 days
# matches the retention-days on the GHA artifact upload above.
aws s3 cp \
--recursive \
--acl=public-read \
--cache-control "public, max-age=2592000, immutable" \
web/playwright-report/ \
"s3://${S3_BUCKET}/${S3_KEY_PREFIX}/"
# Same-repo guard: fork PRs run with a read-only GITHUB_TOKEN (even after
# maintainer approval), so the comment + edit calls would 403. Skip cleanly
# rather than failing the job on every fork PR run.
- name: Comment Playwright result on PR
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
AVAILABLE: ${{ steps.playwright-results.outputs.available }}
PASSED: ${{ steps.playwright-results.outputs.passed }}
FAILED: ${{ steps.playwright-results.outputs.failed }}
FLAKY: ${{ steps.playwright-results.outputs.flaky }}
SKIPPED: ${{ steps.playwright-results.outputs.skipped }}
S3_ENABLED: ${{ vars.PLAYWRIGHT_S3_ENABLED }}
REPORT_URL: https://authentik-playwright-artifacts.s3.amazonaws.com/pr-${{ github.event.pull_request.number }}/run-${{ github.run_id }}/attempt-${{ github.run_attempt }}/index.html
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
run: | # shell
set -euo pipefail
marker='<!-- playwright-result -->'
if [ "$AVAILABLE" = "true" ]; then
if [ "$FAILED" -gt 0 ]; then
status='❌ Failed'
elif [ "$FLAKY" -gt 0 ]; then
status='⚠️ Passed with flakes'
else
status='✅ Passed'
fi
stats=$(printf '| Result | Count |\n|---|---|\n| ✅ Passed | %s |\n| ❌ Failed | %s |\n| ⚠️ Flaky | %s |\n| ⏭️ Skipped | %s |\n' "$PASSED" "$FAILED" "$FLAKY" "$SKIPPED")
else
status='⚠️ No results produced'
stats='The job did not produce `playwright-report/results.json`. The suite likely crashed before the JSON reporter wrote its output — see the workflow run for setup-step failures.'
fi
if [ "$S3_ENABLED" = "true" ]; then
report_line=$(printf '[HTML report](%s) · [Workflow run](%s)' "$REPORT_URL" "$RUN_URL")
else
report_line=$(printf '[Workflow run](%s) · _HTML report hosting is gated off until the `authentik-playwright-artifacts` S3 bucket is provisioned (`vars.PLAYWRIGHT_S3_ENABLED`). Until then, download the `playwright-report` artifact from the run page._' "$RUN_URL")
fi
body=$(printf '%s\n## Playwright e2e — %s\n\n%s\n\n%s\n' "$marker" "$status" "$stats" "$report_line")
existing=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
--paginate \
--jq "[.[] | select(.body != null and (.body | startswith(\"$marker\")))] | .[0].id // empty")
if [ -n "$existing" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${existing}" -f body="$body" > /dev/null
echo "Updated existing comment ${existing}"
else
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -f body="$body" > /dev/null
echo "Created new playwright-result comment"
fi
ci-web-mark:
if: always()
needs:

8
Cargo.lock generated
View File

@@ -288,9 +288,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.17.0"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
dependencies = [
"aws-lc-fips-sys",
"aws-lc-sys",
@@ -300,9 +300,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.41.0"
version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
dependencies = [
"cc",
"cmake",

View File

@@ -22,7 +22,7 @@ publish = false
arc-swap = "= 1.9.1"
argh = "= 0.1.19"
axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
aws-lc-rs = { version = "= 1.17.0", features = ["fips"] }
aws-lc-rs = { version = "= 1.16.3", features = ["fips"] }
axum = { version = "= 0.8.9", features = ["http2", "macros", "ws"] }
clap = { version = "= 4.6.1", features = ["derive", "env"] }
client-ip = { version = "0.2.1", features = ["forwarded-header"] }

View File

@@ -128,7 +128,6 @@ class SessionEndChallenge(WithUserInfoChallenge):
application_launch_url = CharField(required=False)
invalidation_flow_url = CharField(required=False)
overview_url = CharField(required=False)
brand_name = CharField(required=True)

View File

@@ -16,7 +16,7 @@ from sentry_sdk import start_span
from structlog.stdlib import BoundLogger, get_logger
from authentik.common.oauth.constants import PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI
from authentik.core.models import Application, User, UserTypes
from authentik.core.models import Application, User
from authentik.flows.challenge import (
AccessDeniedChallenge,
Challenge,
@@ -331,10 +331,6 @@ class SessionEndStage(ChallengeStageView):
"component": "ak-stage-session-end",
"brand_name": self.request.brand.branding_title,
}
if self.get_pending_user().type == UserTypes.INTERNAL:
data["overview_url"] = self.request.build_absolute_uri(
reverse("authentik_core:root-redirect")
)
if application:
data["application_name"] = application.name
data["application_launch_url"] = application.get_launch_url(self.get_pending_user())

View File

@@ -17,7 +17,6 @@ class CaptchaStageSerializer(StageSerializer):
"private_key",
"js_url",
"api_url",
"request_content_type",
"interactive",
"score_min_threshold",
"score_max_threshold",

View File

@@ -1,24 +0,0 @@
# Generated by Django 5.2.14 on 2026-05-14 23:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_captcha", "0004_captchastage_interactive"),
]
operations = [
migrations.AddField(
model_name="captchastage",
name="request_content_type",
field=models.TextField(
choices=[
("application/x-www-form-urlencoded", "Form encoded"),
("application/json", "JSON"),
],
default="application/x-www-form-urlencoded",
),
),
]

View File

@@ -8,13 +8,6 @@ from rest_framework.serializers import BaseSerializer
from authentik.flows.models import Stage
class CaptchaRequestContentType(models.TextChoices):
"""Supported request content types for CAPTCHA verification."""
FORM = "application/x-www-form-urlencoded", _("Form encoded")
JSON = "application/json", _("JSON")
class CaptchaStage(Stage):
"""Verify the user is human using Google's reCaptcha/other compatible CAPTCHA solutions."""
@@ -37,10 +30,6 @@ class CaptchaStage(Stage):
js_url = models.TextField(default="https://www.recaptcha.net/recaptcha/api.js")
api_url = models.TextField(default="https://www.recaptcha.net/recaptcha/api/siteverify")
request_content_type = models.TextField(
choices=CaptchaRequestContentType.choices,
default=CaptchaRequestContentType.FORM,
)
@property
def serializer(self) -> type[BaseSerializer]:

View File

@@ -15,7 +15,7 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView
from authentik.lib.utils.http import get_http_session
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.captcha.models import CaptchaRequestContentType, CaptchaStage
from authentik.stages.captcha.models import CaptchaStage
LOGGER = get_logger()
PLAN_CONTEXT_CAPTCHA = "captcha"
@@ -35,23 +35,17 @@ class CaptchaChallenge(WithUserInfoChallenge):
def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: str | None = None):
"""Validate captcha token"""
payload = {
"secret": key or stage.private_key,
"response": token,
"remoteip": remote_ip,
}
body_kwargs = (
{"json": payload}
if stage.request_content_type == CaptchaRequestContentType.JSON
else {"data": payload}
)
try:
response = get_http_session().post(
stage.api_url,
headers={
"Content-Type": stage.request_content_type,
"Content-type": "application/x-www-form-urlencoded",
},
data={
"secret": key or stage.private_key,
"response": token,
"remoteip": remote_ip,
},
**body_kwargs,
)
response.raise_for_status()
data = response.json()

View File

@@ -10,7 +10,7 @@ from authentik.flows.planner import FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.stages.captcha.models import CaptchaRequestContentType, CaptchaStage
from authentik.stages.captcha.models import CaptchaStage
from authentik.stages.captcha.stage import (
PLAN_CONTEXT_CAPTCHA_PRIVATE_KEY,
PLAN_CONTEXT_CAPTCHA_SITE_KEY,
@@ -56,39 +56,6 @@ class TestCaptchaStage(FlowTestCase):
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
self.assertEqual(
mock.request_history[0].headers["Content-Type"],
CaptchaRequestContentType.FORM,
)
self.assertIn("response=PASSED", mock.request_history[0].text)
@Mocker()
def test_valid_json_content_type(self, mock: Mocker):
"""Test valid captcha with JSON verification request"""
self.stage.request_content_type = CaptchaRequestContentType.JSON
self.stage.save()
mock.post(
"https://www.recaptcha.net/recaptcha/api/siteverify",
json={
"success": True,
"score": 0.5,
},
)
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
{"token": "PASSED"},
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
self.assertEqual(
mock.request_history[0].headers["Content-Type"],
CaptchaRequestContentType.JSON,
)
self.assertEqual(mock.request_history[0].json()["response"], "PASSED")
@Mocker()
def test_valid_override(self, mock: Mocker):

View File

@@ -4,7 +4,6 @@ from email.mime.image import MIMEImage
from functools import lru_cache
from pathlib import Path
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.core.mail.message import sanitize_address
from django.template.exceptions import TemplateDoesNotExist
@@ -15,10 +14,9 @@ from django.utils import translation
@lru_cache
def logo_data() -> MIMEImage:
"""Get logo as MIME Image for emails"""
path = Path("web/dist/assets/icons/icon_left_brand.png")
# When running tests, assets might not exist, so fallback to a different icon
if settings.TEST:
path = Path("web/authentik/sources/saml.png")
path = Path("web/icons/icon_left_brand.png")
if not path.exists():
path = Path("web/dist/assets/icons/icon_left_brand.png")
with open(path, "rb") as _logo_file:
logo = MIMEImage(_logo_file.read())
logo.add_header("Content-ID", "<logo>")

View File

@@ -15160,14 +15160,6 @@
"minLength": 1,
"title": "Api url"
},
"request_content_type": {
"type": "string",
"enum": [
"application/x-www-form-urlencoded",
"application/json"
],
"title": "Request content type"
},
"interactive": {
"type": "boolean",
"title": "Interactive"

6
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.3.0
github.com/grafana/pyroscope-go v1.2.8
github.com/jackc/pgx/v5 v5.9.2
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/mitchellh/mapstructure v1.5.0
@@ -69,14 +69,14 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-openapi/validate v0.25.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect

12
go.sum
View File

@@ -105,10 +105,10 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8=
github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4=
github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY=
github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g=
github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M=
github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -139,8 +139,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=

View File

@@ -9,12 +9,12 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1121.0",
"aws-cdk": "^2.1120.0",
"cross-env": "^10.1.0"
},
"engines": {
"node": ">=24",
"npm": ">=11.14.1"
"npm": ">=11.10.1"
}
},
"node_modules/@epic-web/invariant": {
@@ -25,9 +25,9 @@
"license": "MIT"
},
"node_modules/aws-cdk": {
"version": "2.1121.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1121.0.tgz",
"integrity": "sha512-cG7CHt/SytYTfwrK+BUNQpqmS1dwhjt8z6ExKL6GK4n+8/6ZCwFzxlZWA/jUd2+Y9xPc+Q8cLKfMqGmgxEXbkg==",
"version": "2.1120.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1120.0.tgz",
"integrity": "sha512-vDVa0IX0FhizARdY/GLSParFglKbdHCIhM8IDmynrAv9w8uLLljzWMeLUOhC1XpMErDZ/npYEihAOjfKxTaMIw==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -7,7 +7,7 @@
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
},
"devDependencies": {
"aws-cdk": "^2.1121.0",
"aws-cdk": "^2.1120.0",
"cross-env": "^10.1.0"
},
"engines": {

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-14 00:33+0000\n"
"POT-Creation-Date: 2026-05-13 05:39+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -2801,11 +2801,7 @@ msgid "SAML Sessions"
msgstr ""
#: authentik/providers/scim/models.py
msgid "OAuth (Silent)"
msgstr ""
#: authentik/providers/scim/models.py
msgid "OAuth (interactive)"
msgid "OAuth"
msgstr ""
#: authentik/providers/scim/models.py

Binary file not shown.

View File

@@ -29,7 +29,6 @@ type SessionEndChallenge struct {
ApplicationName *string `json:"application_name,omitempty"`
ApplicationLaunchUrl *string `json:"application_launch_url,omitempty"`
InvalidationFlowUrl *string `json:"invalidation_flow_url,omitempty"`
OverviewUrl *string `json:"overview_url,omitempty"`
BrandName string `json:"brand_name"`
AdditionalProperties map[string]interface{}
}
@@ -300,38 +299,6 @@ func (o *SessionEndChallenge) SetInvalidationFlowUrl(v string) {
o.InvalidationFlowUrl = &v
}
// GetOverviewUrl returns the OverviewUrl field value if set, zero value otherwise.
func (o *SessionEndChallenge) GetOverviewUrl() string {
if o == nil || IsNil(o.OverviewUrl) {
var ret string
return ret
}
return *o.OverviewUrl
}
// GetOverviewUrlOk returns a tuple with the OverviewUrl field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *SessionEndChallenge) GetOverviewUrlOk() (*string, bool) {
if o == nil || IsNil(o.OverviewUrl) {
return nil, false
}
return o.OverviewUrl, true
}
// HasOverviewUrl returns a boolean if a field has been set.
func (o *SessionEndChallenge) HasOverviewUrl() bool {
if o != nil && !IsNil(o.OverviewUrl) {
return true
}
return false
}
// SetOverviewUrl gets a reference to the given string and assigns it to the OverviewUrl field.
func (o *SessionEndChallenge) SetOverviewUrl(v string) {
o.OverviewUrl = &v
}
// GetBrandName returns the BrandName field value
func (o *SessionEndChallenge) GetBrandName() string {
if o == nil {
@@ -386,9 +353,6 @@ func (o SessionEndChallenge) ToMap() (map[string]interface{}, error) {
if !IsNil(o.InvalidationFlowUrl) {
toSerialize["invalidation_flow_url"] = o.InvalidationFlowUrl
}
if !IsNil(o.OverviewUrl) {
toSerialize["overview_url"] = o.OverviewUrl
}
toSerialize["brand_name"] = o.BrandName
for key, value := range o.AdditionalProperties {
@@ -443,7 +407,6 @@ func (o *SessionEndChallenge) UnmarshalJSON(data []byte) (err error) {
delete(additionalProperties, "application_name")
delete(additionalProperties, "application_launch_url")
delete(additionalProperties, "invalidation_flow_url")
delete(additionalProperties, "overview_url")
delete(additionalProperties, "brand_name")
o.AdditionalProperties = additionalProperties
}

View File

@@ -35,8 +35,6 @@ pub struct SessionEndChallenge {
skip_serializing_if = "Option::is_none"
)]
pub invalidation_flow_url: Option<String>,
#[serde(rename = "overview_url", skip_serializing_if = "Option::is_none")]
pub overview_url: Option<String>,
#[serde(rename = "brand_name")]
pub brand_name: String,
}
@@ -57,7 +55,6 @@ impl SessionEndChallenge {
application_name: None,
application_launch_url: None,
invalidation_flow_url: None,
overview_url: None,
brand_name,
}
}

View File

@@ -14,11 +14,6 @@
import type { FlowSet } from "./FlowSet";
import { FlowSetFromJSON } from "./FlowSet";
import type { RequestContentTypeEnum } from "./RequestContentTypeEnum";
import {
RequestContentTypeEnumFromJSON,
RequestContentTypeEnumToJSON,
} from "./RequestContentTypeEnum";
/**
* CaptchaStage Serializer
@@ -86,12 +81,6 @@ export interface CaptchaStage {
* @memberof CaptchaStage
*/
apiUrl?: string;
/**
*
* @type {RequestContentTypeEnum}
* @memberof CaptchaStage
*/
requestContentType?: RequestContentTypeEnum;
/**
*
* @type {boolean}
@@ -152,10 +141,6 @@ export function CaptchaStageFromJSONTyped(json: any, ignoreDiscriminator: boolea
publicKey: json["public_key"],
jsUrl: json["js_url"] == null ? undefined : json["js_url"],
apiUrl: json["api_url"] == null ? undefined : json["api_url"],
requestContentType:
json["request_content_type"] == null
? undefined
: RequestContentTypeEnumFromJSON(json["request_content_type"]),
interactive: json["interactive"] == null ? undefined : json["interactive"],
scoreMinThreshold:
json["score_min_threshold"] == null ? undefined : json["score_min_threshold"],
@@ -186,7 +171,6 @@ export function CaptchaStageToJSONTyped(
public_key: value["publicKey"],
js_url: value["jsUrl"],
api_url: value["apiUrl"],
request_content_type: RequestContentTypeEnumToJSON(value["requestContentType"]),
interactive: value["interactive"],
score_min_threshold: value["scoreMinThreshold"],
score_max_threshold: value["scoreMaxThreshold"],

View File

@@ -12,12 +12,6 @@
* Do not edit the class manually.
*/
import type { RequestContentTypeEnum } from "./RequestContentTypeEnum";
import {
RequestContentTypeEnumFromJSON,
RequestContentTypeEnumToJSON,
} from "./RequestContentTypeEnum";
/**
* CaptchaStage Serializer
* @export
@@ -54,12 +48,6 @@ export interface CaptchaStageRequest {
* @memberof CaptchaStageRequest
*/
apiUrl?: string;
/**
*
* @type {RequestContentTypeEnum}
* @memberof CaptchaStageRequest
*/
requestContentType?: RequestContentTypeEnum;
/**
*
* @type {boolean}
@@ -113,10 +101,6 @@ export function CaptchaStageRequestFromJSONTyped(
privateKey: json["private_key"],
jsUrl: json["js_url"] == null ? undefined : json["js_url"],
apiUrl: json["api_url"] == null ? undefined : json["api_url"],
requestContentType:
json["request_content_type"] == null
? undefined
: RequestContentTypeEnumFromJSON(json["request_content_type"]),
interactive: json["interactive"] == null ? undefined : json["interactive"],
scoreMinThreshold:
json["score_min_threshold"] == null ? undefined : json["score_min_threshold"],
@@ -145,7 +129,6 @@ export function CaptchaStageRequestToJSONTyped(
private_key: value["privateKey"],
js_url: value["jsUrl"],
api_url: value["apiUrl"],
request_content_type: RequestContentTypeEnumToJSON(value["requestContentType"]),
interactive: value["interactive"],
score_min_threshold: value["scoreMinThreshold"],
score_max_threshold: value["scoreMaxThreshold"],

View File

@@ -12,12 +12,6 @@
* Do not edit the class manually.
*/
import type { RequestContentTypeEnum } from "./RequestContentTypeEnum";
import {
RequestContentTypeEnumFromJSON,
RequestContentTypeEnumToJSON,
} from "./RequestContentTypeEnum";
/**
* CaptchaStage Serializer
* @export
@@ -54,12 +48,6 @@ export interface PatchedCaptchaStageRequest {
* @memberof PatchedCaptchaStageRequest
*/
apiUrl?: string;
/**
*
* @type {RequestContentTypeEnum}
* @memberof PatchedCaptchaStageRequest
*/
requestContentType?: RequestContentTypeEnum;
/**
*
* @type {boolean}
@@ -112,10 +100,6 @@ export function PatchedCaptchaStageRequestFromJSONTyped(
privateKey: json["private_key"] == null ? undefined : json["private_key"],
jsUrl: json["js_url"] == null ? undefined : json["js_url"],
apiUrl: json["api_url"] == null ? undefined : json["api_url"],
requestContentType:
json["request_content_type"] == null
? undefined
: RequestContentTypeEnumFromJSON(json["request_content_type"]),
interactive: json["interactive"] == null ? undefined : json["interactive"],
scoreMinThreshold:
json["score_min_threshold"] == null ? undefined : json["score_min_threshold"],
@@ -144,7 +128,6 @@ export function PatchedCaptchaStageRequestToJSONTyped(
private_key: value["privateKey"],
js_url: value["jsUrl"],
api_url: value["apiUrl"],
request_content_type: RequestContentTypeEnumToJSON(value["requestContentType"]),
interactive: value["interactive"],
score_min_threshold: value["scoreMinThreshold"],
score_max_threshold: value["scoreMaxThreshold"],

View File

@@ -1,58 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.8.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const RequestContentTypeEnum = {
ApplicationXWwwFormUrlencoded: "application/x-www-form-urlencoded",
ApplicationJson: "application/json",
UnknownDefaultOpenApi: "11184809",
} as const;
export type RequestContentTypeEnum =
(typeof RequestContentTypeEnum)[keyof typeof RequestContentTypeEnum];
export function instanceOfRequestContentTypeEnum(value: any): boolean {
for (const key in RequestContentTypeEnum) {
if (Object.prototype.hasOwnProperty.call(RequestContentTypeEnum, key)) {
if (RequestContentTypeEnum[key as keyof typeof RequestContentTypeEnum] === value) {
return true;
}
}
}
return false;
}
export function RequestContentTypeEnumFromJSON(json: any): RequestContentTypeEnum {
return RequestContentTypeEnumFromJSONTyped(json, false);
}
export function RequestContentTypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RequestContentTypeEnum {
return json as RequestContentTypeEnum;
}
export function RequestContentTypeEnumToJSON(value?: RequestContentTypeEnum | null): any {
return value as any;
}
export function RequestContentTypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): RequestContentTypeEnum {
return value as RequestContentTypeEnum;
}

View File

@@ -70,12 +70,6 @@ export interface SessionEndChallenge {
* @memberof SessionEndChallenge
*/
invalidationFlowUrl?: string;
/**
*
* @type {string}
* @memberof SessionEndChallenge
*/
overviewUrl?: string;
/**
*
* @type {string}
@@ -117,7 +111,6 @@ export function SessionEndChallengeFromJSONTyped(
json["application_launch_url"] == null ? undefined : json["application_launch_url"],
invalidationFlowUrl:
json["invalidation_flow_url"] == null ? undefined : json["invalidation_flow_url"],
overviewUrl: json["overview_url"] == null ? undefined : json["overview_url"],
brandName: json["brand_name"],
};
}
@@ -143,7 +136,6 @@ export function SessionEndChallengeToJSONTyped(
application_name: value["applicationName"],
application_launch_url: value["applicationLaunchUrl"],
invalidation_flow_url: value["invalidationFlowUrl"],
overview_url: value["overviewUrl"],
brand_name: value["brandName"],
};
}

View File

@@ -713,7 +713,6 @@ export * from "./RelatedRule";
export * from "./Reputation";
export * from "./ReputationPolicy";
export * from "./ReputationPolicyRequest";
export * from "./RequestContentTypeEnum";
export * from "./Review";
export * from "./ReviewRequest";
export * from "./ReviewerGroup";

View File

@@ -36,7 +36,7 @@ dependencies = [
"fido2==2.2.0",
"geoip2==5.2.0",
"geopy==2.4.1",
"google-api-python-client==2.196.0",
"google-api-python-client==2.195.0",
"gssapi==1.11.1",
"gunicorn==25.3.0",
"jsonpatch==1.33",
@@ -76,7 +76,7 @@ dependencies = [
[dependency-groups]
dev = [
"aws-cdk-lib==2.253.0",
"aws-cdk-lib==2.252.0",
"bandit==1.9.4",
"black==26.3.1",
"bpython==0.26",

View File

@@ -36339,8 +36339,6 @@ components:
type: string
api_url:
type: string
request_content_type:
$ref: '#/components/schemas/RequestContentTypeEnum'
interactive:
type: boolean
score_min_threshold:
@@ -36386,8 +36384,6 @@ components:
api_url:
type: string
minLength: 1
request_content_type:
$ref: '#/components/schemas/RequestContentTypeEnum'
interactive:
type: boolean
score_min_threshold:
@@ -48291,8 +48287,6 @@ components:
api_url:
type: string
minLength: 1
request_content_type:
$ref: '#/components/schemas/RequestContentTypeEnum'
interactive:
type: boolean
score_min_threshold:
@@ -53874,11 +53868,6 @@ components:
minimum: -2147483648
required:
- name
RequestContentTypeEnum:
enum:
- application/x-www-form-urlencoded
- application/json
type: string
Review:
type: object
description: |-
@@ -55974,8 +55963,6 @@ components:
type: string
invalidation_flow_url:
type: string
overview_url:
type: string
brand_name:
type: string
required:

16
uv.lock generated
View File

@@ -345,7 +345,7 @@ requires-dist = [
{ name = "fido2", specifier = "==2.2.0" },
{ name = "geoip2", specifier = "==5.2.0" },
{ name = "geopy", specifier = "==2.4.1" },
{ name = "google-api-python-client", specifier = "==2.196.0" },
{ name = "google-api-python-client", specifier = "==2.195.0" },
{ name = "gssapi", specifier = "==1.11.1" },
{ name = "gunicorn", specifier = "==25.3.0" },
{ name = "jsonpatch", specifier = "==1.33" },
@@ -385,7 +385,7 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "aws-cdk-lib", specifier = "==2.253.0" },
{ name = "aws-cdk-lib", specifier = "==2.252.0" },
{ name = "bandit", specifier = "==1.9.4" },
{ name = "black", specifier = "==26.3.1" },
{ name = "bpython", specifier = "==0.26" },
@@ -495,7 +495,7 @@ wheels = [
[[package]]
name = "aws-cdk-lib"
version = "2.253.0"
version = "2.252.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aws-cdk-asset-awscli-v1" },
@@ -506,9 +506,9 @@ dependencies = [
{ name = "publication" },
{ name = "typeguard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2d/ee/be9ce71331823a9860a8473a3a301359c154a5402f2895a9a30d222acaec/aws_cdk_lib-2.253.0.tar.gz", hash = "sha256:6db33e164f9afd6207698d382579bf62f762f14325f4b6d77643865e92448f20", size = 49584110, upload-time = "2026-05-06T17:42:37.086Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/2e/468ed756570af782831bc0518b4f187773b036342ce1b6f3d4e13e6127d8/aws_cdk_lib-2.252.0.tar.gz", hash = "sha256:2498d771ab141599c48494bd2564ee9a4fbaade54befa9356811e9454616d0a0", size = 49479070, upload-time = "2026-04-30T12:31:54.452Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/94/53b9f2aca8d5111ee952c0355f64ecba92809daced3364170e9bcfed81c6/aws_cdk_lib-2.253.0-py3-none-any.whl", hash = "sha256:58997c8a5af49d5328f5106468581a14235a1356dfe7574bb14656799594a2b5", size = 50269112, upload-time = "2026-05-06T17:41:54.504Z" },
{ url = "https://files.pythonhosted.org/packages/ae/94/32c21ad93dc21554286955fd5ebc68cb91149cc5f7f3154b07927c3fc693/aws_cdk_lib-2.252.0-py3-none-any.whl", hash = "sha256:c96d02582d344ee81ea2ef8a5e22b6e680789973804720ec9f0e95a050257db1", size = 50157828, upload-time = "2026-04-30T12:31:11.041Z" },
]
[[package]]
@@ -1597,7 +1597,7 @@ wheels = [
[[package]]
name = "google-api-python-client"
version = "2.196.0"
version = "2.195.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
@@ -1606,9 +1606,9 @@ dependencies = [
{ name = "httplib2" },
{ name = "uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/f3/34ef8aca7909675fe327f96c1ed927f0520e7acf68af19157e96acc05e76/google_api_python_client-2.196.0.tar.gz", hash = "sha256:9f335d38f6caaa2747bcf64335ed1a9a19047d53e86538eda6a1b17d37f1743d", size = 14628129, upload-time = "2026-05-06T23:47:35.655Z" }
sdist = { url = "https://files.pythonhosted.org/packages/69/07/08d759b9cb10f48af14b25262dd0d6685ca8cda6c1f9e8a8109f57457205/google_api_python_client-2.195.0.tar.gz", hash = "sha256:c72cf2661c3addf01c880ce60541e83e1df354644b874f7f9d8d5ed2070446ae", size = 14584819, upload-time = "2026-04-30T21:51:50.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/c7/1817b4edf966d5afcac1c0781ca36d621bc0cb58104c4e7c2a475ab185f7/google_api_python_client-2.196.0-py3-none-any.whl", hash = "sha256:2591e9b47dcb17e4e62a09370aaee3bcf323af8f28ccecdabcd0a42a23ca4db5", size = 15206663, upload-time = "2026-05-06T23:47:32.886Z" },
{ url = "https://files.pythonhosted.org/packages/21/b9/2c71095e31fff57668fec7c07ac897df065f15521d070e63229e13689590/google_api_python_client-2.195.0-py3-none-any.whl", hash = "sha256:753e62057f23049a89534bea0162b60fe391b85fb86d80bcdf884d05ec91c5bf", size = 15162418, upload-time = "2026-04-30T21:51:47.444Z" },
]
[[package]]

1
web/.gitignore vendored
View File

@@ -32,6 +32,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
playwright-report
playwright-traces
test-results
*.lcov

View File

@@ -3,8 +3,6 @@
* @import { StorybookConfig } from "@storybook/web-components-vite";
*/
import { copyAssets } from "../scripts/build-assets.mjs";
/**
* @param {TemplateStringsArray} strings
* @param {...any} values
@@ -12,14 +10,15 @@ import { copyAssets } from "../scripts/build-assets.mjs";
*/
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
await copyAssets();
/**
* @satisfies {StorybookConfig}
*/
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
staticDirs: [{ from: "../dist/assets", to: "/static/dist/assets" }],
staticDirs: [
{ from: "../icons", to: "/static/dist/assets/icons" },
{ from: "../authentik", to: "/static/authentik" },
],
addons: [
// ---
"@storybook/addon-links",

View File

@@ -2,12 +2,6 @@ import { PageFixture } from "#e2e/fixtures/PageFixture";
import { Page } from "@playwright/test";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
export const BAD_USERNAME = "bad-username@bad-login.io";
export const BAD_PASSWORD = "-this-is-a-bad-password-";
export interface LoginInit {
username?: string;
password?: string;

26
web/e2e/types/node.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
/**
* @file Node.js global types and environment variables.
*/
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
/**
* The email address of the bootstrap user created when running the tests.
*
* @format email
*/
readonly AUTHENTIK_BOOTSTRAP_EMAIL?: string;
/**
* The password of the bootstrap user created when running the tests.
*/
readonly AUTHENTIK_BOOTSTRAP_PASSWORD?: string;
/**
* The directory where the authentik blueprints are stored.
*/
readonly AUTHENTIK_BLUEPRINTS_DIR?: string;
}
}
}
}

BIN
web/icons/brand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

1
web/icons/brand.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3064.87 487.37"><defs><symbol id="a" viewBox="0 0 2865.3 437.72"><g style="isolation:isolate;"><path d="M238.73,125.38h76.4v304.5h-76.4v-32.18c-14.91,14.18-29.87,24.4-44.87,30.65-15,6.25-31.26,9.37-48.78,9.37-39.32,0-73.33-15.25-102.04-45.76C14.35,361.45,0,323.53,0,278.19s13.89-85.54,41.65-115.58c27.77-30.04,61.5-45.06,101.19-45.06,18.26,0,35.4,3.45,51.43,10.35,16.03,6.91,30.84,17.26,44.45,31.07v-33.58ZM158.41,188.07c-23.62,0-43.24,8.35-58.86,25.05-15.62,16.7-23.43,38.11-23.43,64.23s7.95,47.96,23.84,64.93c15.9,16.98,35.47,25.47,58.72,25.47s43.89-8.35,59.69-25.05c15.8-16.7,23.71-38.57,23.71-65.63s-7.9-47.95-23.71-64.37c-15.81-16.42-35.8-24.63-59.97-24.63Z" style="fill:#fd4b2d;"/><path d="M403.16,125.38h77.24v146.65c0,28.55,1.96,48.37,5.89,59.47,3.93,11.1,10.24,19.73,18.94,25.89,8.69,6.16,19.4,9.24,32.12,9.24s23.52-3.03,32.4-9.1c8.88-6.06,15.47-14.97,19.78-26.73,3.18-8.77,4.77-27.52,4.77-56.25V125.38h76.41v129.02c0,53.18-4.2,89.56-12.59,109.15-10.26,23.88-25.38,42.22-45.34,54.99-19.97,12.78-45.34,19.17-76.13,19.17-33.4,0-60.41-7.46-81.02-22.39-20.62-14.92-35.13-35.73-43.52-62.41-5.97-18.47-8.96-52.06-8.96-100.75v-126.78Z" style="fill:#fd4b2d;"/><path d="M796.76,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M999.76,7.84h75.85v148.33c14.93-12.88,29.95-22.53,45.06-28.97,15.11-6.44,30.41-9.65,45.9-9.65,30.23,0,55.7,10.45,76.41,31.34,17.73,18.1,26.59,44.69,26.59,79.76v201.23h-75.29v-133.5c0-35.27-1.68-59.15-5.04-71.65-3.36-12.5-9.09-21.83-17.21-27.99-8.12-6.15-18.15-9.23-30.09-9.23-15.49,0-28.78,5.13-39.88,15.39-11.11,10.26-18.8,24.26-23.09,41.98-2.24,9.14-3.36,30.04-3.36,62.69v122.3h-75.85V7.84Z" style="fill:#fd4b2d;"/><path d="M1688.63,299.74h-245.45c3.54,21.65,13.01,38.86,28.41,51.64,15.39,12.78,35.03,19.17,58.91,19.17,28.55,0,53.08-9.98,73.6-29.95l64.37,30.23c-16.05,22.77-35.26,39.6-57.65,50.52-22.39,10.91-48.98,16.37-79.76,16.37-47.77,0-86.67-15.06-116.71-45.2-30.04-30.13-45.06-67.87-45.06-113.21s14.97-85.03,44.92-115.73c29.95-30.69,67.49-46.04,112.65-46.04,47.95,0,86.95,15.35,116.99,46.04,30.04,30.69,45.06,71.23,45.06,121.61l-.28,14.55ZM1612.22,239.57c-5.05-16.98-15-30.79-29.86-41.42-14.86-10.63-32.1-15.95-51.72-15.95-21.3,0-40,5.98-56.07,17.91-10.09,7.47-19.44,20.62-28.03,39.46h165.68Z" style="fill:#fd4b2d;"/><path d="M1790.6,125.38h76.41v31.21c17.33-14.61,33.02-24.77,47.09-30.48,14.06-5.71,28.46-8.57,43.18-8.57,30.18,0,55.8,10.54,76.85,31.62,17.7,17.91,26.55,44.41,26.55,79.48v201.23h-75.57v-133.35c0-36.34-1.63-60.47-4.89-72.4-3.26-11.93-8.93-21.01-17.03-27.26-8.1-6.24-18.1-9.36-30.01-9.36-15.45,0-28.71,5.17-39.78,15.51-11.08,10.35-18.76,24.65-23.04,42.91-2.24,9.5-3.35,30.1-3.35,61.78v122.16h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2183.92,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M2416.46,0c13.39,0,24.88,4.85,34.46,14.55,9.58,9.7,14.38,21.46,14.38,35.27s-4.75,25.24-14.24,34.84c-9.49,9.61-20.84,14.41-34.04,14.41s-25.16-4.9-34.75-14.69c-9.58-9.79-14.37-21.69-14.37-35.68s4.74-24.91,14.23-34.43c9.49-9.51,20.93-14.27,34.33-14.27ZM2378.26,125.38h76.41v304.5h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2564.75,7.84h76.41v243.09l112.51-125.54h95.96l-131.17,145.94,146.86,158.56h-94.85l-129.3-140.34v140.34h-76.41V7.84Z" style="fill:#fd4b2d;"/></g></symbol></defs><use width="2865.3" height="437.72" transform="translate(99.78 24.83)" xlink:href="#a"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
web/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

1
web/icons/icon.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="c" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000"><defs><symbol id="a" viewBox="0 0 998.94 763.82"><path d="M829.67,0h-425.28c-93.1,0-169.27,76.17-169.27,169.27v425.28c0,93.1,76.17,169.27,169.27,169.27h50.18v-165.68h324.96v165.68h50.14c93.1,0,169.27-76.17,169.27-169.27V169.27C998.94,76.17,922.77,0,829.67,0ZM755.98,463.53H235.4v-114.49h268.96v-158.97h43.68v94.7h25.61v-94.7h30.88v69.64h25.61v-69.64h30.88v116.35h25.61v-116.35h43.68v158.97h25.69v114.49Z" style="fill:#fd4b2d;"/><g id="b"><path d="M237.36,342.19h-.02c-25.34-34.27-63.32-69.15-105.42-69.15-48.4.03-92.89,26.58-115.91,69.15-48.08,83.85,18.39,196.94,115.91,194.36,75.46,0,137.69-111.95,137.69-131.75,0-8.76-12.18-35.49-32.25-62.61ZM77.32,342.19c27.16-23.43,66.59-30.27,95.1,0h.02c21.51,19.51,40.28,47.91,47.08,62.35-84.6,176.88-232.87,26.13-142.2-62.35Z" style="fill:#fd4b2d;"/></g></symbol></defs><use width="998.94" height="763.82" transform="translate(1 117.03)" xlink:href="#a"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="i" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3767.3 592.89"><defs><symbol id="a" viewBox="0 0 998.94 763.82"><path d="M829.67,0h-425.28c-93.1,0-169.27,76.17-169.27,169.27v425.28c0,93.1,76.17,169.27,169.27,169.27h50.18v-165.68h324.96v165.68h50.14c93.1,0,169.27-76.17,169.27-169.27V169.27C998.94,76.17,922.77,0,829.67,0ZM755.98,463.53H235.4v-114.49h268.96v-158.97h43.68v94.7h25.61v-94.7h30.88v69.64h25.61v-69.64h30.88v116.35h25.61v-116.35h43.68v158.97h25.69v114.49Z" style="fill:#fd4b2d;"/><g id="b"><path d="M237.36,342.19h-.02c-25.34-34.27-63.32-69.15-105.42-69.15-48.4.03-92.89,26.58-115.91,69.15-48.08,83.85,18.39,196.94,115.91,194.36,75.46,0,137.69-111.95,137.69-131.75,0-8.76-12.18-35.49-32.25-62.61ZM77.32,342.19c27.16-23.43,66.59-30.27,95.1,0h.02c21.51,19.51,40.28,47.91,47.08,62.35-84.6,176.88-232.87,26.13-142.2-62.35Z" style="fill:#fd4b2d;"/></g></symbol><symbol id="c" viewBox="0 0 2865.3 437.72"><g style="isolation:isolate;"><path d="M238.73,125.38h76.4v304.5h-76.4v-32.18c-14.91,14.18-29.87,24.4-44.87,30.65-15,6.25-31.26,9.37-48.78,9.37-39.32,0-73.33-15.25-102.04-45.76C14.35,361.45,0,323.53,0,278.19s13.89-85.54,41.65-115.58c27.77-30.04,61.5-45.06,101.19-45.06,18.26,0,35.4,3.45,51.43,10.35,16.03,6.91,30.84,17.26,44.45,31.07v-33.58ZM158.41,188.07c-23.62,0-43.24,8.35-58.86,25.05-15.62,16.7-23.43,38.11-23.43,64.23s7.95,47.96,23.84,64.93c15.9,16.98,35.47,25.47,58.72,25.47s43.89-8.35,59.69-25.05c15.8-16.7,23.71-38.57,23.71-65.63s-7.9-47.95-23.71-64.37c-15.81-16.42-35.8-24.63-59.97-24.63Z" style="fill:#fd4b2d;"/><path d="M403.16,125.38h77.24v146.65c0,28.55,1.96,48.37,5.89,59.47,3.93,11.1,10.24,19.73,18.94,25.89,8.69,6.16,19.4,9.24,32.12,9.24s23.52-3.03,32.4-9.1c8.88-6.06,15.47-14.97,19.78-26.73,3.18-8.77,4.77-27.52,4.77-56.25V125.38h76.41v129.02c0,53.18-4.2,89.56-12.59,109.15-10.26,23.88-25.38,42.22-45.34,54.99-19.97,12.78-45.34,19.17-76.13,19.17-33.4,0-60.41-7.46-81.02-22.39-20.62-14.92-35.13-35.73-43.52-62.41-5.97-18.47-8.96-52.06-8.96-100.75v-126.78Z" style="fill:#fd4b2d;"/><path d="M796.76,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M999.76,7.84h75.85v148.33c14.93-12.88,29.95-22.53,45.06-28.97,15.11-6.44,30.41-9.65,45.9-9.65,30.23,0,55.7,10.45,76.41,31.34,17.73,18.1,26.59,44.69,26.59,79.76v201.23h-75.29v-133.5c0-35.27-1.68-59.15-5.04-71.65-3.36-12.5-9.09-21.83-17.21-27.99-8.12-6.15-18.15-9.23-30.09-9.23-15.49,0-28.78,5.13-39.88,15.39-11.11,10.26-18.8,24.26-23.09,41.98-2.24,9.14-3.36,30.04-3.36,62.69v122.3h-75.85V7.84Z" style="fill:#fd4b2d;"/><path d="M1688.63,299.74h-245.45c3.54,21.65,13.01,38.86,28.41,51.64,15.39,12.78,35.03,19.17,58.91,19.17,28.55,0,53.08-9.98,73.6-29.95l64.37,30.23c-16.05,22.77-35.26,39.6-57.65,50.52-22.39,10.91-48.98,16.37-79.76,16.37-47.77,0-86.67-15.06-116.71-45.2-30.04-30.13-45.06-67.87-45.06-113.21s14.97-85.03,44.92-115.73c29.95-30.69,67.49-46.04,112.65-46.04,47.95,0,86.95,15.35,116.99,46.04,30.04,30.69,45.06,71.23,45.06,121.61l-.28,14.55ZM1612.22,239.57c-5.05-16.98-15-30.79-29.86-41.42-14.86-10.63-32.1-15.95-51.72-15.95-21.3,0-40,5.98-56.07,17.91-10.09,7.47-19.44,20.62-28.03,39.46h165.68Z" style="fill:#fd4b2d;"/><path d="M1790.6,125.38h76.41v31.21c17.33-14.61,33.02-24.77,47.09-30.48,14.06-5.71,28.46-8.57,43.18-8.57,30.18,0,55.8,10.54,76.85,31.62,17.7,17.91,26.55,44.41,26.55,79.48v201.23h-75.57v-133.35c0-36.34-1.63-60.47-4.89-72.4-3.26-11.93-8.93-21.01-17.03-27.26-8.1-6.24-18.1-9.36-30.01-9.36-15.45,0-28.71,5.17-39.78,15.51-11.08,10.35-18.76,24.65-23.04,42.91-2.24,9.5-3.35,30.1-3.35,61.78v122.16h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2183.92,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M2416.46,0c13.39,0,24.88,4.85,34.46,14.55,9.58,9.7,14.38,21.46,14.38,35.27s-4.75,25.24-14.24,34.84c-9.49,9.61-20.84,14.41-34.04,14.41s-25.16-4.9-34.75-14.69c-9.58-9.79-14.37-21.69-14.37-35.68s4.74-24.91,14.23-34.43c9.49-9.51,20.93-14.27,34.33-14.27ZM2378.26,125.38h76.41v304.5h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2564.75,7.84h76.41v243.09l112.51-125.54h95.96l-131.17,145.94,146.86,158.56h-94.85l-129.3-140.34v140.34h-76.41V7.84Z" style="fill:#fd4b2d;"/></g></symbol></defs><use width="998.94" height="763.82" transform="translate(28.54 36.14) scale(.68)" xlink:href="#a"/><use width="2865.3" height="437.72" transform="translate(802.22 67.81)" xlink:href="#c"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="h" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><g id="i"><g style="isolation:isolate;"><path d="M97.01,790.9h26.11v104.07h-26.11v-11c-5.1,4.85-10.21,8.34-15.34,10.47-5.13,2.14-10.68,3.2-16.67,3.2-13.44,0-25.06-5.21-34.87-15.64-9.81-10.43-14.71-23.39-14.71-38.88s4.75-29.24,14.24-39.5c9.49-10.27,21.02-15.4,34.59-15.4,6.24,0,12.1,1.18,17.58,3.54,5.48,2.36,10.54,5.9,15.19,10.62v-11.48ZM69.56,812.32c-8.07,0-14.78,2.86-20.12,8.56-5.34,5.71-8.01,13.03-8.01,21.95s2.72,16.39,8.15,22.19c5.44,5.8,12.12,8.7,20.07,8.7s15-2.85,20.4-8.56c5.4-5.71,8.1-13.18,8.1-22.43s-2.7-16.39-8.1-22c-5.4-5.61-12.23-8.42-20.5-8.42Z" style="fill:#fd4b2d;"/><path d="M153.21,790.9h26.4v50.12c0,9.76.67,16.53,2.01,20.33,1.34,3.79,3.5,6.74,6.47,8.85,2.97,2.1,6.63,3.16,10.98,3.16s8.04-1.04,11.08-3.11c3.04-2.07,5.29-5.12,6.76-9.14,1.09-3,1.63-9.41,1.63-19.23v-50.98h26.11v44.1c0,18.17-1.44,30.61-4.3,37.31-3.51,8.16-8.67,14.43-15.5,18.8-6.83,4.37-15.5,6.55-26.02,6.55-11.42,0-20.65-2.55-27.69-7.65-7.05-5.1-12.01-12.21-14.87-21.33-2.04-6.31-3.06-17.79-3.06-34.44v-43.33Z" style="fill:#fd4b2d;"/><path d="M287.73,752.54h26.11v38.36h15.5v22.48h-15.5v81.59h-26.11v-81.59h-13.39v-22.48h13.39v-38.36Z" style="fill:#fd4b2d;"/><path d="M357.11,750.72h25.92v50.7c5.1-4.4,10.23-7.7,15.4-9.9s10.39-3.3,15.69-3.3c10.33,0,19.04,3.57,26.11,10.71,6.06,6.19,9.09,15.27,9.09,27.26v68.78h-25.73v-45.63c0-12.05-.57-20.21-1.72-24.49-1.15-4.27-3.11-7.46-5.88-9.57-2.77-2.1-6.2-3.16-10.28-3.16-5.29,0-9.84,1.75-13.63,5.26-3.8,3.51-6.43,8.29-7.89,14.35-.77,3.13-1.15,10.27-1.15,21.43v41.8h-25.92v-144.25Z" style="fill:#fd4b2d;"/><path d="M592.55,850.49h-83.89c1.21,7.4,4.45,13.28,9.71,17.65,5.26,4.37,11.97,6.55,20.14,6.55,9.76,0,18.14-3.41,25.16-10.23l22,10.33c-5.48,7.78-12.05,13.54-19.7,17.27-7.65,3.73-16.74,5.6-27.26,5.6-16.33,0-29.62-5.15-39.89-15.45-10.27-10.3-15.4-23.2-15.4-38.69s5.12-29.06,15.35-39.55c10.24-10.49,23.07-15.73,38.5-15.73,16.39,0,29.72,5.25,39.98,15.73,10.27,10.49,15.4,24.34,15.4,41.56l-.1,4.97ZM566.44,829.92c-1.72-5.8-5.13-10.52-10.2-14.16-5.08-3.63-10.97-5.45-17.68-5.45-7.28,0-13.67,2.04-19.16,6.12-3.45,2.55-6.64,7.05-9.58,13.49h56.63Z" style="fill:#fd4b2d;"/><path d="M627.41,790.9h26.11v10.67c5.92-4.99,11.29-8.46,16.09-10.42,4.81-1.95,9.73-2.93,14.76-2.93,10.32,0,19.07,3.6,26.27,10.81,6.05,6.12,9.07,15.18,9.07,27.17v68.78h-25.83v-45.57c0-12.42-.56-20.67-1.67-24.74-1.11-4.08-3.05-7.18-5.82-9.32-2.77-2.13-6.19-3.2-10.26-3.2-5.28,0-9.81,1.77-13.6,5.3-3.79,3.54-6.41,8.43-7.87,14.67-.76,3.25-1.14,10.29-1.14,21.11v41.75h-26.11v-104.07Z" style="fill:#fd4b2d;"/><path d="M761.83,752.54h26.11v38.36h15.5v22.48h-15.5v81.59h-26.11v-81.59h-13.39v-22.48h13.39v-38.36Z" style="fill:#fd4b2d;"/><path d="M841.31,748.04c4.58,0,8.5,1.66,11.78,4.97,3.28,3.32,4.91,7.33,4.91,12.05s-1.62,8.63-4.87,11.91c-3.24,3.29-7.12,4.93-11.64,4.93s-8.6-1.67-11.88-5.02c-3.28-3.35-4.91-7.41-4.91-12.2s1.62-8.51,4.86-11.77c3.24-3.25,7.15-4.88,11.73-4.88ZM828.25,790.9h26.11v104.07h-26.11v-104.07Z" style="fill:#fd4b2d;"/><path d="M891.99,750.72h26.11v83.08l38.45-42.91h32.8l-44.83,49.88,50.19,54.19h-32.42l-44.19-47.96v47.96h-26.11v-144.25Z" style="fill:#fd4b2d;"/></g></g><path d="M689.34,93.81h-329.14c-72.05,0-131,58.95-131,131v329.14c0,72.05,58.95,131,131,131h38.83v-128.22h251.49v128.22h38.81c72.05,0,131-58.95,131-131V224.81c0-72.05-58.95-131-131-131ZM632.3,452.55H229.41v-88.61h208.16v-123.03h33.8v73.29h19.82v-73.29h23.9v53.9h19.82v-53.9h23.9v90.04h19.82v-90.04h33.8v123.03h19.88v88.61Z" style="fill:#fd4b2d;"/><g id="j"><path d="M230.92,358.64h-.02c-19.61-26.52-49.01-53.52-81.58-53.52-37.46.03-71.89,20.57-89.7,53.52-37.21,64.9,14.23,152.42,89.7,150.42,58.4,0,106.56-86.64,106.56-101.97,0-6.78-9.42-27.46-24.96-48.46ZM107.07,358.64c21.02-18.14,51.54-23.42,73.6,0h.02c16.65,15.1,31.18,37.08,36.44,48.26-65.47,136.9-180.22,20.22-110.06-48.26Z" style="fill:#fd4b2d;"/></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

117
web/package-lock.json generated
View File

@@ -24,7 +24,6 @@
"@formatjs/intl-listformat": "^8.3.5",
"@fortawesome/fontawesome-free": "^7.2.0",
"@goauthentik/api": "0.0.0",
"@goauthentik/brand-assets": "^2.0.0",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^2.0.1",
"@goauthentik/eslint-config": "^1.3.0",
@@ -58,7 +57,7 @@
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"@typescript-eslint/utils": "^8.57.2",
"@typescript/native-preview": "^7.0.0-dev.20260506.1",
"@typescript/native-preview": "^7.0.0-dev.20260421.2",
"@vitest/browser": "^4.1.6",
"@vitest/browser-playwright": "^4.1.6",
"@webcomponents/webcomponentsjs": "^2.8.0",
@@ -80,7 +79,7 @@
"globals": "^17.6.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"knip": "^6.12.0",
"knip": "^6.11.0",
"lex": "^2025.11.0",
"lit": "^3.3.2",
"lit-analyzer": "^2.0.3",
@@ -1552,12 +1551,6 @@
"resolved": "packages/client-ts",
"link": true
},
"node_modules/@goauthentik/brand-assets": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@goauthentik/brand-assets/-/brand-assets-2.0.0.tgz",
"integrity": "sha512-yRJrV+KuGrz7MNcRzAkZa4e7LuciuFZBVSyPFRd/EndxgiqcFuFHyn+6tEurKNmianBNURhe2qm5ytoLFgEWFQ==",
"license": "UNLICENSED"
},
"node_modules/@goauthentik/core": {
"resolved": "packages/core",
"link": true
@@ -5868,30 +5861,27 @@
}
},
"node_modules/@typescript/native-preview": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-UcEslgHBaHYPAisVQcyARDfps7nKyugmUyXcsfE1HiHcVuvZ4tBJ5C93sG1FDeHWJ9skGQ68ec+Xsx086geAfg==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-CmajHI25HpVWE9R1XFoxr+cphJPxoYD3eFioQtAvXYkMFKnLdICMS9pXre9Pybizb75ejRxjKD5/CVG055rEIg==",
"license": "Apache-2.0",
"bin": {
"tsgo": "bin/tsgo.js"
},
"engines": {
"node": ">=16.20.0"
},
"optionalDependencies": {
"@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260506.1",
"@typescript/native-preview-darwin-x64": "7.0.0-dev.20260506.1",
"@typescript/native-preview-linux-arm": "7.0.0-dev.20260506.1",
"@typescript/native-preview-linux-arm64": "7.0.0-dev.20260506.1",
"@typescript/native-preview-linux-x64": "7.0.0-dev.20260506.1",
"@typescript/native-preview-win32-arm64": "7.0.0-dev.20260506.1",
"@typescript/native-preview-win32-x64": "7.0.0-dev.20260506.1"
"@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260421.2",
"@typescript/native-preview-darwin-x64": "7.0.0-dev.20260421.2",
"@typescript/native-preview-linux-arm": "7.0.0-dev.20260421.2",
"@typescript/native-preview-linux-arm64": "7.0.0-dev.20260421.2",
"@typescript/native-preview-linux-x64": "7.0.0-dev.20260421.2",
"@typescript/native-preview-win32-arm64": "7.0.0-dev.20260421.2",
"@typescript/native-preview-win32-x64": "7.0.0-dev.20260421.2"
}
},
"node_modules/@typescript/native-preview-darwin-arm64": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-dAd7qG2J508+4CRSuoEA0EUxViIedQ0D+8xKoZiM0EQHCwww8glWYCo72UTjcRZctS3QbJY3PtGSvo3nzL4oVw==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-fHv1r3ZmVo6zxuAIFmuX3w9QxbcauoG0SsWhmDwm6VmRubLlOJIcmTtlmV3JAb9oOnq8LuzZljzT7Q39fSMQDw==",
"cpu": [
"arm64"
],
@@ -5899,15 +5889,12 @@
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@typescript/native-preview-darwin-x64": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-1Q7Elncpuiozvx3HCTgFbSxNz2m2FIkO1QW5f15igcZDG3vMW4QglNflmXosc69bzYI7KfYZuaGX3yGzJkGbfg==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-KWTR6xbW9t+JS7D5DQIzo75pqVXVWUxF9PMv/+S6xsnOjCVd6g0ixHcFpFMJMKSUQpGPr8Z5f7b8ks6LHW01jg==",
"cpu": [
"x64"
],
@@ -5915,15 +5902,12 @@
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@typescript/native-preview-linux-arm": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-MfYn1p+aOorZ2Y+7sqLvSoAXPEz/RfKgHfeYO240Udco30B4oapm7Hsq2PsS9Z2Oth/RorGjY0jLP2OhnkY2Ig==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-BWLQO3nemLDSV5PoE5GPHe1dU9Dth77Kv8/cle9Ujcp4LhPo0KincdPqFH/qKeU/xvW25mgFueflZ1nc4rKuww==",
"cpu": [
"arm"
],
@@ -5931,15 +5915,12 @@
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@typescript/native-preview-linux-arm64": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-Q1W4DHplR2urmtPwoz9tw6XUGWRNXF+CIXJQ8ZpIZFj/OHgvTw8vkYkKFuaEao3lSjTsR4lQe/wL2Xr5K0hxuA==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-VLMEuml3BhUb+jaL0TXQ4xvVODxJF+RhkI+tBWvlynsJI4khTXEiwWh+wPOJrsfBRYFRMXEu28Odl/HXkYze8w==",
"cpu": [
"arm64"
],
@@ -5947,15 +5928,12 @@
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@typescript/native-preview-linux-x64": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-b+sbLBCIchbrGQNbjIvVN2qd+ieqqp/nghi0n2zOAKGPsfd5wG6ceqxWJKADdBDCohsCCGt//rZccUwFugIsyA==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-qUrJWTB5/wv4wnRG0TRXElAxc2kykNiRNyEIEqBbLmzDlrcvAW7RRy8MXoY1ZyTiKGMu14itZ3x9oW6+blFpRw==",
"cpu": [
"x64"
],
@@ -5963,15 +5941,12 @@
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@typescript/native-preview-win32-arm64": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-l59d8pZjFT7GoWpgCOy6aBcxLSALphA91X4Z/2XHo5HnM0bQ/yJjB7XMeUQZBdk5DZCdZL+sWTfmXLRggm7sFg==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-Rc6NsWlZmCs5YUKVzKgwoBOoRUGsPzct4BDMRX0csD1devLBBc4AbUXWKsJRbpwIAnqMO1ld4sNHEb+wXgfNHQ==",
"cpu": [
"arm64"
],
@@ -5979,15 +5954,12 @@
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@typescript/native-preview-win32-x64": {
"version": "7.0.0-dev.20260506.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260506.1.tgz",
"integrity": "sha512-dJDLSzaz2xjRYYmTSfcCepZUi3ITjQSJ6Gk5YGplMF57UmZCAGI+ns4Te/V74IJiQigXqTnyEIGorwsOqhW8gQ==",
"version": "7.0.0-dev.20260421.2",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260421.2.tgz",
"integrity": "sha512-GQv1+dya1t6EqF2Cpsb+xoozovdX10JUSf6Kl/8xNkTapzmlHd+uMr+8ku3jIASTxoRGn0Mklgjj3MDKrOTuLg==",
"cpu": [
"x64"
],
@@ -5995,10 +5967,7 @@
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
]
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
@@ -11978,9 +11947,9 @@
}
},
"node_modules/knip": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/knip/-/knip-6.12.0.tgz",
"integrity": "sha512-nRg8+DOFcfBD6NjmNzu9+3D35QnEmMsnojJGOHQUqv+70r1aOx99wpSUXvEV7syQVOL5E6tNXXkoyG1Fuz8BWg==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/knip/-/knip-6.11.0.tgz",
"integrity": "sha512-84PTlN8Q5smLpTbzs8smTVh8PMbTDXtw0tFksXq/m6auGFC/KSzJykKFmnYh3As38kiWDkoDBvdTTyKk5M1TAQ==",
"funding": [
{
"type": "github",

View File

@@ -99,7 +99,6 @@
"@formatjs/intl-listformat": "^8.3.5",
"@fortawesome/fontawesome-free": "^7.2.0",
"@goauthentik/api": "0.0.0",
"@goauthentik/brand-assets": "^2.0.0",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^2.0.1",
"@goauthentik/eslint-config": "^1.3.0",
@@ -133,7 +132,7 @@
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"@typescript-eslint/utils": "^8.57.2",
"@typescript/native-preview": "^7.0.0-dev.20260506.1",
"@typescript/native-preview": "^7.0.0-dev.20260421.2",
"@vitest/browser": "^4.1.6",
"@vitest/browser-playwright": "^4.1.6",
"@webcomponents/webcomponentsjs": "^2.8.0",
@@ -155,7 +154,7 @@
"globals": "^17.6.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"knip": "^6.12.0",
"knip": "^6.11.0",
"lex": "^2025.11.0",
"lit": "^3.3.2",
"lit-analyzer": "^2.0.3",

View File

@@ -23,10 +23,16 @@ export default defineConfig({
testDir: "./test/browser",
fullyParallel: true,
forbidOnly: CI,
retries: CI ? 2 : 0,
workers: CI ? 1 : undefined,
retries: CI ? 1 : 0,
workers: "50%",
maxFailures: CI ? 5 : 2,
reporter: CI
? "github"
? [
// ---
["github"],
["html", { open: "never", outputFolder: "playwright-report" }],
["json", { outputFile: "playwright-report/results.json" }],
]
: [
// ---
["list", { printSteps: true }],
@@ -36,6 +42,8 @@ export default defineConfig({
testIdAttribute: "data-test-id",
baseURL,
trace: "on-first-retry",
screenshot: "only-on-failure",
video: CI ? "retain-on-failure" : "off",
colorScheme: "dark",
launchOptions: {
logger: {

View File

@@ -1,75 +0,0 @@
import "@goauthentik/core/environment/load/node";
import * as fs from "node:fs/promises";
import { createRequire } from "node:module";
import * as path from "node:path";
import { ConsoleLogger } from "#logger/node";
import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node";
const require = createRequire(import.meta.url);
const logger = ConsoleLogger.child({ name: "Assets" });
/**
* @typedef {[from: string, to: string]} SourceDestinationPair
*/
/**
* @type {SourceDestinationPair[]}
*/
const assets = [
[
path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup"),
path.dirname(EntryPoint.StandaloneLoading.out),
],
[path.resolve(PackageRoot, "src", "assets", "images"), "./assets/images"],
[require.resolve("@goauthentik/brand-assets/brand.png"), "./assets/icons/brand.png"],
[require.resolve("@goauthentik/brand-assets/brand.svg"), "./assets/icons/brand.svg"],
[require.resolve("@goauthentik/brand-assets/icon.png"), "./assets/icons/icon.png"],
[require.resolve("@goauthentik/brand-assets/icon.svg"), "./assets/icons/icon.svg"],
[
require.resolve("@goauthentik/brand-assets/icon_left_brand.png"),
"./assets/icons/icon_left_brand.png",
],
[
require.resolve("@goauthentik/brand-assets/icon_left_brand.svg"),
"./assets/icons/icon_left_brand.svg",
],
[
require.resolve("@goauthentik/brand-assets/icon_pride_lgbt.png"),
"./assets/icons/icon_pride_lgbt.png",
],
[
require.resolve("@goauthentik/brand-assets/icon_pride_trans.png"),
"./assets/icons/icon_pride_trans.png",
],
[
require.resolve("@goauthentik/brand-assets/icon_top_brand.png"),
"./assets/icons/icon_top_brand.png",
],
[
require.resolve("@goauthentik/brand-assets/icon_top_brand.svg"),
"./assets/icons/icon_top_brand.svg",
],
];
export async function copyAssets() {
/**
* @param {SourceDestinationPair} pair
*/
const copy = ([from, to]) => {
const resolvedDestination = path.resolve(DistDirectory, to);
logger.debug(`📋 Copying assets from ${from} to ${to}`);
return fs
.cp(from, resolvedDestination, {
recursive: true,
})
.catch((error) => {
logger.error(`Failed to copy assets from ${from} to ${to}: ${error}`);
});
};
return await Promise.all(assets.map(copy));
}

View File

@@ -7,8 +7,6 @@ import "@goauthentik/core/environment/load/node";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { copyAssets } from "./build-assets.mjs";
/**
* @file ESBuild script for building the authentik web UI.
*
@@ -37,6 +35,22 @@ const publicBundledDefinitions = Object.fromEntries(
);
logger.info(publicBundledDefinitions, "Bundle definitions");
/**
* @typedef {[from: string, to: string]} SourceDestinationPair
*/
/**
* @type {SourceDestinationPair[]}
*/
const assets = [
[
path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup"),
path.dirname(EntryPoint.StandaloneLoading.out),
],
[path.resolve(PackageRoot, "src", "assets", "images"), "./assets/images"],
[path.resolve(PackageRoot, "icons"), "./assets/icons"],
];
const entryPointNames = Object.keys(EntryPoint);
const entryPoints = Object.values(EntryPoint);
const entryPointsDescription = entryPointNames.join("\n\t");
@@ -54,7 +68,29 @@ const BASE_ESBUILD_PLUGINS = [
*/
const errors = [];
await copyAssets();
/**
* @param {SourceDestinationPair} pair
*/
const copy = ([from, to]) => {
const resolvedDestination = path.resolve(DistDirectory, to);
logger.debug(`📋 Copying assets from ${from} to ${to}`);
return fs
.cp(from, resolvedDestination, {
recursive: true,
})
.catch((error) => {
errors.push({
text: `Failed to copy assets from ${from} to ${to}: ${error}`,
location: {
file: from,
},
});
});
};
await Promise.all(assets.map(copy));
return { errors };
});

View File

@@ -15,7 +15,6 @@ import {
availableHashes,
DEFAULT_HASH_ALGORITHM,
digestAlgorithmOptions,
logoutMethodOptions,
retrieveSignatureAlgorithm,
SAMLSupportedKeyTypes,
} from "./SAMLProviderOptions.js";
@@ -30,6 +29,7 @@ import {
PropertymappingsApi,
PropertymappingsProviderSamlListRequest,
SAMLBindingsEnum,
SAMLLogoutMethods,
SAMLNameIDPolicyEnum,
SAMLPropertyMapping,
SAMLProvider,
@@ -90,6 +90,23 @@ function renderHasSlsUrl(
logoutMethod: string,
setLogoutMethod?: (ev: Event) => void,
) {
const logoutMethodOptions: RadioOption<string>[] = [
{
label: msg("Front-channel (Iframe)"),
value: SAMLLogoutMethods.FrontchannelIframe,
default: true,
},
{
label: msg("Front-channel (Native)"),
value: SAMLLogoutMethods.FrontchannelNative,
},
{
label: msg("Back-channel (POST)"),
value: SAMLLogoutMethods.Backchannel,
disabled: !hasPostBinding,
},
];
return html`<ak-radio-input
label=${msg("SLS Binding")}
name="slsBinding"
@@ -104,7 +121,7 @@ function renderHasSlsUrl(
<ak-radio-input
label=${msg("Logout Method")}
name="logoutMethod"
.options=${logoutMethodOptions(hasPostBinding)}
.options=${logoutMethodOptions}
.value=${logoutMethod}
help=${msg("Method to use for logout when SLS URL is configured.")}
@change=${setLogoutMethod}

View File

@@ -2,7 +2,6 @@ import {
DigestAlgorithmEnum,
KeyTypeEnum,
SAMLBindingsEnum,
SAMLLogoutMethods,
SignatureAlgorithmEnum,
} from "@goauthentik/api";
@@ -23,38 +22,6 @@ export const spBindingOptions = toOptions([
[msg("Post"), SAMLBindingsEnum.Post],
]);
export function logoutMethodLabel(method?: SAMLLogoutMethods | string): string {
switch (method) {
case SAMLLogoutMethods.FrontchannelIframe:
return msg("Front-channel (Iframe)");
case SAMLLogoutMethods.FrontchannelNative:
return msg("Front-channel (Native)");
case SAMLLogoutMethods.Backchannel:
return msg("Back-channel (POST)");
default:
return method ?? "";
}
}
export function logoutMethodOptions(hasPostBinding: boolean) {
return [
{
label: logoutMethodLabel(SAMLLogoutMethods.FrontchannelIframe),
value: SAMLLogoutMethods.FrontchannelIframe,
default: true,
},
{
label: logoutMethodLabel(SAMLLogoutMethods.FrontchannelNative),
value: SAMLLogoutMethods.FrontchannelNative,
},
{
label: logoutMethodLabel(SAMLLogoutMethods.Backchannel),
value: SAMLLogoutMethods.Backchannel,
disabled: !hasPostBinding,
},
];
}
export const digestAlgorithmOptions = toOptions([
["SHA1", DigestAlgorithmEnum.HttpWwwW3Org200009Xmldsigsha1],
["SHA256", DigestAlgorithmEnum.HttpWwwW3Org200104Xmlencsha256, true],

View File

@@ -9,8 +9,6 @@ import "#elements/buttons/ActionButton/index";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/index";
import { logoutMethodLabel } from "./SAMLProviderOptions.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { MessageLevel } from "#common/messages";
@@ -154,13 +152,6 @@ export class SAMLProviderViewPage extends AKElement {
}
}
renderLogoutMethod(): string {
if (!this.provider?.slsUrl) {
return "-";
}
return logoutMethodLabel(this.provider.logoutMethod) || "-";
}
renderRelatedObjects(): TemplateResult {
const relatedObjects = [];
if (this.provider?.assignedApplicationName) {
@@ -327,18 +318,6 @@ export class SAMLProviderViewPage extends AKElement {
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
"Audience",
)}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.audience || "-"}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
@@ -354,24 +333,24 @@ export class SAMLProviderViewPage extends AKElement {
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
"SLS URL",
"Audience",
)}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.slsUrl || "-"}
${this.provider.audience || "-"}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
"Logout Method",
"Issuer",
)}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.renderLogoutMethod()}
${this.provider.issuerOverride}
</div>
</dd>
</div>

View File

@@ -18,7 +18,7 @@ import { RoleForm } from "#admin/roles/ak-role-form";
import { RbacApi, Role } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { html, PropertyValues, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
@@ -83,7 +83,11 @@ export class RoleListPage extends TablePage<Role> {
row(item: Role): SlottedTemplateResult[] {
return [
html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`,
html`<a
href="#/identity/roles/${item.pk}"
aria-label=${msg(str`View details of role "${item.name}"`)}
>${item.name}</a
>`,
html`<div class="ak-c-table__actions">${IconEditButton(RoleForm, item.pk)}</div>`,
];
}

View File

@@ -14,11 +14,9 @@ import { SlottedTemplateResult } from "#elements/types";
import { BaseStageForm } from "#admin/stages/BaseStageForm";
import {
CAPTCHA_PROVIDERS,
CAPTCHA_REQUEST_CONTENT_TYPES,
CaptchaProviderKey,
CaptchaProviderKeys,
CaptchaProviderPreset,
deriveCapSiteVerifyURL,
detectProviderFromInstance,
pluckFormValues,
} from "#admin/stages/captcha/shared";
@@ -85,15 +83,6 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
public async send(
data: CaptchaStageRequest | PatchedCaptchaStageRequest,
): Promise<CaptchaStage> {
if (this.selectedProvider === "cap" && data.publicKey) {
const presetURL = CAPTCHA_PROVIDERS.cap.apiUrl;
// The Cap verification URL includes the site key, so derive it from the
// widget endpoint unless the advanced field was explicitly customized.
if (!data.apiUrl || data.apiUrl === presetURL) {
data.apiUrl = deriveCapSiteVerifyURL(data.publicKey);
}
}
if (this.instance) {
return this.#api.stagesCaptchaPartialUpdate({
stageUuid: this.instance.pk || "",
@@ -151,34 +140,20 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
}
protected renderKeyFields(): SlottedTemplateResult {
const publicKeyLabel =
this.selectedProvider === "cap" ? msg("API Endpoint") : msg("Public Key");
const publicKeyPlaceholder =
this.selectedProvider === "cap"
? msg("https://cap.example.com/site-key/")
: msg("Paste your CAPTCHA public key...");
const publicKeyHelp =
this.selectedProvider === "cap"
? msg("The public Cap endpoint used by the widget, including the site key path.", {
id: "captcha.cap-endpoint.description",
desc: "Description for Cap endpoint field.",
})
: msg("The public key is used by authentik to render the CAPTCHA widget.", {
id: "captcha.public-key.description",
desc: "Description for CAPTCHA public key field.",
});
return html`
<ak-text-input
label=${publicKeyLabel}
label=${msg("Public Key")}
required
name="publicKey"
type="text"
value="${ifDefined(this.instance?.publicKey || "")}"
autocomplete="off"
input-hint="code"
placeholder=${publicKeyPlaceholder}
help=${publicKeyHelp}
placeholder=${msg("Paste your CAPTCHA public key...")}
help=${msg("The public key is used by authentik to render the CAPTCHA widget.", {
id: "captcha.public-key.description",
desc: "Description for CAPTCHA public key field.",
})}
>
</ak-text-input>
@@ -271,35 +246,10 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
type="url"
value="${ifDefined(formValues.apiUrl)}"
required
help=${this.selectedProvider === "cap"
? msg(
"Cap's server-side verification endpoint, for example https://cap.example.com/site-key/siteverify.",
)
: msg(
"URL used to validate CAPTCHA response on the backend. Automatically set based on provider selection but can be customized.",
)}
help=${msg(
"URL used to validate CAPTCHA response on the backend. Automatically set based on provider selection but can be customized.",
)}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Request Content Type")}
name="requestContentType"
>
<select class="pf-c-form-control" name="requestContentType">
${CAPTCHA_REQUEST_CONTENT_TYPES.map(
(type) =>
html`<option
value=${type.value}
?selected=${type.value === formValues.requestContentType}
>
${type.formatDisplayName()}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${msg(
"Content-Type used for server-side verification. Cap requires JSON; most other providers use form-encoded requests.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}

View File

@@ -2,35 +2,12 @@ import { CaptchaStage, CaptchaStageRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
export type CaptchaRequestContentType = "application/x-www-form-urlencoded" | "application/json";
export const CAPTCHA_REQUEST_CONTENT_TYPES = [
{
value: "application/x-www-form-urlencoded",
formatDisplayName: () =>
msg("Form encoded", {
id: "captcha.request-content-type.form",
}),
},
{
value: "application/json",
formatDisplayName: () =>
msg("JSON", {
id: "captcha.request-content-type.json",
}),
},
] as const satisfies {
value: CaptchaRequestContentType;
formatDisplayName: () => string;
}[];
export const CaptchaProviderKeys = [
"recaptcha_v2",
"recaptcha_v3",
"recaptcha_enterprise",
"hcaptcha",
"turnstile",
"cap",
"custom",
] as const satisfies string[];
@@ -40,7 +17,6 @@ export interface CaptchaProviderPreset {
formatDisplayName: () => string;
jsUrl: string;
apiUrl: string;
requestContentType: CaptchaRequestContentType;
interactive: boolean;
supportsScore: boolean;
score?: { min: number; max: number };
@@ -61,7 +37,6 @@ export const CAPTCHA_PROVIDERS = {
}),
jsUrl: "https://www.recaptcha.net/recaptcha/api.js",
apiUrl: "https://www.recaptcha.net/recaptcha/api/siteverify",
requestContentType: "application/x-www-form-urlencoded",
interactive: true,
supportsScore: false,
formatAPISource: () =>
@@ -77,7 +52,6 @@ export const CAPTCHA_PROVIDERS = {
}),
jsUrl: "https://www.recaptcha.net/recaptcha/api.js",
apiUrl: "https://www.recaptcha.net/recaptcha/api/siteverify",
requestContentType: "application/x-www-form-urlencoded",
interactive: false,
supportsScore: true,
score: { min: 0.5, max: 1.0 },
@@ -94,7 +68,6 @@ export const CAPTCHA_PROVIDERS = {
}),
jsUrl: "https://www.recaptcha.net/recaptcha/enterprise.js",
apiUrl: "https://www.recaptcha.net/recaptcha/api/siteverify",
requestContentType: "application/x-www-form-urlencoded",
interactive: false,
supportsScore: true,
score: { min: 0.5, max: 1.0 },
@@ -111,7 +84,6 @@ export const CAPTCHA_PROVIDERS = {
}),
jsUrl: "https://js.hcaptcha.com/1/api.js",
apiUrl: "https://api.hcaptcha.com/siteverify",
requestContentType: "application/x-www-form-urlencoded",
interactive: true,
supportsScore: true,
score: { min: 0.0, max: 0.5 },
@@ -128,7 +100,6 @@ export const CAPTCHA_PROVIDERS = {
}),
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
apiUrl: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
requestContentType: "application/x-www-form-urlencoded",
interactive: true,
supportsScore: false,
formatAPISource: () =>
@@ -137,22 +108,6 @@ export const CAPTCHA_PROVIDERS = {
}),
keyURL: "https://dash.cloudflare.com",
},
cap: {
formatDisplayName: () =>
msg("Cap", {
id: "captcha.providers.cap",
}),
jsUrl: "https://cdn.jsdelivr.net/npm/cap-widget",
apiUrl: "https://cap.example.com/site-key/siteverify",
requestContentType: "application/json",
interactive: true,
supportsScore: false,
formatAPISource: () =>
msg("Cap setup guide", {
id: "captcha.providers.cap.setup-guide",
}),
keyURL: "https://capjs.js.org/guide/",
},
custom: {
formatDisplayName: () =>
msg("Custom", {
@@ -160,19 +115,12 @@ export const CAPTCHA_PROVIDERS = {
}),
jsUrl: "https://www.recaptcha.net/recaptcha/api.js",
apiUrl: "https://www.recaptcha.net/recaptcha/api/siteverify",
requestContentType: "application/x-www-form-urlencoded",
interactive: false,
supportsScore: true,
score: { min: 0.5, max: 1.0 },
},
} as const satisfies Record<CaptchaProviderKey, CaptchaProviderPreset>;
export function deriveCapSiteVerifyURL(endpoint: string): string {
const normalizedEndpoint = endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
return new URL("siteverify", normalizedEndpoint).toString();
}
/**
* Detect which provider preset matches the given {@linkcode CaptchaStage} instance.
* This allows the form to show the correct provider in the dropdown when editing
@@ -184,14 +132,6 @@ export function detectProviderFromInstance(stage?: CaptchaStage | null): Captcha
for (const key of CaptchaProviderKeys) {
const preset = CAPTCHA_PROVIDERS[key];
if (
key === "cap" &&
stage.jsUrl === preset.jsUrl &&
stage.requestContentType === preset.requestContentType
) {
return key;
}
if (stage.jsUrl === preset.jsUrl && stage.apiUrl === preset.apiUrl) {
return key;
}
@@ -213,7 +153,6 @@ export function pluckFormValues(
return {
jsUrl: instance.jsUrl,
apiUrl: instance.apiUrl,
requestContentType: instance.requestContentType,
interactive: instance.interactive,
scoreMinThreshold: instance.scoreMinThreshold,
scoreMaxThreshold: instance.scoreMaxThreshold,
@@ -224,7 +163,6 @@ export function pluckFormValues(
return {
jsUrl: preset.jsUrl,
apiUrl: preset.apiUrl,
requestContentType: preset.requestContentType,
interactive: preset.interactive,
scoreMinThreshold: preset.score?.min ?? 0.5,
scoreMaxThreshold: preset.score?.max ?? 1.0,

View File

@@ -7,7 +7,6 @@ import "#components/ak-radio-input";
import "#components/ak-switch-input";
import { DEFAULT_CONFIG } from "#common/api/config";
import { DefaultUIConfig } from "#common/ui/config";
import { ModelForm } from "#elements/forms/ModelForm";
import { RadioOption } from "#elements/forms/Radio";
@@ -59,8 +58,8 @@ export class UserForm extends ModelForm<User, number> {
@property({ attribute: false })
public targetRole: Role | null = null;
@property({ type: String, attribute: "default-path", useDefault: true })
public defaultPath: string = DefaultUIConfig.defaults.userPath;
@property({ type: String, attribute: "default-path" })
public defaultPath: string = "users";
@property({ attribute: false })
public userType: UserTypeEnum | null = null;

View File

@@ -91,15 +91,15 @@ export class UserListPage extends WithLicenseSummary(
public override searchPlaceholder = msg("Search by username, email, etc...");
public override searchLabel = msg("User Search");
public override pageTitle = msg("Users");
public override pageDescription = "";
public override pageIcon = "pf-icon pf-icon-user";
public pageTitle = msg("Users");
public pageDescription = "";
public pageIcon = "pf-icon pf-icon-user";
@property({ type: String })
public order = "-last_login";
@property({ type: String, useDefault: true })
public activePath: string = DefaultUIConfig.defaults.userPath;
@property({ type: String })
public activePath: string;
@state()
protected hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
@@ -107,23 +107,27 @@ export class UserListPage extends WithLicenseSummary(
@state()
protected userPaths: UserPath | null = null;
constructor() {
super();
const defaultPath = DefaultUIConfig.defaults.userPath;
this.activePath = getURLParam<string>("path", defaultPath);
if (this.uiConfig.defaults.userPath !== defaultPath) {
this.activePath = this.uiConfig.defaults.userPath;
}
}
protected canImpersonate = false;
public override connectedCallback(): void {
super.connectedCallback();
this.canImpersonate = this.can(CapabilitiesEnum.CanImpersonate);
const initialDefaultUserPath = DefaultUIConfig.defaults.userPath;
const brandDefaultUserPath = this.uiConfig.defaults.userPath;
this.activePath = getURLParam<string>(
"path",
brandDefaultUserPath || initialDefaultUserPath,
);
}
protected override async apiEndpoint(): Promise<PaginatedResponse<User>> {
async apiEndpoint(): Promise<PaginatedResponse<User>> {
const users = await this.#api.coreUsersList({
...(await this.defaultEndpointConfig()),
pathStartswith: this.activePath,
@@ -138,18 +142,6 @@ export class UserListPage extends WithLicenseSummary(
return users;
}
protected buildExportParams = async (): Promise<CoreUsersExportCreateRequest> => {
return {
...(await this.defaultEndpointConfig()),
pathStartswith: this.activePath,
isActive: this.hideDeactivated ? true : undefined,
};
};
protected createExport = (params: CoreUsersExportCreateRequest) => {
return this.#api.coreUsersExportCreate(params);
};
protected override rowLabel(item: User): string {
if (item.name) {
return msg(str`${item.username} (${item.name})`);
@@ -167,8 +159,6 @@ export class UserListPage extends WithLicenseSummary(
[msg("Actions"), null, msg("Row Actions")],
];
//#region Renderering
protected override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
const { currentUser, originalUser } = this;
@@ -259,7 +249,7 @@ export class UserListPage extends WithLicenseSummary(
</div>`;
}
protected override row(item: User): SlottedTemplateResult[] {
protected row(item: User) {
const { currentUser } = this;
const showImpersonation = this.canImpersonate && currentUser && item.pk !== currentUser.pk;
@@ -304,7 +294,7 @@ export class UserListPage extends WithLicenseSummary(
];
}
protected override renderExpanded(item: User): SlottedTemplateResult {
renderExpanded(item: User): TemplateResult {
return html`<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
@@ -345,6 +335,18 @@ export class UserListPage extends WithLicenseSummary(
</dl>`;
}
protected buildExportParams = async () => {
return {
...(await this.defaultEndpointConfig()),
pathStartswith: this.activePath,
isActive: this.hideDeactivated ? true : undefined,
};
};
protected createExport = (params: CoreUsersExportCreateRequest) => {
return this.#api.coreUsersExportCreate(params);
};
protected renderObjectCreate(): SlottedTemplateResult {
const { activePath } = this;
@@ -368,7 +370,7 @@ export class UserListPage extends WithLicenseSummary(
});
}
protected renderSidebarBefore(): SlottedTemplateResult {
protected renderSidebarBefore(): TemplateResult {
return html`<aside aria-labelledby="sidebar-left-panel-header" class="pf-c-sidebar__panel">
<div class="pf-c-card tree">
<div
@@ -392,8 +394,6 @@ export class UserListPage extends WithLicenseSummary(
</div>
</aside>`;
}
//#endregion
}
declare global {

View File

@@ -6,8 +6,6 @@ import "#elements/wizard/FormWizardPage";
import "#elements/wizard/TypeCreateWizardPage";
import "#elements/wizard/Wizard";
import { DefaultUIConfig } from "#common/ui/config";
import { LitPropertyRecord, SlottedTemplateResult } from "#elements/types";
import { CreateWizard } from "#elements/wizard/CreateWizard";
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
@@ -123,8 +121,8 @@ export class AKUserWizard extends CreateWizard {
/**
* Default path to assign to new users created via the wizard.
*/
@property({ type: String, attribute: "default-path", useDefault: true })
public defaultPath: string = DefaultUIConfig.defaults.userPath;
@property({ type: String, attribute: "default-path" })
public defaultPath: string = "users";
protected apiEndpoint(): Promise<TypeCreate[]> {
return Promise.resolve(DEFAULT_USER_TYPES);

View File

@@ -1,6 +1,8 @@
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
import { globalAK } from "#common/global";
import { SlottedTemplateResult } from "#elements/types";
import { FlowUserDetails } from "#flow/FormStatic";
@@ -22,19 +24,6 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
static styles: CSSResult[] = [PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
getText(challenge: SessionEndChallenge) {
if (challenge.overviewUrl && challenge.invalidationFlowUrl) {
return msg(
str`You've logged out of ${challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`,
);
} else if (challenge.invalidationFlowUrl) {
return msg(
str`You've logged out of ${challenge.applicationName}. You can log out of your authentik account.`,
);
}
return msg(str`You've logged out of ${challenge.applicationName}.`);
}
protected render(): SlottedTemplateResult {
const { challenge } = this;
@@ -42,16 +31,18 @@ export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
return nothing;
}
return html`<ak-flow-card .challenge=${challenge}>
return html`<ak-flow-card .challenge=${this.challenge}>
<form class="pf-c-form">
${FlowUserDetails({ challenge: challenge })}
${FlowUserDetails({ challenge: this.challenge })}
<p>${this.getText(challenge)}</p>
${challenge.overviewUrl
? html`<a href="${challenge.overviewUrl}" class="pf-c-button pf-m-primary">
${msg("Go back to overview")}
</a>`
: nothing}
<p>
${msg(
str`You've logged out of ${challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`,
)}
</p>
<a href="${globalAK().api.base}" class="pf-c-button pf-m-primary">
${msg("Go back to overview")}
</a>
${challenge.invalidationFlowUrl
? html`
<a

View File

@@ -36,9 +36,4 @@ ak-stage-captcha[theme="dark"].style-scope {
background-color: var(--captcha-background-from);
animation: captcha-background-animation 1s infinite var(--pf-global--TimingFunction);
}
&[data-transparent-loading="true"][data-ready="loading"] {
background-color: transparent;
animation: none;
}
}

View File

@@ -12,7 +12,6 @@ import { AKFormErrors, ErrorProp } from "#components/ak-field-errors";
import { FlowUserDetails } from "#flow/FormStatic";
import { BaseStage } from "#flow/stages/base";
import Styles from "#flow/stages/captcha/CaptchaStage.css";
import { CapController } from "#flow/stages/captcha/controllers/cap";
import {
CaptchaController,
CaptchaControllerConstructor,
@@ -54,14 +53,7 @@ interface LoadMessage {
message: "load";
}
interface ErrorMessage {
source?: string;
context?: string;
message: "error";
error: string;
}
type IframeMessageEvent = MessageEvent<CaptchaMessage | LoadMessage | ErrorMessage>;
type IframeMessageEvent = MessageEvent<CaptchaMessage | LoadMessage>;
@customElement("ak-stage-captcha")
export class CaptchaStage
@@ -87,7 +79,6 @@ export class CaptchaStage
HCaptchaController,
GReCaptchaController,
TurnstileController,
CapController,
]);
#logger = ConsoleLogger.prefix("flow:captcha");
@@ -174,9 +165,6 @@ export class CaptchaStage
return match(data)
.with({ message: "captcha" }, ({ token }) => this.onTokenChange(token))
.with({ message: "load" }, this.#loadListener)
.with({ message: "error" }, ({ error }) => {
this.error = error;
})
.otherwise(({ message }) => {
this.#logger.debug(`Unknown message: ${message}`);
});
@@ -195,17 +183,12 @@ export class CaptchaStage
}
if (this.challenge?.interactive) {
// Cap renders its own framed widget, so the generic iframe loading shimmer looks like
// an extra CAPTCHA box flashing behind it.
const isCapChallenge = this.challenge.jsUrl.includes("cap-widget");
return html`
<iframe
aria-label=${msg("CAPTCHA challenge")}
${ref(this.iframeRef)}
style="height: ${this.iframeHeight}px;"
data-ready=${this.#iframeLoaded ? "ready" : "loading"}
data-transparent-loading=${isCapChallenge ? "true" : "false"}
class="ak-interactive-challenge"
id="ak-captcha"
></iframe>
@@ -323,13 +306,8 @@ export class CaptchaStage
// Then, load the new script...
const scriptElement = document.createElement("script");
const matchedController = Array.from(CaptchaStage.controllers).find((Controller) =>
Controller.matchesURL(challengeURL),
);
scriptElement.src = challengeURL.toString();
scriptElement.type =
matchedController?.scriptType === "module" ? "module" : "text/javascript";
scriptElement.async = true;
scriptElement.defer = true;
scriptElement.onload = this.#scriptLoadListener;
@@ -552,7 +530,6 @@ export class CaptchaStage
challengeURL: challengeURL.toString(),
theme: this.activeTheme,
scriptOnLoad: !(controller instanceof TurnstileController),
scriptType: controller.scriptType,
});
if (

View File

@@ -28,20 +28,6 @@ export abstract class CaptchaController implements ReactiveController {
return (this.constructor as typeof CaptchaController).globalName;
}
public static readonly scriptType: "classic" | "module" = "classic";
public get scriptType(): "classic" | "module" {
return (this.constructor as typeof CaptchaController).scriptType;
}
public static isAvailable(): boolean {
return Object.hasOwn(window, this.globalName);
}
public static matchesURL(_url: URL): boolean {
return false;
}
/**
* A prefix for log messages from this controller.
*/
@@ -56,7 +42,7 @@ export abstract class CaptchaController implements ReactiveController {
): Array<CaptchaControllerConstructor | undefined> {
return Array.from(controllerConstructors).filter((Controller) => {
// Can we find the global for this captcha provider?
return Controller.isAvailable();
return Object.hasOwn(window, Controller.globalName);
});
}
@@ -112,9 +98,6 @@ export abstract class CaptchaController implements ReactiveController {
export type CaptchaControllerConstructor = {
globalName: string;
scriptType: "classic" | "module";
isAvailable: () => boolean;
matchesURL: (url: URL) => boolean;
} & (new (host: CaptchaHandlerHost) => CaptchaController);
export interface CaptchaHandlerHost extends ReactiveControllerHost {

View File

@@ -1,57 +0,0 @@
import { CaptchaController } from "#flow/stages/captcha/controllers/CaptchaController";
import { html } from "lit";
export class CapController extends CaptchaController {
public static readonly globalName = "cap-widget";
public static readonly scriptType = "module";
public static override isAvailable(): boolean {
return customElements.get("cap-widget") !== undefined;
}
public static override matchesURL(url: URL): boolean {
return url.pathname.includes("cap-widget");
}
public interactive = () => {
const endpoint = this.host.challenge?.siteKey ?? "";
return html`<div id="ak-container" class="cap-container">
<cap-widget
id="ak-cap-widget"
required
data-cap-api-endpoint=${endpoint}
></cap-widget>
</div>
<script>
const widget = document.getElementById("ak-cap-widget");
widget.addEventListener("solve", (event) => {
callback(event.detail.token);
});
widget.addEventListener("error", (event) => {
self.parent.postMessage({
message: "error",
source: "goauthentik.io",
context: "flow-executor",
error: event.detail.message,
});
});
</script>`;
};
public refreshInteractive = async () => {
this.host.iframeRef.value?.contentWindow?.location.reload();
};
public execute = async () => {
throw new Error("Cap requires interactive mode.");
};
public refresh = async () => {
throw new Error("Cap requires interactive mode.");
};
}

View File

@@ -30,7 +30,6 @@ export interface IFrameTemplateInit {
* Defaults to `true`.
*/
scriptOnLoad?: boolean;
scriptType?: "classic" | "module";
}
/**
@@ -43,7 +42,7 @@ export interface IFrameTemplateInit {
*/
export function iframeTemplate(
children: TemplateResult,
{ challengeURL, theme, scriptOnLoad = true, scriptType = "classic" }: IFrameTemplateInit,
{ challengeURL, theme, scriptOnLoad = true }: IFrameTemplateInit,
) {
return createDocumentTemplate({
head: html`
@@ -76,7 +75,7 @@ export function iframeTemplate(
<style>
html,
body {
background: transparent;
background: ${ThemeColor[theme]};
}
body {
@@ -89,23 +88,15 @@ export function iframeTemplate(
}
.g-recaptcha,
.h-captcha,
.cap-container {
.h-captcha {
display: flex;
align-items: center;
justify-content: center;
}
.cap-container {
box-sizing: border-box;
padding-block: 0.5rem;
width: 100%;
}
</style>
${children}
<script
${scriptOnLoad ? 'onload="loadListener()"' : ""}
${scriptType === "module" ? 'type="module"' : ""}
src="${challengeURL.toString()}"
></script>
`,

View File

@@ -49,12 +49,6 @@ export class CaptchaDisplayController implements ReactiveController {
const input = this.#inputRef.value;
if (!input) return;
input.value = token;
// The surrounding identification form only updates its validity when form controls
// emit normal input events, so mirror a user's field change after the CAPTCHA solves.
input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
input.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
this.#loaded = true;
this.host.requestUpdate();
};
public onFailure() {

View File

@@ -1,6 +1,19 @@
version: 1
metadata:
name: Playwright e2e test admin
entries:
- attrs:
- model: authentik_core.group
state: present
identifiers:
name: "authentik Admins"
attrs:
is_superuser: true
id: admin-group
- model: authentik_core.user
state: present
identifiers:
username: akadmin
attrs:
email: test-admin@goauthentik.io
is_active: true
name: authentik Default Admin
@@ -8,9 +21,4 @@ entries:
path: users
type: internal
groups:
- !Find [authentik_core.group, [name, "authentik Admins"]]
conditions: []
identifiers:
username: akadmin
model: authentik_core.user
state: present
- !KeyOf admin-group

View File

@@ -17,8 +17,6 @@ test.describe("Session Lifecycle", () => {
test.beforeAll(
'Ensure "Enable Remember me on this device" is on for the default identification stage',
async ({ browser }, { title: testName }) => {
if (Date.now()) return;
const context = await browser.newContext();
const page = await context.newPage();
const navigator = new NavigatorFixture(page, testName);

View File

@@ -161,8 +161,13 @@ test.describe("Roles", () => {
});
await test.step("Verify role name updated on view page", async () => {
// The role name shows up in both the page-navbar heading (as
// "Role <name>") and the description-list text (raw <name>); a
// bare getByText match hits both and triggers strict-mode. Pin
// to the description list so we're asserting the canonical
// value the edit just wrote, not the heading template.
await expect(
page.getByText(updatedName),
page.getByText(updatedName, { exact: true }),
"Updated role name is visible on view page",
).toBeVisible();
});

View File

@@ -17,7 +17,15 @@ test.describe("Provider Wizard", () => {
const dialog = page.getByRole("dialog", { name: "New Provider Wizard" });
await test.step("Authenticate", async () => session.login());
// session.login() with no `to` waits for the auth flow URL pattern to
// re-appear, which it does immediately (we just navigated there), so
// the helper returns *before* the post-login redirect lands. The
// wizard buttons probed below live on /if/admin/#/core/providers, so
// pin the destination explicitly — same shape as the other test files.
await test.step("Authenticate", async () =>
session.login({
to: "/if/admin/#/core/providers",
}));
await test.step("Navigate to provider wizard", async () => {
await expect(dialog, "Dialog is initially closed").toBeHidden();

View File

@@ -3,9 +3,14 @@ import { expect, test as setup } from "#e2e";
setup("Web server availability", async ({ baseURL }) => {
expect(baseURL, "Base URL is set").toBeTruthy();
const ok = await fetch(baseURL!)
// Probe the default authentication flow rather than the root URL — the root
// redirects to /setup when AUTHENTIK_BOOTSTRAP_* env vars are unset, and
// /setup raises FlowNonApplicableException (HTTP 500) once an admin user
// already has a usable password (as our test-admin blueprint installs).
const probeURL = new URL("/if/flow/default-authentication-flow/", baseURL!);
const ok = await fetch(probeURL)
.then((res) => res.ok)
.catch(() => false);
expect(ok, `Web server should be listening on ${baseURL}`).toBeTruthy();
expect(ok, `Web server should be serving ${probeURL}`).toBeTruthy();
});

View File

@@ -2349,6 +2349,10 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -11199,36 +11203,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2371,6 +2371,10 @@
<source>Authenticate SCIM requests using a static token.</source>
<target>SCIM-Anfragen mit einem statischen Token authentifizieren.</target>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
<target>SCIM-Anfragen per OAuth authentifizieren.</target>
@@ -11231,36 +11235,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1808,6 +1808,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -9221,36 +9224,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2326,6 +2326,9 @@ Si se deja vacío, AuthnContextClassRef se establecerá según los métodos de a
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -11157,36 +11160,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2415,6 +2415,10 @@
<source>Authenticate SCIM requests using a static token.</source>
<target>Todenna SCIM-pyynnöt staattisella tunnisteella.</target>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
<target>Todenna SCIM-pyynnöt OAuthilla.</target>
@@ -11397,36 +11401,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2412,6 +2412,10 @@
<source>Authenticate SCIM requests using a static token.</source>
<target>Authentifier les requêtes SCIM avec un jeton statique.</target>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
<target>Authentifier les requêtes SCIM avec OAuth.</target>
@@ -11386,36 +11390,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2293,6 +2293,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -11106,36 +11109,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2410,6 +2410,10 @@
<source>Authenticate SCIM requests using a static token.</source>
<target>静的トークンを使用してSCIMリクエストを認証します。</target>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
<target>OAuthを使用してSCIMリクエストを認証します。</target>
@@ -11387,36 +11391,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2235,6 +2235,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -10758,36 +10761,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2147,6 +2147,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -10443,36 +10446,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2234,6 +2234,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -10782,36 +10785,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2414,6 +2414,10 @@
<source>Authenticate SCIM requests using a static token.</source>
<target>Autenticar solicitações SCIM usando um token estático.</target>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
<target>Autenticar solicitações SCIM usando OAuth.</target>
@@ -11379,36 +11383,6 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2255,6 +2255,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -10868,36 +10871,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2240,6 +2240,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -10858,36 +10861,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2409,6 +2409,10 @@
<source>Authenticate SCIM requests using a static token.</source>
<target>使用静态令牌验证 SCIM 请求。</target>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
<target>OAuth</target>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
<target>使用 OAuth 验证 SCIM 请求</target>
@@ -11681,36 +11685,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2166,6 +2166,9 @@
<trans-unit id="s24d7ac2fc280ca5e">
<source>Authenticate SCIM requests using a static token.</source>
</trans-unit>
<trans-unit id="s0e1641692e0dac94">
<source>OAuth</source>
</trans-unit>
<trans-unit id="s1c34cb0a4cccc041">
<source>Authenticate SCIM requests using OAuth.</source>
</trans-unit>
@@ -10495,36 +10498,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s3162a5abea92514e">
<source>View Credentials</source>
</trans-unit>
<trans-unit id="s7cbd6ddbb3cdbda4">
<source>OAuth (Silent)</source>
</trans-unit>
<trans-unit id="sf74719393c9d600b">
<source>OAuth (Interactive)</source>
</trans-unit>
<trans-unit id="s364366c872ccd349">
<source>Authenticate SCIM requests using OAuth, interactively authorized.</source>
</trans-unit>
<trans-unit id="s67dbb822a17bf6fc">
<source>OAuth Token last updated</source>
</trans-unit>
<trans-unit id="sb586db2f11959745">
<source>OAuth Token expires</source>
</trans-unit>
<trans-unit id="s4011de0d7365d106">
<source>OAuth Status</source>
</trans-unit>
<trans-unit id="s169a1a1dce3987b8">
<source>Authenticated</source>
</trans-unit>
<trans-unit id="s02e7dfce69646f50">
<source>No token saved</source>
</trans-unit>
<trans-unit id="sde2c41117de5db9d">
<source>(Re-)authenticate</source>
</trans-unit>
<trans-unit id="s13c4adf0d185f12e">
<source>OAuth Callback URL</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2,7 +2,7 @@
title: Captcha stage
---
The Captcha stage adds CAPTCHA verification to a flow by using Google reCAPTCHA or compatible alternatives like hCaptcha, Cloudflare Turnstile, and Cap.
The Captcha stage adds CAPTCHA verification to a flow by using Google reCAPTCHA or compatible alternatives like hCaptcha and Cloudflare Turnstile.
## Overview
@@ -20,7 +20,6 @@ It can either be bound to a flow or embedded inside the [Identification stage](.
- **Error on invalid score**: show an error immediately when the score is outside the configured threshold. If disabled, the flow continues and policies can inspect the result from context.
- **JS URL**: JavaScript loader URL for the provider.
- **API URL**: verification endpoint URL for the provider.
- **Request content type**: content type used when authentik verifies the CAPTCHA token with the provider.
## Flow integration

View File

@@ -205,6 +205,13 @@ Copy the generated recovery key and paste it into the URL, after the domain. For
## End-to-end (E2E) setup
authentik ships two end-to-end test suites:
- The Django/Selenium suite under `tests/e2e/` — driven by Django's test runner. It is exercised in CI by the `test-e2e` job in `ci-main.yml`.
- The browser-side Playwright suite under `web/test/browser/` — driven by `web/playwright.config.js`. It is exercised in CI by the `e2e (playwright)` job in `ci-web.yml` and runs against a live authentik server.
### Django/Selenium suite
Start the E2E test services with the following command:
```shell
@@ -219,6 +226,40 @@ Alternatively, you can connect directly via VNC on port `5900` using the passwor
When using Docker Desktop, host networking needs to be enabled via **Docker Settings** > **Resources** > **Network** > **Enable host networking**.
:::
### Playwright suite
The Playwright suite assumes that:
- An authentik stack is reachable at `http://localhost:9000` (override with `AK_TEST_RUNNER_PAGE_URL`).
- The blueprint at `web/test/blueprints/test-admin-user.yaml` has been applied so that the `test-admin@goauthentik.io` user (password `test-runner`) can log in.
Both prerequisites are satisfied by the standard full development environment described above, plus a one-off blueprint apply. From the repository root:
```shell
# Make the test admin blueprint discoverable by the worker, then start authentik.
mkdir -p blueprints/local
cp web/test/blueprints/test-admin-user.yaml blueprints/local/test-admin-user.yaml
# In separate terminals:
make run-server
make run-worker
```
Once the worker has applied the blueprint, install Playwright's browsers (one-time) and run the suite — these are the same npm scripts CI runs:
```shell
corepack npm exec --prefix web -- playwright install --with-deps chromium
corepack npm run --prefix web test:e2e
```
After a failing run, open the HTML report to inspect traces, screenshots, and (in CI) videos:
```shell
corepack npm exec --prefix web -- playwright show-report
```
In CI, the same artifacts are uploaded under the `playwright-report` and `playwright-traces` artifact names on the failed workflow run.
## Contributing code
### Before submitting a pull request

View File

@@ -102,7 +102,7 @@ To support the integration of AWS with authentik using SCIM, you need to create
#### Create property mappings
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Property Mappings**, click **Create**, select **SCIM Provider Mapping**, and click **Next**.
2. Navigate to **Customization** > **Property Mappings**, click **Create**, select **SCIM Mapping**, and click **Next**.
3. Configure the first _user mapping_ property mapping:
- **Name**: Provide a name lexically lower than `authentik default` (e.g. `AWS SCIM User mapping`).
- **Expression**: