mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
78 Commits
fix-logger
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca5a54307 | ||
|
|
ef1aad5dbb | ||
|
|
29d880920e | ||
|
|
fc6f8374e6 | ||
|
|
a8668bbac4 | ||
|
|
d686932166 | ||
|
|
feceb220b1 | ||
|
|
937df6e07f | ||
|
|
48e6b968a6 | ||
|
|
cd89c45e75 | ||
|
|
e53995e2c1 | ||
|
|
33d5f11f0e | ||
|
|
565e16eca7 | ||
|
|
9a0164b722 | ||
|
|
8af491630b | ||
|
|
8e25e7a213 | ||
|
|
4d183657da | ||
|
|
be89b6052d | ||
|
|
ad5d2bb611 | ||
|
|
8d30fb3d25 | ||
|
|
cea3fbfa9b | ||
|
|
151d889ff4 | ||
|
|
58ca3ecbd5 | ||
|
|
1a6c7082a3 | ||
|
|
1dc60276f9 | ||
|
|
de045c6d7b | ||
|
|
850728e9bb | ||
|
|
84a605a4ba | ||
|
|
1780bb0cf0 | ||
|
|
cd75fe235d | ||
|
|
e6e62e9de1 | ||
|
|
ac7a4f8a22 | ||
|
|
0290ed3342 | ||
|
|
e367525794 | ||
|
|
93c319baee | ||
|
|
1d02ee7d74 | ||
|
|
93439b5742 | ||
|
|
6682a6664e | ||
|
|
0b5bac74e9 | ||
|
|
062823f1b2 | ||
|
|
a17fe58971 | ||
|
|
422ea893b1 | ||
|
|
15c9f93851 | ||
|
|
e2202d498b | ||
|
|
9ea9a86ad3 | ||
|
|
4bac1edd61 | ||
|
|
24726be3c9 | ||
|
|
411f06756f | ||
|
|
4bdcab48c3 | ||
|
|
00dbd377a7 | ||
|
|
a01c0575db | ||
|
|
6e51d044bb | ||
|
|
6d1b168dc4 | ||
|
|
43675c2b22 | ||
|
|
8645273eaf | ||
|
|
eb6f4712fe | ||
|
|
7b9505242e | ||
|
|
3dda20ebc7 | ||
|
|
dfd2bc5c3c | ||
|
|
06a270913c | ||
|
|
430507fc72 | ||
|
|
847af7f9ea | ||
|
|
8f1cb636e8 | ||
|
|
e61c876002 | ||
|
|
33c0d3df0a | ||
|
|
3a03e1ebfd | ||
|
|
1e41b77761 | ||
|
|
6c1662f99f | ||
|
|
bb5bc5c8da | ||
|
|
30670c9070 | ||
|
|
fdbf9ffedc | ||
|
|
2ec433d724 | ||
|
|
55297b9e6a | ||
|
|
f9dda6582c | ||
|
|
3394c17bfd | ||
|
|
a37d101b10 | ||
|
|
4774b4db87 | ||
|
|
fdb52c9394 |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -58,7 +58,7 @@ runs:
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/compose.yml up -d
|
||||
cd web && npm i
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: uv run python {0}
|
||||
|
||||
21
.github/workflows/release-publish.yml
vendored
21
.github/workflows/release-publish.yml
vendored
@@ -160,10 +160,17 @@ jobs:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build web
|
||||
- name: Install web dependencies
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
@@ -210,12 +217,12 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
docker compose pull -q
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql
|
||||
docker compose run -u root server test-all
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
|
||||
docker compose -f lifecycle/container/compose.yml pull -q
|
||||
docker compose -f lifecycle/container/compose.yml up --no-start
|
||||
docker compose -f lifecycle/container/compose.yml start postgresql
|
||||
docker compose -f lifecycle/container/compose.yml run -u root server test-all
|
||||
sentry-release:
|
||||
needs:
|
||||
- build-server
|
||||
|
||||
9
.github/workflows/release-tag.yml
vendored
9
.github/workflows/release-tag.yml
vendored
@@ -91,6 +91,7 @@ jobs:
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
|
||||
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
|
||||
git pull
|
||||
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
@@ -174,21 +175,25 @@ jobs:
|
||||
if: "${{ inputs.release_reason == 'feature' }}"
|
||||
run: |
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}"
|
||||
reason="{{ inputs.release_reason }}"
|
||||
jq \
|
||||
--arg version "${{ inputs.version }}" \
|
||||
--arg changelog "See ${changelog_url}" \
|
||||
--arg changelog_url "${changelog_url}" \
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
|
||||
--arg reason "${reason}" \
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
|
||||
mv version.new.json version.json
|
||||
- name: Bump version
|
||||
if: "${{ inputs.release_reason != 'feature' }}"
|
||||
run: |
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version}} | sed 's/\.//g')"
|
||||
reason="{{ inputs.release_reason }}"
|
||||
jq \
|
||||
--arg version "${{ inputs.version }}" \
|
||||
--arg changelog "See ${changelog_url}" \
|
||||
--arg changelog_url "${changelog_url}" \
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
|
||||
--arg reason "${reason}" \
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
|
||||
mv version.new.json version.json
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
|
||||
8
Makefile
8
Makefile
@@ -148,11 +148,11 @@ bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||
ifndef version
|
||||
$(error Usage: make bump version=20xx.xx.xx )
|
||||
endif
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
npm version --no-git-tag-version --allow-same-version $(version)
|
||||
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
|
||||
$(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
|
||||
echo -n $(version) > ${PWD}/internal/constants/VERSION
|
||||
|
||||
#########################
|
||||
|
||||
@@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ---------- |
|
||||
| 2025.10.x | ✅ |
|
||||
| 2025.12.x | ✅ |
|
||||
| 2026.2.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.2.0-rc1"
|
||||
VERSION = "2026.2.1-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Schema generation tests"""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
@@ -29,15 +31,14 @@ class TestSchemaGeneration(APITestCase):
|
||||
|
||||
def test_build_schema(self):
|
||||
"""Test schema build command"""
|
||||
blueprint_file = Path("blueprints/schema.json")
|
||||
api_file = Path("schema.yml")
|
||||
blueprint_file.unlink()
|
||||
api_file.unlink()
|
||||
tmp = Path(gettempdir())
|
||||
blueprint_file = tmp / f"{str(uuid4())}.json"
|
||||
api_file = tmp / f"{str(uuid4())}.yml"
|
||||
with (
|
||||
CONFIG.patch("debug", True),
|
||||
CONFIG.patch("tenants.enabled", True),
|
||||
CONFIG.patch("outposts.disable_embedded_outpost", True),
|
||||
):
|
||||
call_command("build_schema")
|
||||
call_command("build_schema", blueprint_file=blueprint_file, api_file=api_file)
|
||||
self.assertTrue(blueprint_file.exists())
|
||||
self.assertTrue(api_file.exists())
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""authentik core models"""
|
||||
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from enum import StrEnum
|
||||
from hashlib import sha256
|
||||
@@ -528,23 +530,35 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
"default: in 30 days). See authentik logs for every will invocation of this "
|
||||
"deprecation."
|
||||
)
|
||||
stacktrace = traceback.format_stack()
|
||||
# The last line is this function, the next-to-last line is its caller
|
||||
cause = stacktrace[-2] if len(stacktrace) > 1 else "Unknown, see stacktrace in logs"
|
||||
if search := re.search(r'"(.*?)"', cause):
|
||||
cause = f"Property mapping or Expression policy named {search.group(1)}"
|
||||
|
||||
LOGGER.warning(
|
||||
"deprecation used",
|
||||
message=message_logger,
|
||||
deprecation=deprecation,
|
||||
replacement=replacement,
|
||||
cause=cause,
|
||||
stacktrace=stacktrace,
|
||||
)
|
||||
if not Event.filter_not_expired(
|
||||
action=EventAction.CONFIGURATION_WARNING, context__deprecation=deprecation
|
||||
action=EventAction.CONFIGURATION_WARNING,
|
||||
context__deprecation=deprecation,
|
||||
context__cause=cause,
|
||||
).exists():
|
||||
event = Event.new(
|
||||
EventAction.CONFIGURATION_WARNING,
|
||||
deprecation=deprecation,
|
||||
replacement=replacement,
|
||||
message=message_event,
|
||||
cause=cause,
|
||||
)
|
||||
event.expires = datetime.now() + timedelta(days=30)
|
||||
event.save()
|
||||
|
||||
return self.groups
|
||||
|
||||
def set_password(self, raw_password, signal=True, sender=None, request=None):
|
||||
|
||||
@@ -44,19 +44,24 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer aria-label="Site footer" class="pf-c-login__footer pf-m-dark">
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<span>
|
||||
{% trans 'Powered by authentik' %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<footer
|
||||
name="site-footer"
|
||||
aria-label="{% trans 'Site footer' %}"
|
||||
class="pf-c-login__footer pf-m-dark">
|
||||
<div name="flow-links" aria-label="{% trans 'Flow links' %}">
|
||||
<ul class="pf-c-list pf-m-inline" part="list">
|
||||
{% for link in footer_links %}
|
||||
<li part="list-item">
|
||||
<a part="list-item-link" href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li part="list-item">
|
||||
<span>
|
||||
{% trans 'Powered by authentik' %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ def generate_key_id_legacy(key_data: str) -> str:
|
||||
"""Generate Key ID using MD5 (legacy format for backwards compatibility)."""
|
||||
if not key_data:
|
||||
return ""
|
||||
return md5(key_data.encode("utf-8")).hexdigest() # nosec
|
||||
return md5(key_data.encode("utf-8"), usedforsecurity=False).hexdigest() # nosec
|
||||
|
||||
|
||||
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from hashlib import sha256
|
||||
from json import loads
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
from jwt import encode
|
||||
@@ -232,3 +233,43 @@ class TestEndpointStage(FlowTestCase):
|
||||
plan = plan()
|
||||
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
|
||||
self.assertEqual(plan.context[PLAN_CONTEXT_DEVICE], self.device)
|
||||
|
||||
def test_endpoint_stage_connector_no_stage_optional(self):
|
||||
flow = create_test_flow()
|
||||
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.OPTIONAL)
|
||||
FlowStageBinding.objects.create(stage=stage, target=flow, order=0)
|
||||
|
||||
with patch(
|
||||
"authentik.endpoints.connectors.agent.models.AgentConnector.stage",
|
||||
PropertyMock(return_value=None),
|
||||
):
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
|
||||
plan = plan()
|
||||
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
|
||||
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)
|
||||
|
||||
def test_endpoint_stage_connector_no_stage_required(self):
|
||||
flow = create_test_flow()
|
||||
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.REQUIRED)
|
||||
FlowStageBinding.objects.create(stage=stage, target=flow, order=0)
|
||||
|
||||
with patch(
|
||||
"authentik.endpoints.connectors.agent.models.AgentConnector.stage",
|
||||
PropertyMock(return_value=None),
|
||||
):
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
res,
|
||||
component="ak-stage-access-denied",
|
||||
error_message="Invalid stage configuration",
|
||||
)
|
||||
plan = plan()
|
||||
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
|
||||
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from authentik.endpoints.models import EndpointStage
|
||||
from authentik.endpoints.models import EndpointStage, StageMode
|
||||
from authentik.flows.stage import StageView
|
||||
|
||||
PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
|
||||
@@ -6,15 +6,24 @@ PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
|
||||
|
||||
class EndpointStageView(StageView):
|
||||
|
||||
def _get_inner(self):
|
||||
def _get_inner(self) -> StageView | None:
|
||||
stage: EndpointStage = self.executor.current_stage
|
||||
inner_stage: type[StageView] | None = stage.connector.stage
|
||||
if not inner_stage:
|
||||
return self.executor.stage_ok()
|
||||
return None
|
||||
return inner_stage(self.executor, request=self.request)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return self._get_inner().dispatch(request, *args, **kwargs)
|
||||
inner = self._get_inner()
|
||||
if inner is None:
|
||||
stage: EndpointStage = self.executor.current_stage
|
||||
if stage.mode == StageMode.OPTIONAL:
|
||||
return self.executor.stage_ok()
|
||||
else:
|
||||
return self.executor.stage_invalid("Invalid stage configuration")
|
||||
return inner.dispatch(request, *args, **kwargs)
|
||||
|
||||
def cleanup(self):
|
||||
return self._get_inner().cleanup()
|
||||
inner = self._get_inner()
|
||||
if inner is not None:
|
||||
return inner.cleanup()
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.core.cache import cache
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.timezone import now
|
||||
from jwt import PyJWTError, decode, get_unverified_header
|
||||
from jwt.algorithms import ECAlgorithm
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
ChoiceField,
|
||||
@@ -109,13 +110,20 @@ class LicenseKey:
|
||||
intermediate.verify_directly_issued_by(get_licensing_key())
|
||||
except InvalidSignature, TypeError, ValueError, Error:
|
||||
raise ValidationError("Unable to verify license") from None
|
||||
_validate_curve_original = ECAlgorithm._validate_curve
|
||||
try:
|
||||
# authentik's license are generated with `algorithm="ES512"` and signed with
|
||||
# a key of curve `secp384r1`. Starting with version 2.11.0, pyjwt enforces the spec, see
|
||||
# https://github.com/jpadilla/pyjwt/commit/5b8622773358e56d3d3c0a9acf404809ff34433a
|
||||
# authentik will change its license generation to `algorithm="ES384"` in 2026.
|
||||
# TODO: remove this when the last incompatible license runs out.
|
||||
ECAlgorithm._validate_curve = lambda *_: True
|
||||
body = from_dict(
|
||||
LicenseKey,
|
||||
decode(
|
||||
jwt,
|
||||
our_cert.public_key(),
|
||||
algorithms=["ES512"],
|
||||
algorithms=["ES384", "ES512"],
|
||||
audience=get_license_aud(),
|
||||
options={"verify_exp": check_expiry, "verify_signature": check_expiry},
|
||||
),
|
||||
@@ -125,6 +133,8 @@ class LicenseKey:
|
||||
if unverified["aud"] != get_license_aud():
|
||||
raise ValidationError("Invalid Install ID in license") from None
|
||||
raise ValidationError("Unable to verify license") from None
|
||||
finally:
|
||||
ECAlgorithm._validate_curve = _validate_curve_original
|
||||
return body
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import BooleanField as ModelBooleanField
|
||||
from django.db.models import Case, Q, Value, When
|
||||
from django_filters.rest_framework import BooleanFilter, FilterSet
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import DateField, IntegerField, SerializerMethodField
|
||||
from rest_framework.fields import IntegerField, SerializerMethodField
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@@ -21,6 +21,7 @@ from authentik.enterprise.lifecycle.utils import (
|
||||
ReviewerUserSerializer,
|
||||
admin_link_for_model,
|
||||
parse_content_type,
|
||||
start_of_day,
|
||||
)
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
@@ -67,13 +68,13 @@ class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||
def get_object_admin_url(self, iteration: LifecycleIteration) -> str:
|
||||
return admin_link_for_model(iteration.object)
|
||||
|
||||
@extend_schema_field(DateField())
|
||||
def get_grace_period_end(self, iteration: LifecycleIteration) -> date:
|
||||
return iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
|
||||
def get_grace_period_end(self, iteration: LifecycleIteration) -> datetime:
|
||||
return start_of_day(
|
||||
iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
|
||||
)
|
||||
|
||||
@extend_schema_field(DateField())
|
||||
def get_next_review_date(self, iteration: LifecycleIteration):
|
||||
return iteration.opened_on + timedelta_from_string(iteration.rule.interval)
|
||||
def get_next_review_date(self, iteration: LifecycleIteration) -> datetime:
|
||||
return start_of_day(iteration.opened_on + timedelta_from_string(iteration.rule.interval))
|
||||
|
||||
def get_user_can_review(self, iteration: LifecycleIteration) -> bool:
|
||||
return iteration.user_can_review(self.context["request"].user)
|
||||
@@ -102,7 +103,7 @@ class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet
|
||||
default=Value(False),
|
||||
output_field=ModelBooleanField(),
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-13 09:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_lifecycle", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="lifecycleiteration",
|
||||
name="opened_on",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
@@ -13,7 +14,7 @@ from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.blueprints.models import ManagedModel
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.enterprise.lifecycle.utils import link_for_model
|
||||
from authentik.enterprise.lifecycle.utils import link_for_model, start_of_day
|
||||
from authentik.events.models import Event, EventAction, NotificationSeverity, NotificationTransport
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
@@ -98,7 +99,9 @@ class LifecycleRule(SerializerModel):
|
||||
|
||||
def _get_newly_overdue_iterations(self) -> QuerySet[LifecycleIteration]:
|
||||
return self.lifecycleiteration_set.filter(
|
||||
opened_on__lte=timezone.now() - timedelta_from_string(self.grace_period),
|
||||
opened_on__lt=start_of_day(
|
||||
timezone.now() + timedelta(days=1) - timedelta_from_string(self.grace_period)
|
||||
),
|
||||
state=ReviewState.PENDING,
|
||||
)
|
||||
|
||||
@@ -106,7 +109,9 @@ class LifecycleRule(SerializerModel):
|
||||
recent_iteration_ids = LifecycleIteration.objects.filter(
|
||||
content_type=self.content_type,
|
||||
object_id__isnull=False,
|
||||
opened_on__gte=timezone.now() - timedelta_from_string(self.interval),
|
||||
opened_on__gte=start_of_day(
|
||||
timezone.now() + timedelta(days=1) - timedelta_from_string(self.interval)
|
||||
),
|
||||
).values_list(Cast("object_id", output_field=self._get_pk_field()), flat=True)
|
||||
|
||||
return self.get_objects().exclude(pk__in=recent_iteration_ids)
|
||||
@@ -186,7 +191,7 @@ class LifecycleIteration(SerializerModel, ManagedModel):
|
||||
rule = models.ForeignKey(LifecycleRule, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
state = models.CharField(max_length=10, choices=ReviewState, default=ReviewState.PENDING)
|
||||
opened_on = models.DateField(auto_now_add=True)
|
||||
opened_on = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=["content_type", "opened_on"])]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import datetime as dt
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -319,7 +320,7 @@ class TestLifecycleModels(TestCase):
|
||||
content_type=content_type, object_id=str(app_one.pk), rule=rule_overdue
|
||||
)
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=(timezone.now().date() - timedelta(days=20))
|
||||
opened_on=(timezone.now() - timedelta(days=20))
|
||||
)
|
||||
|
||||
# Apply again to trigger overdue logic
|
||||
@@ -383,7 +384,7 @@ class TestLifecycleModels(TestCase):
|
||||
content_type=content_type, object_id=str(app_overdue.pk), rule=rule_overdue
|
||||
)
|
||||
LifecycleIteration.objects.filter(pk=overdue_iteration.pk).update(
|
||||
opened_on=(timezone.now().date() - timedelta(days=20))
|
||||
opened_on=(timezone.now() - timedelta(days=20))
|
||||
)
|
||||
|
||||
# Apply overdue rule to mark iteration as overdue
|
||||
@@ -667,3 +668,178 @@ class TestLifecycleModels(TestCase):
|
||||
reviewers = list(rule.get_reviewers())
|
||||
self.assertIn(explicit_reviewer, reviewers)
|
||||
self.assertIn(group_member, reviewers)
|
||||
|
||||
|
||||
class TestLifecycleDateBoundaries(TestCase):
|
||||
"""Verify that start_of_day normalization ensures correct overdue/due
|
||||
detection regardless of exact task execution time within a day.
|
||||
|
||||
The daily task may run at any point during the day. The start_of_day
|
||||
normalization in _get_newly_overdue_iterations and _get_newly_due_objects
|
||||
ensures that the boundary is always at midnight, so millisecond variations
|
||||
in task execution time do not affect results."""
|
||||
|
||||
def _create_rule_and_iteration(self, grace_period="days=1", interval="days=365"):
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
content_type = ContentType.objects.get_for_model(Application)
|
||||
rule = LifecycleRule.objects.create(
|
||||
name=generate_id(),
|
||||
content_type=content_type,
|
||||
object_id=str(app.pk),
|
||||
interval=interval,
|
||||
grace_period=grace_period,
|
||||
)
|
||||
iteration = LifecycleIteration.objects.get(
|
||||
content_type=content_type, object_id=str(app.pk), rule=rule
|
||||
)
|
||||
return app, rule, iteration
|
||||
|
||||
def test_overdue_iteration_opened_yesterday(self):
|
||||
"""grace_period=1 day: iteration opened yesterday at any time is overdue today."""
|
||||
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=1")
|
||||
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
|
||||
for opened_on in [
|
||||
dt.datetime(2025, 6, 14, 0, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 14, 12, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 14, 23, 59, 59, 999999, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(opened_on=opened_on):
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=opened_on, state=ReviewState.PENDING
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertIn(iteration, list(rule._get_newly_overdue_iterations()))
|
||||
|
||||
def test_not_overdue_iteration_opened_today(self):
|
||||
"""grace_period=1 day: iteration opened today at any time is NOT overdue."""
|
||||
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=1")
|
||||
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
|
||||
for opened_on in [
|
||||
dt.datetime(2025, 6, 15, 0, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 23, 59, 59, 999999, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(opened_on=opened_on):
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=opened_on, state=ReviewState.PENDING
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertNotIn(iteration, list(rule._get_newly_overdue_iterations()))
|
||||
|
||||
def test_overdue_independent_of_task_execution_time(self):
|
||||
"""Overdue detection gives the same result whether the task runs at 00:00:01 or 23:59:59."""
|
||||
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=1")
|
||||
opened_on = dt.datetime(2025, 6, 14, 18, 0, 0, tzinfo=dt.UTC)
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=opened_on, state=ReviewState.PENDING
|
||||
)
|
||||
for task_time in [
|
||||
dt.datetime(2025, 6, 15, 0, 0, 1, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 12, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 23, 59, 59, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(task_time=task_time):
|
||||
with patch("django.utils.timezone.now", return_value=task_time):
|
||||
self.assertIn(iteration, list(rule._get_newly_overdue_iterations()))
|
||||
|
||||
def test_overdue_boundary_multi_day_grace_period(self):
|
||||
"""grace_period=30 days: overdue after 30 full days, not after 29."""
|
||||
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=30")
|
||||
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
|
||||
|
||||
# Opened 30 days ago (May 16), should go overdue
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=dt.datetime(2025, 5, 16, 12, 0, 0, tzinfo=dt.UTC),
|
||||
state=ReviewState.PENDING,
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertIn(iteration, list(rule._get_newly_overdue_iterations()))
|
||||
|
||||
# Opened 29 days ago (May 17), should NOT go overdue
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=dt.datetime(2025, 5, 17, 12, 0, 0, tzinfo=dt.UTC),
|
||||
state=ReviewState.PENDING,
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertNotIn(iteration, list(rule._get_newly_overdue_iterations()))
|
||||
|
||||
def test_due_object_iteration_opened_yesterday(self):
|
||||
"""interval=1 day: object with iteration opened yesterday is due for a new review."""
|
||||
app, rule, iteration = self._create_rule_and_iteration(interval="days=1")
|
||||
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
|
||||
for opened_on in [
|
||||
dt.datetime(2025, 6, 14, 0, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 14, 12, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 14, 23, 59, 59, 999999, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(opened_on=opened_on):
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(opened_on=opened_on)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertIn(app, list(rule._get_newly_due_objects()))
|
||||
|
||||
def test_not_due_object_iteration_opened_today(self):
|
||||
"""interval=1 day: object with iteration opened today is NOT due."""
|
||||
app, rule, iteration = self._create_rule_and_iteration(interval="days=1")
|
||||
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
|
||||
for opened_on in [
|
||||
dt.datetime(2025, 6, 15, 0, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 23, 59, 59, 999999, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(opened_on=opened_on):
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(opened_on=opened_on)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertNotIn(app, list(rule._get_newly_due_objects()))
|
||||
|
||||
def test_due_independent_of_task_execution_time(self):
|
||||
"""Due detection gives the same result whether the task runs at 00:00:01 or 23:59:59."""
|
||||
app, rule, iteration = self._create_rule_and_iteration(interval="days=1")
|
||||
opened_on = dt.datetime(2025, 6, 14, 18, 0, 0, tzinfo=dt.UTC)
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(opened_on=opened_on)
|
||||
for task_time in [
|
||||
dt.datetime(2025, 6, 15, 0, 0, 1, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 12, 0, 0, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 23, 59, 59, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(task_time=task_time):
|
||||
with patch("django.utils.timezone.now", return_value=task_time):
|
||||
self.assertIn(app, list(rule._get_newly_due_objects()))
|
||||
|
||||
def test_due_boundary_multi_day_interval(self):
|
||||
"""interval=30 days: due after 30 full days, not after 29."""
|
||||
app, rule, iteration = self._create_rule_and_iteration(interval="days=30")
|
||||
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
|
||||
|
||||
# Previous review opened 30 days ago (May 16), review is due for the object
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=dt.datetime(2025, 5, 16, 12, 0, 0, tzinfo=dt.UTC)
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertIn(app, list(rule._get_newly_due_objects()))
|
||||
|
||||
# Previous review opened 29 days ago (May 17), new review is NOT due
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=dt.datetime(2025, 5, 17, 12, 0, 0, tzinfo=dt.UTC)
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
self.assertNotIn(app, list(rule._get_newly_due_objects()))
|
||||
|
||||
def test_apply_overdue_at_boundary(self):
|
||||
"""apply() marks iteration overdue when grace period just expired,
|
||||
regardless of what time the daily task runs."""
|
||||
_, rule, iteration = self._create_rule_and_iteration(
|
||||
grace_period="days=1", interval="days=365"
|
||||
)
|
||||
opened_on = dt.datetime(2025, 6, 14, 20, 0, 0, tzinfo=dt.UTC)
|
||||
for task_time in [
|
||||
dt.datetime(2025, 6, 15, 0, 0, 1, tzinfo=dt.UTC),
|
||||
dt.datetime(2025, 6, 15, 23, 59, 59, tzinfo=dt.UTC),
|
||||
]:
|
||||
with self.subTest(task_time=task_time):
|
||||
LifecycleIteration.objects.filter(pk=iteration.pk).update(
|
||||
opened_on=opened_on, state=ReviewState.PENDING
|
||||
)
|
||||
with patch("django.utils.timezone.now", return_value=task_time):
|
||||
rule.apply()
|
||||
iteration.refresh_from_db()
|
||||
self.assertEqual(iteration.state, ReviewState.OVERDUE)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from urllib import parse
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -39,6 +40,10 @@ def link_for_model(model: Model) -> str:
|
||||
return f"{reverse("authentik_core:if-admin")}#{admin_link_for_model(model)}"
|
||||
|
||||
|
||||
def start_of_day(dt: datetime) -> datetime:
|
||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
class ContentTypeField(ChoiceField):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(choices=model_choices(), **kwargs)
|
||||
|
||||
@@ -78,7 +78,8 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
|
||||
def create(self, user: User):
|
||||
"""Create user from scratch and create a connection object"""
|
||||
microsoft_user = self.to_schema(user, None)
|
||||
self.check_email_valid(microsoft_user.user_principal_name)
|
||||
if microsoft_user.user_principal_name:
|
||||
self.check_email_valid(microsoft_user.user_principal_name)
|
||||
with transaction.atomic():
|
||||
try:
|
||||
response = self._request(self.client.users.post(microsoft_user))
|
||||
@@ -118,7 +119,8 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
|
||||
def update(self, user: User, connection: MicrosoftEntraProviderUser):
|
||||
"""Update existing user"""
|
||||
microsoft_user = self.to_schema(user, connection)
|
||||
self.check_email_valid(microsoft_user.user_principal_name)
|
||||
if microsoft_user.user_principal_name:
|
||||
self.check_email_valid(microsoft_user.user_principal_name)
|
||||
response = self._request(
|
||||
self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.urls import reverse
|
||||
from rest_framework.fields import CharField, SerializerMethodField, URLField
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.providers.ws_federation.models import WSFederationProvider
|
||||
from authentik.enterprise.providers.ws_federation.processors.metadata import MetadataProcessor
|
||||
@@ -18,6 +19,29 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial
|
||||
wtrealm = CharField(source="audience")
|
||||
url_wsfed = SerializerMethodField()
|
||||
|
||||
def get_url_download_metadata(self, instance: WSFederationProvider) -> str:
|
||||
"""Get metadata download URL"""
|
||||
if "request" not in self._context:
|
||||
return ""
|
||||
request: HttpRequest = self._context["request"]._request
|
||||
try:
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_ws_federation:metadata-download",
|
||||
kwargs={"application_slug": instance.application.slug},
|
||||
)
|
||||
)
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_api:wsfederationprovider-metadata",
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
},
|
||||
)
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
def get_url_wsfed(self, instance: WSFederationProvider) -> str:
|
||||
"""Get WS-Fed url"""
|
||||
if "request" not in self._context:
|
||||
|
||||
@@ -81,6 +81,8 @@ class SignInProcessor:
|
||||
self.sign_in_request = sign_in_request
|
||||
self.saml_processor = AssertionProcessor(self.provider, self.request, AuthNRequest())
|
||||
self.saml_processor.provider.audience = self.sign_in_request.wtrealm
|
||||
if self.provider.signing_kp:
|
||||
self.saml_processor.provider.sign_assertion = True
|
||||
|
||||
def create_response_token(self):
|
||||
root = Element(f"{{{NS_WS_FED_TRUST}}}RequestSecurityTokenResponse", nsmap=NS_MAP)
|
||||
@@ -148,7 +150,8 @@ class SignInProcessor:
|
||||
def response(self) -> dict[str, str]:
|
||||
root = self.create_response_token()
|
||||
assertion = root.xpath("//saml:Assertion", namespaces=NS_MAP)[0]
|
||||
self.saml_processor._sign(assertion)
|
||||
if self.provider.signing_kp:
|
||||
self.saml_processor._sign(assertion)
|
||||
str_token = etree.tostring(root).decode("utf-8") # nosec
|
||||
return delete_none_values(
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.providers.ws_federation.api.providers import WSFederationProviderViewSet
|
||||
from authentik.enterprise.providers.ws_federation.views import WSFedEntryView
|
||||
from authentik.enterprise.providers.ws_federation.views import MetadataDownload, WSFedEntryView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
@@ -11,6 +11,12 @@ urlpatterns = [
|
||||
WSFedEntryView.as_view(),
|
||||
name="wsfed",
|
||||
),
|
||||
# Metadata
|
||||
path(
|
||||
"<slug:application_slug>/metadata/",
|
||||
MetadataDownload.as_view(),
|
||||
name="metadata-download",
|
||||
),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application, AuthenticatedSession
|
||||
@@ -160,3 +162,24 @@ class WSFedFlowFinalView(ChallengeStageView):
|
||||
"attrs": response,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class MetadataDownload(View):
|
||||
"""Redirect to metadata download"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
app = Application.objects.filter(slug=application_slug).with_provider().first()
|
||||
if not app:
|
||||
raise Http404
|
||||
provider = app.get_provider()
|
||||
if not provider:
|
||||
raise Http404
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_api:wsfederationprovider-metadata",
|
||||
kwargs={
|
||||
"pk": provider.pk,
|
||||
},
|
||||
)
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
{{ block.super }}
|
||||
<link rel="prefetch" href="{{ flow_background_url }}" />
|
||||
{% if flow.compatibility_mode and not inspector %}
|
||||
<script data-id="shady-dom">ShadyDOM = { force: true };</script>
|
||||
{% comment %}
|
||||
@see {@link web/types/webcomponents.d.ts} for type definitions.
|
||||
{% endcomment %}
|
||||
<script data-id="shady-dom">
|
||||
"use strict";
|
||||
|
||||
window.ShadyDOM = window.ShadyDOM || {}
|
||||
window.ShadyDOM.force = true
|
||||
</script>
|
||||
{% endif %}
|
||||
{% include "base/header_js.html" %}
|
||||
<script data-id="flow-config">
|
||||
@@ -45,16 +53,11 @@
|
||||
slug="{{ flow.slug }}"
|
||||
class="pf-c-login"
|
||||
data-layout="{{ flow.layout|default:'stacked' }}"
|
||||
loading
|
||||
>
|
||||
{% include "base/placeholder.html" %}
|
||||
|
||||
<ak-brand-links
|
||||
slot="footer"
|
||||
exportparts="list:brand-links-list, list-item:brand-links-list-item"
|
||||
role="contentinfo"
|
||||
aria-label="{% trans 'Site footer' %}"
|
||||
class="pf-c-login__footer {% if flow.layout == 'stacked' %}pf-m-dark{% endif %}"
|
||||
></ak-brand-links>
|
||||
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
|
||||
</ak-flow-executor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +141,10 @@ web:
|
||||
# workers: 2
|
||||
threads: 4
|
||||
path: /
|
||||
timeout_http_read_header: 5s
|
||||
timeout_http_read: 30s
|
||||
timeout_http_write: 60s
|
||||
timeout_http_idle: 120s
|
||||
|
||||
worker:
|
||||
processes: 1
|
||||
|
||||
@@ -42,7 +42,7 @@ ARG_SANITIZE = re.compile(r"[:.-]")
|
||||
|
||||
|
||||
def sanitize_arg(arg_name: str) -> str:
|
||||
return re.sub(ARG_SANITIZE, "_", arg_name)
|
||||
return re.sub(ARG_SANITIZE, "_", slugify(arg_name))
|
||||
|
||||
|
||||
class BaseEvaluator:
|
||||
@@ -311,7 +311,9 @@ class BaseEvaluator:
|
||||
|
||||
def wrap_expression(self, expression: str) -> str:
|
||||
"""Wrap expression in a function, call it, and save the result as `result`"""
|
||||
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
|
||||
handler_signature = ",".join(
|
||||
[x for x in [sanitize_arg(x) for x in self._context.keys()] if x]
|
||||
)
|
||||
full_expression = ""
|
||||
full_expression += f"def handler({handler_signature}):\n"
|
||||
full_expression += indent(expression, " ")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test Evaluator base functions"""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
@@ -353,3 +354,18 @@ class TestEvaluator(TestCase):
|
||||
self.assertEqual(message.to, ["to@example.com"])
|
||||
self.assertEqual(message.cc, ["cc1@example.com", "cc2@example.com"])
|
||||
self.assertEqual(message.bcc, ["bcc1@example.com", "bcc2@example.com"])
|
||||
|
||||
def test_expr_arg_escape(self):
|
||||
"""Test escaping of arguments"""
|
||||
eval = BaseEvaluator()
|
||||
eval._context = {
|
||||
'z=getattr(getattr(__import__("os"), "popen")("id > /tmp/test"), "read")()': "bar",
|
||||
"@@": "baz",
|
||||
"{{": "baz",
|
||||
"aa@@": "baz",
|
||||
}
|
||||
res = eval.evaluate("return locals()")
|
||||
self.assertEqual(
|
||||
res, {"zgetattrgetattr__import__os_popenid_tmptest_read": "bar", "aa": "baz"}
|
||||
)
|
||||
self.assertFalse(Path("/tmp/test").exists())
|
||||
|
||||
@@ -132,9 +132,14 @@ class PolicyEngine:
|
||||
# If we didn't find any static bindings, do nothing
|
||||
return
|
||||
self.logger.debug("P_ENG: Found static bindings", **matched_bindings)
|
||||
if matched_bindings.get("passing", 0) > 0:
|
||||
# Any passing static binding -> passing
|
||||
passing = True
|
||||
if self.mode == PolicyEngineMode.MODE_ANY:
|
||||
if matched_bindings.get("passing", 0) > 0:
|
||||
# Any passing static binding -> passing
|
||||
passing = True
|
||||
elif self.mode == PolicyEngineMode.MODE_ALL:
|
||||
if matched_bindings.get("passing", 0) == matched_bindings["total"]:
|
||||
# All static bindings are passing -> passing
|
||||
passing = True
|
||||
elif matched_bindings["total"] > 0 and matched_bindings.get("passing", 0) < 1:
|
||||
# No matching static bindings but at least one is configured -> not passing
|
||||
passing = False
|
||||
@@ -185,6 +190,16 @@ class PolicyEngine:
|
||||
# Only call .recv() if no result is saved, otherwise we just deadlock here
|
||||
if not proc_info.result:
|
||||
proc_info.result = proc_info.connection.recv()
|
||||
if proc_info.result and proc_info.result._exec_time:
|
||||
HIST_POLICIES_EXECUTION_TIME.labels(
|
||||
binding_order=proc_info.binding.order,
|
||||
binding_target_type=proc_info.binding.target_type,
|
||||
binding_target_name=proc_info.binding.target_name,
|
||||
object_type=(
|
||||
class_to_path(self.request.obj.__class__) if self.request.obj else ""
|
||||
),
|
||||
mode="execute_process",
|
||||
).observe(proc_info.result._exec_time)
|
||||
return self
|
||||
|
||||
@property
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from multiprocessing import get_context
|
||||
from multiprocessing.connection import Connection
|
||||
from time import perf_counter
|
||||
|
||||
from django.core.cache import cache
|
||||
from sentry_sdk import start_span
|
||||
@@ -11,8 +12,6 @@ from structlog.stdlib import get_logger
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.policies.apps import HIST_POLICIES_EXECUTION_TIME
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.types import CACHE_PREFIX, PolicyRequest, PolicyResult
|
||||
@@ -123,18 +122,9 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
|
||||
def profiling_wrapper(self):
|
||||
"""Run with profiling enabled"""
|
||||
with (
|
||||
start_span(
|
||||
op="authentik.policy.process.execute",
|
||||
) as span,
|
||||
HIST_POLICIES_EXECUTION_TIME.labels(
|
||||
binding_order=self.binding.order,
|
||||
binding_target_type=self.binding.target_type,
|
||||
binding_target_name=self.binding.target_name,
|
||||
object_type=class_to_path(self.request.obj.__class__) if self.request.obj else "",
|
||||
mode="execute_process",
|
||||
).time(),
|
||||
):
|
||||
with start_span(
|
||||
op="authentik.policy.process.execute",
|
||||
) as span:
|
||||
span: Span
|
||||
span.set_data("policy", self.binding.policy)
|
||||
span.set_data("request", self.request)
|
||||
@@ -142,8 +132,14 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
|
||||
def run(self): # pragma: no cover
|
||||
"""Task wrapper to run policy checking"""
|
||||
result = None
|
||||
try:
|
||||
self.connection.send(self.profiling_wrapper())
|
||||
start = perf_counter()
|
||||
result = self.profiling_wrapper()
|
||||
end = perf_counter()
|
||||
result._exec_time = max((end - start), 0)
|
||||
except Exception as exc: # noqa
|
||||
LOGGER.warning("Policy failed to run", exc=exc)
|
||||
self.connection.send(PolicyResult(False, str(exc)))
|
||||
result = PolicyResult(False, str(exc))
|
||||
finally:
|
||||
self.connection.send(result)
|
||||
|
||||
@@ -33,6 +33,9 @@ class TestPolicyEngine(TestCase):
|
||||
self.policy_raises = ExpressionPolicy.objects.create(
|
||||
name=generate_id(), expression="{{ 0/0 }}"
|
||||
)
|
||||
self.group_member = Group.objects.create(name=generate_id())
|
||||
self.user.groups.add(self.group_member)
|
||||
self.group_non_member = Group.objects.create(name=generate_id())
|
||||
|
||||
def test_engine_empty(self):
|
||||
"""Ensure empty policy list passes"""
|
||||
@@ -51,7 +54,7 @@ class TestPolicyEngine(TestCase):
|
||||
self.assertEqual(result.passing, True)
|
||||
self.assertEqual(result.messages, ("dummy",))
|
||||
|
||||
def test_engine_mode_all(self):
|
||||
def test_engine_mode_all_dyn(self):
|
||||
"""Ensure all policies passes with AND mode (false and true -> false)"""
|
||||
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ALL)
|
||||
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
|
||||
@@ -67,7 +70,7 @@ class TestPolicyEngine(TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
def test_engine_mode_any(self):
|
||||
def test_engine_mode_any_dyn(self):
|
||||
"""Ensure all policies passes with OR mode (false and true -> true)"""
|
||||
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ANY)
|
||||
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
|
||||
@@ -83,6 +86,26 @@ class TestPolicyEngine(TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
def test_engine_mode_all_static(self):
|
||||
"""Ensure all policies passes with OR mode (false and true -> true)"""
|
||||
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ALL)
|
||||
PolicyBinding.objects.create(target=pbm, group=self.group_member, order=0)
|
||||
PolicyBinding.objects.create(target=pbm, group=self.group_non_member, order=1)
|
||||
engine = PolicyEngine(pbm, self.user)
|
||||
result = engine.build().result
|
||||
self.assertEqual(result.passing, False)
|
||||
self.assertEqual(result.messages, ())
|
||||
|
||||
def test_engine_mode_any_static(self):
|
||||
"""Ensure all policies passes with OR mode (false and true -> true)"""
|
||||
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ANY)
|
||||
PolicyBinding.objects.create(target=pbm, group=self.group_member, order=0)
|
||||
PolicyBinding.objects.create(target=pbm, group=self.group_non_member, order=1)
|
||||
engine = PolicyEngine(pbm, self.user)
|
||||
result = engine.build().result
|
||||
self.assertEqual(result.passing, True)
|
||||
self.assertEqual(result.messages, ())
|
||||
|
||||
def test_engine_negate(self):
|
||||
"""Test negate flag"""
|
||||
pbm = PolicyBindingModel.objects.create()
|
||||
|
||||
@@ -77,6 +77,8 @@ class PolicyResult:
|
||||
|
||||
log_messages: list[LogEvent] | None
|
||||
|
||||
_exec_time: int | None
|
||||
|
||||
def __init__(self, passing: bool, *messages: str):
|
||||
self.passing = passing
|
||||
self.messages = messages
|
||||
@@ -84,6 +86,7 @@ class PolicyResult:
|
||||
self.source_binding = None
|
||||
self.source_results = []
|
||||
self.log_messages = []
|
||||
self._exec_time = None
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -68,6 +68,8 @@ class IDToken:
|
||||
at_hash: str | None = None
|
||||
# Session ID, https://openid.net/specs/openid-connect-frontchannel-1_0.html#ClaimsContents
|
||||
sid: str | None = None
|
||||
# JWT ID, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7
|
||||
jti: str | None = None
|
||||
|
||||
claims: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@@ -81,6 +83,7 @@ class IDToken:
|
||||
(token.expires if token.expires is not None else default_token_duration()).timestamp()
|
||||
)
|
||||
id_token.iss = provider.get_issuer(request)
|
||||
id_token.jti = generate_id()
|
||||
id_token.aud = provider.client_id
|
||||
id_token.claims = {}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
@@ -690,18 +691,21 @@ class TestAuthorize(OAuthTestCase):
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
self.client.logout()
|
||||
response = self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "foo://localhost",
|
||||
"ui_locales": "invalid fr",
|
||||
},
|
||||
)
|
||||
parsed = parse_qs(urlparse(response.url).query)
|
||||
self.assertEqual(parsed["locale"], ["fr"])
|
||||
try:
|
||||
response = self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "foo://localhost",
|
||||
"ui_locales": "invalid fr",
|
||||
},
|
||||
)
|
||||
parsed = parse_qs(urlparse(response.url).query)
|
||||
self.assertEqual(parsed["locale"], ["fr"])
|
||||
finally:
|
||||
translation.deactivate()
|
||||
|
||||
@apply_blueprint("default/flow-default-authentication-flow.yaml")
|
||||
def test_ui_locales_invalid(self):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Device backchannel tests"""
|
||||
|
||||
from base64 import b64encode
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
@@ -26,7 +27,7 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
|
||||
provider=self.provider,
|
||||
)
|
||||
|
||||
def test_backchannel_invalid(self):
|
||||
def test_backchannel_invalid_client_id_via_post_body(self):
|
||||
"""Test backchannel"""
|
||||
res = self.client.post(
|
||||
reverse("authentik_providers_oauth2:device"),
|
||||
@@ -50,7 +51,7 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_backchannel(self):
|
||||
def test_backchannel_client_id_via_post_body(self):
|
||||
"""Test backchannel"""
|
||||
res = self.client.post(
|
||||
reverse("authentik_providers_oauth2:device"),
|
||||
@@ -61,3 +62,37 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
|
||||
self.assertEqual(res.status_code, 200)
|
||||
body = loads(res.content.decode())
|
||||
self.assertEqual(body["expires_in"], 60)
|
||||
|
||||
def test_backchannel_invalid_client_id_via_auth_header(self):
|
||||
"""Test backchannel"""
|
||||
creds = b64encode(b"foo:").decode()
|
||||
res = self.client.post(
|
||||
reverse("authentik_providers_oauth2:device"),
|
||||
HTTP_AUTHORIZATION=f"Basic {creds}",
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
res = self.client.post(
|
||||
reverse("authentik_providers_oauth2:device"),
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
# test without application
|
||||
self.application.provider = None
|
||||
self.application.save()
|
||||
res = self.client.post(
|
||||
reverse("authentik_providers_oauth2:device"),
|
||||
data={
|
||||
"client_id": "test",
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_backchannel_client_id_via_auth_header(self):
|
||||
"""Test backchannel"""
|
||||
creds = b64encode(f"{self.provider.client_id}:".encode()).decode()
|
||||
res = self.client.post(
|
||||
reverse("authentik_providers_oauth2:device"),
|
||||
HTTP_AUTHORIZATION=f"Basic {creds}",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
body = loads(res.content.decode())
|
||||
self.assertEqual(body["expires_in"], 60)
|
||||
|
||||
@@ -16,7 +16,7 @@ from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.errors import DeviceCodeError
|
||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||
from authentik.providers.oauth2.utils import TokenResponse
|
||||
from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth
|
||||
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
|
||||
|
||||
LOGGER = get_logger()
|
||||
@@ -32,7 +32,7 @@ class DeviceView(View):
|
||||
|
||||
def parse_request(self):
|
||||
"""Parse incoming request"""
|
||||
client_id = self.request.POST.get("client_id", None)
|
||||
client_id, _ = extract_client_auth(self.request)
|
||||
if not client_id:
|
||||
raise DeviceCodeError("invalid_client")
|
||||
provider = OAuth2Provider.objects.filter(client_id=client_id).first()
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
|
||||
from ldap3.core.exceptions import LDAPException, LDAPInsufficientAccessRightsResult, LDAPSchemaError
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import (
|
||||
Group,
|
||||
@@ -31,6 +32,7 @@ from authentik.tasks.schedules.common import ScheduleSpec
|
||||
LDAP_TIMEOUT = 15
|
||||
LDAP_UNIQUENESS = "ldap_uniq"
|
||||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def flatten(value: Any) -> Any:
|
||||
@@ -268,6 +270,7 @@ class LDAPSource(IncomingSyncSource):
|
||||
)
|
||||
|
||||
if self.start_tls:
|
||||
LOGGER.debug("Connection StartTLS", source=self)
|
||||
conn.start_tls(read_server_info=False)
|
||||
try:
|
||||
successful = conn.bind()
|
||||
@@ -278,7 +281,9 @@ class LDAPSource(IncomingSyncSource):
|
||||
# See https://github.com/goauthentik/authentik/issues/4590
|
||||
# See also https://github.com/goauthentik/authentik/issues/3399
|
||||
if server_kwargs.get("get_info", ALL) == NONE:
|
||||
LOGGER.warning("Failed to connect after schema downgrade", source=self, exc=exc)
|
||||
raise exc
|
||||
LOGGER.warning("Downgrading connection to no schema info", source=self, exc=exc)
|
||||
server_kwargs["get_info"] = NONE
|
||||
return self.connection(server, server_kwargs, connection_kwargs)
|
||||
finally:
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from lxml.etree import _Element # nosec
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.common.saml.constants import (
|
||||
@@ -217,9 +218,8 @@ class SAMLSource(Source):
|
||||
def property_mapping_type(self) -> type[PropertyMapping]:
|
||||
return SAMLSourcePropertyMapping
|
||||
|
||||
def get_base_user_properties(self, root: Any, name_id: Any, **kwargs):
|
||||
def get_base_user_properties(self, root: _Element, assertion: _Element, name_id: Any, **kwargs):
|
||||
attributes = {}
|
||||
assertion = root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
if assertion is None:
|
||||
raise ValueError("Assertion element not found")
|
||||
attribute_statement = assertion.find(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
|
||||
|
||||
@@ -66,6 +66,8 @@ class ResponseProcessor:
|
||||
|
||||
_http_request: HttpRequest
|
||||
|
||||
_assertion: _Element | None = None
|
||||
|
||||
def __init__(self, source: SAMLSource, request: HttpRequest):
|
||||
self._source = source
|
||||
self._http_request = request
|
||||
@@ -122,6 +124,7 @@ class ResponseProcessor:
|
||||
index_of,
|
||||
decrypted_assertion,
|
||||
)
|
||||
self._assertion = decrypted_assertion
|
||||
|
||||
def _verify_signature(self, signature_node: _Element):
|
||||
"""Verify a single signature node"""
|
||||
@@ -162,6 +165,10 @@ class ResponseProcessor:
|
||||
raise InvalidSignature("No Signature exists in the Assertion element.")
|
||||
|
||||
self._verify_signature(signature_nodes[0])
|
||||
parent = signature_nodes[0].getparent()
|
||||
if parent is None or parent.tag != f"{{{NS_SAML_ASSERTION}}}Assertion":
|
||||
raise InvalidSignature("No Signature exists in the Assertion element.")
|
||||
self._assertion = parent
|
||||
|
||||
def _verify_request_id(self):
|
||||
if self._source.allow_idp_initiated:
|
||||
@@ -239,14 +246,21 @@ class ResponseProcessor:
|
||||
identifier=str(name_id.text),
|
||||
user_info={
|
||||
"root": self._root,
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
},
|
||||
policy_context={},
|
||||
)
|
||||
|
||||
def get_assertion(self) -> Element | None:
|
||||
"""Get assertion element, if we have a signed assertion"""
|
||||
if self._assertion is not None:
|
||||
return self._assertion
|
||||
return self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
|
||||
def _get_name_id(self) -> Element:
|
||||
"""Get NameID Element"""
|
||||
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
assertion = self.get_assertion()
|
||||
if assertion is None:
|
||||
raise ValueError("Assertion element not found")
|
||||
subject = assertion.find(f"{{{NS_SAML_ASSERTION}}}Subject")
|
||||
@@ -299,6 +313,7 @@ class ResponseProcessor:
|
||||
identifier=str(name_id.text),
|
||||
user_info={
|
||||
"root": self._root,
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
},
|
||||
policy_context={
|
||||
|
||||
68
authentik/sources/saml/tests/fixtures/response_signed_assertion_dup.xml
vendored
Normal file
68
authentik/sources/saml/tests/fixtures/response_signed_assertion_dup.xml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
|
||||
<samlp:Status>
|
||||
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||
</samlp:Status>
|
||||
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_other_id_pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
|
||||
<saml:Subject>
|
||||
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">bad</saml:NameID>
|
||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
|
||||
<ds:Reference URI="#pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>zNDuGxwP4gVkv/Dzt7kiKo/4gzk=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>GLP/vE8uxerB0uDpPslUgLPBL6ePQB619MoQ0I2Y5lAtFE6CB1zh8BnzChRx/bFjNy4byfOe8mFfM0r7WUi1PJOFWyUPoatdLl7wHHBIRTnPpYmu3Tb2Gz0sOP0F8wW7JkBft5gJfVw49nk5si9/3Q3o52jnJZ7dPtqfIOh8uNeopikK0HLF6sU05qCCtjcXfniEnLQFNBFMo9uY5GQqmR5n3nqPz1wYyyfFOAbVmGgBIoO2PfGX2GVLQhltc9qf2JMhks4jgZsZ8iLUIiH1lcLGWZEEs94k8k0P6gSv1uZ7Vbhksd/N9Jq9pCVuEJ/jRPcAdVjzbxqKQAj6ELwr8O6fepTzA+CAdwEolBnx/C6TmSbVZ+IWk6QUGe4x4+IAukC+0hkKENlO0ELOScksvyhpgHbxNA4rp+DhGupCaO/I2RrsQkmvavbqm+wSEspK7scK112SDunjDvqPHsPYgukD33T/97PxTLorg2kKP9HHJwPJKoXXeyOGcA6vwK+RqrAlZ2dLGAgcXo+sJcdCLuvxDNz9VXofBjBZIKVKdmYhm0QJaPYHtuQsAyFavQhdOBOmGHb7QX3YE3Xy4dX4LymtT+Jlb1I4FJSht/9HUIHW1FdhfDak4f7gUgjuMamMddLD0jVgeESupSREzFv/gj2IrctkbgjAO0iuuiBgKMg=</ds:SignatureValue>
|
||||
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
|
||||
<saml:Subject>
|
||||
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
|
||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
|
||||
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
</samlp:Response>
|
||||
@@ -36,7 +36,9 @@ class TestPropertyMappings(TestCase):
|
||||
|
||||
def test_user_base_properties(self):
|
||||
"""Test user base properties"""
|
||||
properties = self.source.get_base_user_properties(root=ROOT, name_id=NAME_ID)
|
||||
properties = self.source.get_base_user_properties(
|
||||
root=ROOT, assertion=ROOT.find(f"{{{NS_SAML_ASSERTION}}}Assertion"), name_id=NAME_ID
|
||||
)
|
||||
self.assertEqual(
|
||||
properties,
|
||||
{
|
||||
@@ -49,7 +51,11 @@ class TestPropertyMappings(TestCase):
|
||||
|
||||
def test_group_base_properties(self):
|
||||
"""Test group base properties"""
|
||||
properties = self.source.get_base_user_properties(root=ROOT_GROUPS, name_id=NAME_ID)
|
||||
properties = self.source.get_base_user_properties(
|
||||
root=ROOT_GROUPS,
|
||||
assertion=ROOT_GROUPS.find(f"{{{NS_SAML_ASSERTION}}}Assertion"),
|
||||
name_id=NAME_ID,
|
||||
)
|
||||
self.assertEqual(properties["groups"], ["group 1", "group 2"])
|
||||
for group_id in ["group 1", "group 2"]:
|
||||
properties = self.source.get_base_group_properties(root=ROOT, group_id=group_id)
|
||||
|
||||
@@ -164,6 +164,31 @@ class TestResponseProcessor(TestCase):
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
|
||||
def test_verification_assertion_duplicate(self):
|
||||
"""Test verifying signature inside assertion, where the response has another assertion
|
||||
before our signed assertion"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
kp = CertificateKeyPair.objects.create(
|
||||
name=generate_id(),
|
||||
certificate_data=key,
|
||||
)
|
||||
self.source.verification_kp = kp
|
||||
self.source.signed_assertion = True
|
||||
self.source.signed_response = False
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
data={
|
||||
"SAMLResponse": b64encode(
|
||||
load_fixture("fixtures/response_signed_assertion_dup.xml").encode()
|
||||
).decode()
|
||||
},
|
||||
)
|
||||
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
self.assertNotEqual(parser._get_name_id().text, "bad")
|
||||
self.assertEqual(parser._get_name_id().text, "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
|
||||
|
||||
def test_verification_response(self):
|
||||
"""Test verifying signature inside response"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib.auth.views import redirect_to_login
|
||||
from django.http.request import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import get_user
|
||||
from authentik.core.models import Session
|
||||
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
|
||||
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
|
||||
@@ -54,11 +55,13 @@ class SessionBindingBroken(SentryIgnoredException):
|
||||
|
||||
def logout_extra(request: HttpRequest, exc: SessionBindingBroken):
|
||||
"""Similar to django's logout method, but able to carry more info to the signal"""
|
||||
# Dispatch the signal before the user is logged out so the receivers have a
|
||||
# chance to find out *who* logged out.
|
||||
user = getattr(request, "user", None)
|
||||
# Since this middleware runs before the AuthenticationMiddleware, we can't use `request.user`
|
||||
# as it hasn't been populated yet.
|
||||
user = get_user(request)
|
||||
if not getattr(user, "is_authenticated", True):
|
||||
user = None
|
||||
# Dispatch the signal before the user is logged out so the receivers have a
|
||||
# chance to find out *who* logged out.
|
||||
user_logged_out.send(
|
||||
sender=user.__class__, request=request, user=user, event_extra=exc.to_event()
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ from django.utils.timezone import now
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import AuthenticatedSession, Session
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
@@ -270,6 +272,7 @@ class TestUserLoginStage(FlowTestCase):
|
||||
|
||||
def test_session_binding_broken(self):
|
||||
"""Test session binding"""
|
||||
Event.objects.all().delete()
|
||||
self.client.force_login(self.user)
|
||||
session = self.client.session
|
||||
session[Session.Keys.LAST_IP] = "192.0.2.1"
|
||||
@@ -285,3 +288,5 @@ class TestUserLoginStage(FlowTestCase):
|
||||
)
|
||||
+ f"?{NEXT_ARG_NAME}={reverse("authentik_api:user-me")}",
|
||||
)
|
||||
event = Event.objects.filter(action=EventAction.LOGOUT).first()
|
||||
self.assertEqual(event.user, get_user(self.user))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2026.2.0-rc1 Blueprint schema",
|
||||
"title": "authentik 2026.2.1-rc1 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
|
||||
@@ -29,7 +29,7 @@ entries:
|
||||
password=request.user.password
|
||||
)
|
||||
# ...otherwise we set an immutable ID based on the user's UID
|
||||
user["on_premises_immutable_id"] = request.user.uid,
|
||||
user["on_premises_immutable_id"] = request.user.uid
|
||||
return user
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/microsoft_entra/group
|
||||
|
||||
@@ -104,7 +104,11 @@ type OutpostConfig struct {
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
Path string `yaml:"path" env:"PATH, overwrite"`
|
||||
Path string `yaml:"path" env:"PATH, overwrite"`
|
||||
TimeoutHttpReadHeader string `yaml:"timeout_http_read_header" env:"TIMEOUT_HTTP_READ_HEADER, overwrite"`
|
||||
TimeoutHttpRead string `yaml:"timeout_http_read" env:"TIMEOUT_HTTP_READ, overwrite"`
|
||||
TimeoutHttpWrite string `yaml:"timeout_http_write" env:"TIMEOUT_HTTP_WRITE, overwrite"`
|
||||
TimeoutHttpIdle string `yaml:"timeout_http_idle" env:"TIMEOUT_HTTP_IDLE, overwrite"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026.2.0-rc1
|
||||
2026.2.1-rc1
|
||||
@@ -76,7 +76,7 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path)
|
||||
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.EscapedPath())
|
||||
|
||||
if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".")
|
||||
|
||||
@@ -27,6 +27,24 @@ func TestRedirectToStart_Proxy(t *testing.T) {
|
||||
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestRedirectToStart_Proxy_EncodedSlash(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr()
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
// %2F is a URL-encoded forward slash, used by apps like RabbitMQ in queue paths
|
||||
req, _ := http.NewRequest("GET", "/api/queues/%2F/MYChannelCreated", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.redirectToStart(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Contains(t, loc.String(), "%252F", "encoded slash %2F must be preserved in redirect URL")
|
||||
|
||||
s, _ := a.sessions.Get(req, a.SessionName())
|
||||
assert.Contains(t, s.Values[constants.SessionRedirect].(string), "%2F", "encoded slash %2F must be preserved in session redirect")
|
||||
}
|
||||
|
||||
func TestRedirectToStart_Forward(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_SINGLE.Ptr()
|
||||
|
||||
@@ -3,15 +3,26 @@ package web
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"goauthentik.io/internal/config"
|
||||
)
|
||||
|
||||
func durationOrFallback(raw string, fallback time.Duration) time.Duration {
|
||||
p, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func Server(h http.Handler) *http.Server {
|
||||
c := config.Get()
|
||||
return &http.Server{
|
||||
Handler: h,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
ReadHeaderTimeout: durationOrFallback(c.Web.TimeoutHttpReadHeader, 5*time.Second),
|
||||
ReadTimeout: durationOrFallback(c.Web.TimeoutHttpRead, 30*time.Second),
|
||||
WriteTimeout: durationOrFallback(c.Web.TimeoutHttpWrite, 60*time.Second),
|
||||
IdleTimeout: durationOrFallback(c.Web.TimeoutHttpIdle, 120*time.Second),
|
||||
MaxHeaderBytes: http.DefaultMaxHeaderBytes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2026.2.0-rc1
|
||||
Default: 2026.2.1-rc1
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.1-rc1}
|
||||
ports:
|
||||
- ${COMPOSE_PORT_HTTP:-9000}:9000
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.1-rc1}
|
||||
restart: unless-stopped
|
||||
shm_size: 512mb
|
||||
user: root
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.1-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.1-rc1",
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@goauthentik/eslint-config": "./packages/eslint-config",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.1-rc1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import platform
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from multiprocessing import set_start_method
|
||||
from typing import Any
|
||||
|
||||
from django.apps.registry import apps
|
||||
@@ -69,7 +71,10 @@ class Command(BaseCommand):
|
||||
args.pid_file = pid_file
|
||||
|
||||
args.verbose = verbosity - 1
|
||||
|
||||
# > On macOS [...] the fork start method should be considered unsafe
|
||||
# https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods
|
||||
if not platform.system() == "Darwin":
|
||||
set_start_method("fork")
|
||||
connections.close_all()
|
||||
sys.exit(main(args)) # type: ignore[no-untyped-call]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2026.2.0-rc1"
|
||||
version = "2026.2.1-rc1"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.14.*"
|
||||
@@ -9,7 +9,7 @@ dependencies = [
|
||||
"argon2-cffi==25.1.0",
|
||||
"cachetools==7.0.0",
|
||||
"channels==4.3.2",
|
||||
"cryptography==46.0.4",
|
||||
"cryptography==46.0.5",
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2026.2.0-rc1
|
||||
version: 2026.2.1-rc1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@@ -42144,15 +42144,15 @@ components:
|
||||
readOnly: true
|
||||
opened_on:
|
||||
type: string
|
||||
format: date
|
||||
format: date-time
|
||||
readOnly: true
|
||||
grace_period_end:
|
||||
type: string
|
||||
format: date
|
||||
format: date-time
|
||||
readOnly: true
|
||||
next_review_date:
|
||||
type: string
|
||||
format: date
|
||||
format: date-time
|
||||
readOnly: true
|
||||
reviews:
|
||||
type: array
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e -x -o pipefail
|
||||
hash="$(git rev-parse HEAD || openssl rand -base64 36 | sha256sum)"
|
||||
|
||||
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
|
||||
AUTHENTIK_IMAGE="authentik.invalid/goauthentik/server"
|
||||
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
|
||||
|
||||
if [ -f lifecycle/container/.env ]; then
|
||||
@@ -24,7 +24,7 @@ if [[ -v BUILD ]]; then
|
||||
make gen-client-go
|
||||
touch lifecycle/container/.env
|
||||
|
||||
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
|
||||
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" -f lifecycle/container/Dockerfile .
|
||||
fi
|
||||
|
||||
docker compose -f lifecycle/container/compose.yml up --no-start
|
||||
|
||||
92
uv.lock
generated
92
uv.lock
generated
@@ -221,7 +221,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authentik"
|
||||
version = "2026.2.0rc1"
|
||||
version = "2026.2.1rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "ak-guardian" },
|
||||
@@ -338,7 +338,7 @@ requires-dist = [
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.0" },
|
||||
{ name = "channels", specifier = "==4.3.2" },
|
||||
{ name = "cryptography", specifier = "==46.0.4" },
|
||||
{ name = "cryptography", specifier = "==46.0.5" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
{ name = "deepmerge", specifier = "==2.0" },
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
@@ -932,55 +932,55 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.4"
|
||||
version = "46.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
77
web/package-lock.json
generated
77
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.1-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.1-rc1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
@@ -188,7 +188,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2610,6 +2609,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
@@ -3879,6 +3879,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": {
|
||||
"version": "0.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
|
||||
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-reference": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.1.tgz",
|
||||
@@ -4017,7 +4029,6 @@
|
||||
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.25"
|
||||
@@ -4347,7 +4358,8 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
@@ -4722,7 +4734,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz",
|
||||
"integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -4741,7 +4752,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
|
||||
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -4831,7 +4841,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -5072,7 +5081,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz",
|
||||
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -5538,7 +5546,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6081,7 +6088,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -6370,7 +6376,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -6402,7 +6407,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
@@ -6673,7 +6677,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -7068,7 +7071,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -7229,7 +7231,6 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@@ -7497,6 +7498,7 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz",
|
||||
"integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
@@ -7509,6 +7511,7 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
|
||||
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
@@ -7559,7 +7562,8 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
@@ -7854,7 +7858,6 @@
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -7948,7 +7951,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9240,6 +9242,7 @@
|
||||
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz",
|
||||
"integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
|
||||
}
|
||||
@@ -10849,7 +10852,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
|
||||
"integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-element": "^4.2.0",
|
||||
@@ -11112,6 +11114,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -13555,7 +13558,6 @@
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
@@ -13657,7 +13659,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -13692,6 +13693,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -13706,6 +13708,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -13717,7 +13720,8 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
@@ -13925,7 +13929,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
|
||||
"integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ramda"
|
||||
@@ -14005,7 +14008,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14015,7 +14017,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -14552,7 +14553,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -15153,13 +15153,15 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.0.1.tgz",
|
||||
"integrity": "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/sort-package-json": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.5.0.tgz",
|
||||
"integrity": "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-indent": "^7.0.1",
|
||||
"detect-newline": "^4.0.1",
|
||||
@@ -15294,7 +15296,6 @@
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.7.tgz",
|
||||
"integrity": "sha512-LFKSuZyF6EW2/Kkl5d7CvqgwhXXfuWv+aLBuoc616boLKJ3mxXuea+GxIgfk02NEyTKctJ0QsnSh5pAomf6Qkg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^2.0.1",
|
||||
@@ -15695,6 +15696,7 @@
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
@@ -15900,6 +15902,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
|
||||
"integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-json": {
|
||||
"version": "0.24.8",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz",
|
||||
@@ -16142,7 +16156,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16156,7 +16169,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
|
||||
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
@@ -16575,7 +16587,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16664,7 +16675,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -17357,7 +17367,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.1-rc1",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -89,7 +89,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
html` <ak-forms-modal>
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.stageObj?.component, {
|
||||
slot: "form",
|
||||
instancePk: item.pk,
|
||||
instancePk: item.stageObj?.pk,
|
||||
actionLabel: msg("Update"),
|
||||
headline: msg(str`Update ${item.stageObj?.verboseName}`, {
|
||||
id: "form.headline.update",
|
||||
|
||||
@@ -243,8 +243,10 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
|
||||
name="minReviewersIsPerGroup"
|
||||
?checked=${this.instance?.minReviewersIsPerGroup ?? false}
|
||||
label=${msg("Min reviewers is per-group")}
|
||||
help=${msg(
|
||||
"If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.",
|
||||
.help=${msg(
|
||||
html`If checked, approving a review will require at least that many users from
|
||||
<em>each</em> of the selected groups. When disabled, the value is a total
|
||||
across all groups.`,
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
@@ -112,7 +112,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
return html`<ak-forms-modal>
|
||||
${StrictUnsafe<CustomFormElementTagName>(item.policyObj?.component, {
|
||||
slot: "form",
|
||||
instancePk: item.pk,
|
||||
instancePk: item.policyObj?.pk,
|
||||
actionLabel: msg("Update"),
|
||||
headline: msg(str`Update ${item.policyObj?.name}`, {
|
||||
id: "form.headline.update",
|
||||
|
||||
@@ -63,10 +63,13 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
|
||||
}
|
||||
|
||||
send(data: RoleAssignData): Promise<unknown> {
|
||||
const [app, _model] = this.model?.split(".") || "";
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssign({
|
||||
uuid: data.role,
|
||||
permissionAssignRequest: {
|
||||
permissions: Object.keys(data.permissions).filter((key) => data.permissions[key]),
|
||||
permissions: Object.keys(data.permissions)
|
||||
.filter((key) => data.permissions[key])
|
||||
.map((permission) => `${app}.${permission}`),
|
||||
model: this.model!,
|
||||
objectPk: this.objectPk,
|
||||
},
|
||||
|
||||
@@ -53,6 +53,10 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
|
||||
[EventActions.EmailSent, msg("Email sent")],
|
||||
[EventActions.UpdateAvailable, msg("Update available")],
|
||||
[EventActions.ExportReady, msg("Data export ready")],
|
||||
[EventActions.ReviewInitiated, msg("Review initiated")],
|
||||
[EventActions.ReviewOverdue, msg("Review overdue")],
|
||||
[EventActions.ReviewAttested, msg("Review attested")],
|
||||
[EventActions.ReviewCompleted, msg("Review completed")],
|
||||
]);
|
||||
|
||||
export const actionToLabel = (action?: EventActions): string =>
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AkSwitchInput extends AKElement {
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
help: string | TemplateResult = "";
|
||||
|
||||
/**
|
||||
* For more complex help instructions, provide a template result.
|
||||
@@ -47,11 +47,13 @@ export class AkSwitchInput extends AKElement {
|
||||
#fieldID: string = IDGenerator.randomID();
|
||||
|
||||
protected renderHelp() {
|
||||
const helpText = this.help.trim();
|
||||
const helpContent = typeof this.help === "string" ? this.help.trim() : this.help;
|
||||
|
||||
return [
|
||||
helpText
|
||||
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">${helpText}</p>`
|
||||
helpContent
|
||||
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">
|
||||
${helpContent}
|
||||
</p>`
|
||||
: nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
:host {
|
||||
:host,
|
||||
ak-loading-overlay.style-scope {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
@@ -11,6 +12,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
:host([topmost]) {
|
||||
:host([topmost]),
|
||||
ak-loading-overlay[topmost].style-scope {
|
||||
z-index: var(--pf-global--ZIndex--2xl);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
|
||||
import { LitElement, nothing } from "lit";
|
||||
import { LitElement, nothing, PropertyDeclaration } from "lit";
|
||||
import { html as staticHTML, unsafeStatic } from "lit-html/static.js";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
|
||||
@@ -35,21 +35,57 @@ export function isAKElementConstructor(input: CustomElementConstructor): input i
|
||||
return Object.prototype.isPrototypeOf.call(AKElement, input);
|
||||
}
|
||||
|
||||
function getPrefix(type: unknown, isProperty: boolean) {
|
||||
if (isProperty) {
|
||||
return ".";
|
||||
export const Prefix = {
|
||||
Property: ".",
|
||||
BooleanAttribute: "?",
|
||||
Attribute: "",
|
||||
} as const;
|
||||
|
||||
export type Prefix = (typeof Prefix)[keyof typeof Prefix];
|
||||
|
||||
/**
|
||||
* Given a Lit property declaration, determine the appropriate prefix for rendering the property as either a property or an attribute, based on the declaration's type and attribute configuration.
|
||||
*
|
||||
* @param propDeclaration The Lit property declaration to analyze.
|
||||
* @returns The determined prefix for rendering the property.
|
||||
*/
|
||||
function resolvePrefix<T extends PropertyDeclaration<unknown, unknown>>(
|
||||
propDeclaration: T,
|
||||
): Prefix {
|
||||
if (!propDeclaration.attribute) {
|
||||
return Prefix.Property;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
switch (propDeclaration.type) {
|
||||
case String:
|
||||
return "";
|
||||
return Prefix.Attribute;
|
||||
case Boolean:
|
||||
return "?";
|
||||
return Prefix.BooleanAttribute;
|
||||
default:
|
||||
return ".";
|
||||
return Prefix.Property;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a Lit property declaration, a resolved prefix, and the original property key,
|
||||
* determine the appropriate name to use for rendering the property,
|
||||
* taking into account any custom attribute name specified in the declaration.
|
||||
*/
|
||||
function resolvePropertyName<T extends PropertyDeclaration<unknown, unknown>>(
|
||||
propDeclaration: T,
|
||||
prefix: Prefix,
|
||||
key: string,
|
||||
): string {
|
||||
if (prefix === Prefix.Property) {
|
||||
return key;
|
||||
}
|
||||
|
||||
if ("attribute" in propDeclaration && typeof propDeclaration.attribute === "string") {
|
||||
return propDeclaration.attribute;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
/**
|
||||
* Given a pre-registered custom element tag name and a record of properties,
|
||||
* render the element with the given properties applied.
|
||||
@@ -63,16 +99,19 @@ export function StrictUnsafe<T extends CustomElementTagName>(
|
||||
tagName: T,
|
||||
props?: LitPropertyRecord<HTMLElementTagNameMap[T]>,
|
||||
): SlottedTemplateResult;
|
||||
|
||||
export function StrictUnsafe<T extends AKElement>(
|
||||
tagName: string,
|
||||
props?: LitPropertyRecord<T>,
|
||||
): SlottedTemplateResult;
|
||||
|
||||
export function StrictUnsafe<T extends string>(
|
||||
tagName: string,
|
||||
props?: T extends CustomElementTagName
|
||||
? LitPropertyRecord<HTMLElementTagNameMap[T]>
|
||||
: LitPropertyRecord<LitElement>,
|
||||
): SlottedTemplateResult;
|
||||
|
||||
export function StrictUnsafe<T extends string>(
|
||||
tagName: string,
|
||||
props?: T extends CustomElementTagName
|
||||
@@ -103,17 +142,20 @@ export function StrictUnsafe<T extends string>(
|
||||
|
||||
const filteredProps: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(props || {})) {
|
||||
const propDeclaration = elementProperties.get(key);
|
||||
for (const [propName, propValue] of Object.entries(props || {})) {
|
||||
const propDeclaration = elementProperties.get(propName);
|
||||
|
||||
if (propDeclaration) {
|
||||
const prefix = getPrefix(propDeclaration.type, !propDeclaration.attribute);
|
||||
filteredProps[`${prefix}${key}`] = value;
|
||||
const prefix = resolvePrefix(propDeclaration);
|
||||
const name = resolvePropertyName(propDeclaration, prefix, propName);
|
||||
|
||||
filteredProps[`${prefix}${name}`] = propValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observedAttributes.has(key) || key in ElementConstructor.prototype) {
|
||||
filteredProps[key] = String(value);
|
||||
if (observedAttributes.has(propName) || propName in ElementConstructor.prototype) {
|
||||
filteredProps[propName] = String(propValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "../styles/authentik/components/Login/login.css";
|
||||
|
||||
:host {
|
||||
:host,
|
||||
ak-flow-executor.style-scope {
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
flex-flow: column nowrap;
|
||||
@@ -49,14 +50,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
filter: var(--ak-global--background-contrast-Filter);
|
||||
filter: var(--ak-global--BackgroundContrastFilter);
|
||||
|
||||
grid-area: header;
|
||||
|
||||
/* At least a third of the card cut-off is available. */
|
||||
@media (width <= 61.25rem) and (height <= 61.25rem) {
|
||||
--ak-global--background-contrast-Filter: none;
|
||||
--ak-c-flow-executor__locale-select--Color: var(--ak-global--background-contrast);
|
||||
--ak-global--BackgroundContrastFilter: none;
|
||||
--ak-c-flow-executor__locale-select--Color: var(--ak-c-login__main--Color);
|
||||
|
||||
grid-area: main;
|
||||
}
|
||||
@@ -79,7 +80,7 @@
|
||||
@media (min-width: 70rem) and (min-height: 17.5rem) {
|
||||
:host([data-layout^="sidebar"]),
|
||||
[data-layout^="sidebar"] /* Compatibility mode */ {
|
||||
--ak-global--background-contrast-Filter: none !important;
|
||||
--ak-global--BackgroundContrastFilter: none !important;
|
||||
|
||||
[part="locale-select"] {
|
||||
--ak-c-flow-executor__locale-select--Color: inherit !important;
|
||||
|
||||
@@ -68,6 +68,18 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
*
|
||||
* @attr {string} slug - The slug of the flow to execute.
|
||||
* @prop {ChallengeTypes | null} challenge - The current challenge to render.
|
||||
*
|
||||
* @part main - The main container for the flow content.
|
||||
* @part content - The container for the stage content.
|
||||
* @part content-iframe - The iframe element when using a frame background layout.
|
||||
* @part footer - The footer container.
|
||||
* @part locale-select - The locale select component.
|
||||
* @part branding - The branding element, used for the background image in some layouts.
|
||||
* @part loading-overlay - The loading overlay element.
|
||||
* @part challenge-additional-actions - Container in stages which have additional actions.
|
||||
* @part challenge-footer-band - Container for the stage footer, used for additional actions in some stages.
|
||||
* @part locale-select-label - The label of the locale select component.
|
||||
* @part locale-select-select - The select element of the locale select component.
|
||||
*/
|
||||
@customElement("ak-flow-executor")
|
||||
export class FlowExecutor
|
||||
@@ -538,7 +550,7 @@ export class FlowExecutor
|
||||
//#region Render
|
||||
|
||||
protected renderLoading(): SlottedTemplateResult {
|
||||
return html`<slot class="slotted-content" name="placeholder"></slot>`;
|
||||
return html`<slot name="placeholder"></slot>`;
|
||||
}
|
||||
|
||||
protected renderFrameBackground(): SlottedTemplateResult {
|
||||
@@ -567,6 +579,21 @@ export class FlowExecutor
|
||||
});
|
||||
}
|
||||
|
||||
protected renderFooter(): SlottedTemplateResult {
|
||||
return guard([this.layout], () => {
|
||||
return html`<footer
|
||||
aria-label=${msg("Site footer")}
|
||||
name="site-footer"
|
||||
part="footer"
|
||||
class="pf-c-login__footer ${this.layout === FlowLayoutEnum.Stacked
|
||||
? "pf-m-dark"
|
||||
: ""}"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</footer>`;
|
||||
});
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const { component } = this.challenge || {};
|
||||
|
||||
@@ -593,11 +620,11 @@ export class FlowExecutor
|
||||
})}
|
||||
</div>
|
||||
${this.loading && this.challenge
|
||||
? html`<ak-loading-overlay></ak-loading-overlay>`
|
||||
? html`<ak-loading-overlay part="loading-overlay"></ak-loading-overlay>`
|
||||
: nothing}
|
||||
${component ? until(this.renderChallenge(component)) : this.renderLoading()}
|
||||
</main>
|
||||
<slot name="footer"></slot>`;
|
||||
${this.renderFooter()}`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host {
|
||||
:host,
|
||||
ak-flow-inspector.style-scope {
|
||||
background-color: var(--pf-c-notification-drawer--BackgroundColor);
|
||||
--pf-c-drawer__panel--BackgroundColor: var(--pf-global--BackgroundColor--150) !important;
|
||||
}
|
||||
|
||||
41
web/src/flow/FormStatic.css
Normal file
41
web/src/flow/FormStatic.css
Normal file
@@ -0,0 +1,41 @@
|
||||
:host,
|
||||
ak-form-static.style-scope {
|
||||
margin-block-start: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-flow: wrap;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-c-avatar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.primary-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
max-width: 20rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
display: box;
|
||||
display: -webkit-box;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { LitFC } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { isDefaultAvatar } from "#elements/utils/images";
|
||||
|
||||
import Styles from "#flow/FormStatic.css";
|
||||
|
||||
import {
|
||||
AccessDeniedChallenge,
|
||||
AuthenticatorDuoChallenge,
|
||||
@@ -18,7 +20,7 @@ import {
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing } from "lit";
|
||||
import { CSSResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
|
||||
@@ -26,6 +28,8 @@ import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
|
||||
@customElement("ak-form-static")
|
||||
export class AKFormStatic extends AKElement {
|
||||
static styles: CSSResult[] = [PFAvatar, Styles];
|
||||
|
||||
public override role = "banner";
|
||||
public override ariaLabel = msg("User information");
|
||||
|
||||
@@ -35,59 +39,12 @@ export class AKFormStatic extends AKElement {
|
||||
@property({ type: String })
|
||||
public username: string = "";
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFAvatar,
|
||||
css`
|
||||
:host {
|
||||
margin-block-start: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-flow: wrap;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-c-avatar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.primary-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
max-width: 20rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
display: box;
|
||||
display: -webkit-box;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected override render() {
|
||||
if (!this.username) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="primary-content">
|
||||
return html`<div class="primary-content">
|
||||
${this.avatar && !isDefaultAvatar(this.avatar)
|
||||
? html`<img
|
||||
class="pf-c-avatar"
|
||||
@@ -105,8 +62,7 @@ export class AKFormStatic extends AKElement {
|
||||
</div>
|
||||
<div class="links">
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +94,7 @@ export const FlowUserDetails: LitFC<FlowUserDetailsProps> = ({ challenge }) => {
|
||||
[pendingUserAvatar, pendingUser, flowInfo],
|
||||
() =>
|
||||
html`<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
avatar=${ifPresent(pendingUserAvatar)}
|
||||
.avatar=${ifPresent(pendingUserAvatar)}
|
||||
username=${ifPresent(pendingUser)}
|
||||
>
|
||||
${flowInfo?.cancelUrl
|
||||
|
||||
@@ -6,45 +6,60 @@ import { AKElement } from "#elements/Base";
|
||||
import { FooterLink } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { map } from "lit/directives/map.js";
|
||||
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
|
||||
const styles = css`
|
||||
.pf-c-list a {
|
||||
color: unset;
|
||||
}
|
||||
ul.pf-c-list.pf-m-inline {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
column-gap: var(--pf-global--spacer--xl);
|
||||
row-gap: var(--pf-global--spacer--md);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* @part list - The list element containing the links
|
||||
* @part list-item - Each item in the list, including the "Powered by authentik" item
|
||||
* @part list-item-link - The link element for each item, if applicable
|
||||
*/
|
||||
@customElement("ak-brand-links")
|
||||
export class BrandLinks extends AKElement {
|
||||
static styles = [PFList, styles];
|
||||
/**
|
||||
* Rendering in the light DOM ensures consistent styling across some of the
|
||||
* more complex flow environments, such as...
|
||||
*
|
||||
* - When JavaScript is not available, such as on error pages.
|
||||
* - During the initial loading of the page, before the web components are fully initialized.
|
||||
* - After the flow executor has initialized, to avoid repaint issues.
|
||||
*/
|
||||
protected createRenderRoot(): HTMLElement | DocumentFragment {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public links: FooterLink[] = globalAK().brand.uiFooterLinks || [];
|
||||
|
||||
render() {
|
||||
return html`<ul aria-label=${msg("Site links")} class="pf-c-list pf-m-inline" part="list">
|
||||
${map(this.links, (link) => {
|
||||
const links = [
|
||||
...this.links,
|
||||
{
|
||||
name: msg("Powered by authentik"),
|
||||
href: null,
|
||||
},
|
||||
];
|
||||
return html`<ul
|
||||
aria-label=${msg("Site links")}
|
||||
class="pf-c-list pf-m-inline"
|
||||
part="list"
|
||||
data-count=${links.length}
|
||||
>
|
||||
${map(links, (link, idx) => {
|
||||
const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
|
||||
|
||||
if (link.href) {
|
||||
return html`<li><a href="${link.href}">${children}</a></li>`;
|
||||
}
|
||||
|
||||
return html`<li part="list-item">
|
||||
<span>${children}</span>
|
||||
return html`<li
|
||||
part="list-item"
|
||||
data-index=${idx}
|
||||
data-kind=${link.href ? "link" : "text"}
|
||||
data-track-name=${idx === 0 ? "start" : idx === links.length - 1 ? "end" : idx}
|
||||
>
|
||||
${link.href
|
||||
? html`<a part="list-item-link" href=${link.href}>${children}</a>`
|
||||
: children}
|
||||
</li>`;
|
||||
})}
|
||||
<li part="list-item"><span>${msg("Powered by authentik")}</span></li>
|
||||
</ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,13 @@ export class FlowCard extends AKElement {
|
||||
// No title if the challenge doesn't provide a title and no custom title is set
|
||||
let title: null | SlottedTemplateResult = null;
|
||||
if (this.hasSlotted("title")) {
|
||||
title = html`<h1 class="pf-c-title pf-m-3xl"><slot name="title"></slot></h1>`;
|
||||
title = html`<h1 class="pf-c-title pf-m-3xl ak-m-clamped">
|
||||
<slot name="title"></slot>
|
||||
</h1>`;
|
||||
} else if (this.challenge?.flowInfo?.title) {
|
||||
title = html`<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo.title}</h1>`;
|
||||
title = html`<h1 class="pf-c-title pf-m-3xl ak-m-clamped">
|
||||
${this.challenge.flowInfo.title}
|
||||
</h1>`;
|
||||
}
|
||||
const footer = this.hasSlotted("footer") ? html`<slot name="footer"></slot>` : null;
|
||||
const footerBand = this.hasSlotted("footer-band")
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
.authenticator-button {
|
||||
/* compatibility-mode-fix */
|
||||
& {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(auto, 2rem) minmax(33%, max-content);
|
||||
gap: var(--pf-global--spacer--lg);
|
||||
}
|
||||
.authenticator-button,
|
||||
ak-stage-authenticator-validate.style-scope .authenticator-button {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(auto, 2rem) minmax(33%, max-content);
|
||||
gap: var(--pf-global--spacer--lg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--pf-global--BackgroundColor--200);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: var(--pf-global--icon--FontSize--lg);
|
||||
}
|
||||
i {
|
||||
font-size: var(--pf-global--icon--FontSize--lg);
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: left;
|
||||
.content {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
:host {
|
||||
:host,
|
||||
ak-stage-captcha.style-scope {
|
||||
--captcha-background-to: var(--pf-global--BackgroundColor--light-100);
|
||||
--captcha-background-from: var(--pf-global--BackgroundColor--light-300);
|
||||
}
|
||||
|
||||
:host([theme="dark"]) {
|
||||
:host([theme="dark"]),
|
||||
ak-stage-captcha[theme="dark"].style-scope {
|
||||
--captcha-background-to: var(--ak-dark-background-light);
|
||||
--captcha-background-from: var(--pf-global--BackgroundColor--300);
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ export class IdentificationStage extends BaseStage<
|
||||
? html`
|
||||
<p>
|
||||
${msg(
|
||||
"Enter the email associated with your account, and we'll send you a link to reset your password.",
|
||||
"Enter the email address or username associated with your account.",
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
fieldset[name="login-sources"] {
|
||||
fieldset[name="login-sources"],
|
||||
ak-stage-identification.style-scope fieldset[name="login-sources"] {
|
||||
--ak-c-login-sources-padding-inline: var(--pf-global--spacer--xl);
|
||||
|
||||
/* compatibility-mode-fix */
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
|
||||
& {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
|
||||
padding-inline: var(--ak-c-login-sources-padding-inline) !important;
|
||||
padding-block-start: var(--pf-global--spacer--md) !important;
|
||||
}
|
||||
padding-inline: var(--ak-c-login-sources-padding-inline) !important;
|
||||
padding-block-start: var(--pf-global--spacer--md) !important;
|
||||
|
||||
.source-button {
|
||||
display: flex;
|
||||
@@ -65,9 +62,12 @@ fieldset[name="login-sources"] {
|
||||
}
|
||||
}
|
||||
|
||||
:host([theme="dark"]) fieldset[name="login-sources"] .pf-c-button__icon {
|
||||
img,
|
||||
.pf-c-button__icon .fas {
|
||||
filter: invert(1);
|
||||
:host([theme="dark"]),
|
||||
ak-stage-identification[theme="dark"].style-scope {
|
||||
fieldset[name="login-sources"] .pf-c-button__icon {
|
||||
img,
|
||||
.pf-c-button__icon .fas {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
--pf-global--BackgroundColor--100: var(--pf-global--BackgroundColor--light-100);
|
||||
}
|
||||
|
||||
.pf-m-title {
|
||||
.pf-m-3xl.ak-m-clamped {
|
||||
--pf-c-title--m-3xl--FontSize: clamp(1rem, var(--pf-global--FontSize--3xl), 7dvw);
|
||||
}
|
||||
}
|
||||
|
||||
.pf-m-monospace {
|
||||
font-family: var(--pf-global--FontFamily--monospace);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
/* #region authentik extensions */
|
||||
|
||||
/* #region Root */
|
||||
|
||||
:root {
|
||||
--ak-accent: #fd4b2d;
|
||||
|
||||
@@ -32,8 +33,9 @@
|
||||
--ak-dark-background-light: #1c1e21;
|
||||
--ak-dark-background-lighter: #2b2e33;
|
||||
|
||||
--ak-global--background-contrast: var(--pf-global--Color--100);
|
||||
--ak-global--background-contrast-Filter: drop-shadow(
|
||||
--ak-global--BackgroundColorContrast--100: var(--pf-global--Color--light-100);
|
||||
|
||||
--ak-global--BackgroundContrastFilter: drop-shadow(
|
||||
0 0 2px
|
||||
var(--ak-locale-select--ShadowBlendColor, var(--pf-global--BackgroundColor--dark-200))
|
||||
);
|
||||
@@ -42,4 +44,8 @@
|
||||
--ak-sidebar--minimum-auto-width: 80rem;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--ak-global--BackgroundColorContrast--100: var(--pf-global--palette--black-150);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
/**
|
||||
* @file Patternfly Login component overrides and customizations.
|
||||
*
|
||||
* These styles are not as simple as they may seem at first glance.
|
||||
* The overlap between the concept of the login page, the flow executor,
|
||||
* and Django-provided templates means that these styles need to be flexible.
|
||||
*
|
||||
* - The initial render of the login page is server-side rendered.
|
||||
* - The use of ShadyDOM in the flow executor means that styles need to be compatible with both Shadow DOM and Light DOM contexts.
|
||||
* - The layout must adapt for mobile, tablet, and desktop.
|
||||
* - And dark and light themes must work while allowing for user provided style overrides.
|
||||
* - These styles have a unique relationship with the styles in `FlowExecutor.css` and `static.global.css`.
|
||||
*
|
||||
* All that said, we generally follow Patternfly's structure, save for the mobile layout which is unique to our implementation.
|
||||
*/
|
||||
|
||||
/* #region Login Component */
|
||||
|
||||
/* compatibility-mode-fix */
|
||||
.pf-c-login.pf-c-login {
|
||||
--ak-c-login--PaddingMax: 8dvw;
|
||||
--ak-c-login--padding: clamp(
|
||||
var(--pf-global--spacer--md),
|
||||
var(--pf-global--spacer--2xl),
|
||||
var(--ak-c-login--PaddingMax)
|
||||
);
|
||||
|
||||
--ak-c-login__main--brand-PaddingMin: var(--pf-global--spacer--xs);
|
||||
--ak-c-login__main--brand-PaddingIdeal: 5rem;
|
||||
--ak-c-login__main--brand-PaddingMax: 15dvh;
|
||||
|
||||
--ak-c-login__footer--PaddingBlock: var(--pf-global--spacer--md);
|
||||
|
||||
--ak-c-login--MaxWidth: 35rem;
|
||||
|
||||
--ak-c-login__main-ColumnWidth: minmax(
|
||||
min(100%, var(--ak-c-login--MaxWidth)),
|
||||
var(--ak-c-login--MaxWidth)
|
||||
);
|
||||
|
||||
--pf-c-login__main-body--PaddingBottom: 0;
|
||||
|
||||
--ak-c-login__main--footer-PaddingMin: var(--pf-global--spacer--xs);
|
||||
--ak-c-login__main--footer-PaddingIdeal: 3rem;
|
||||
--ak-c-login__main--footer-PaddingMax: 9dvh;
|
||||
|
||||
--pf-c-login__main-footer--PaddingBottom: clamp(
|
||||
var(--ak-c-login__main--footer-PaddingMin),
|
||||
var(--ak-c-login__main--footer-PaddingIdeal),
|
||||
var(--ak-c-login__main--footer-PaddingMax)
|
||||
);
|
||||
|
||||
--pf-c-login__main-footer-band--BackgroundColor: transparent;
|
||||
|
||||
/**
|
||||
* Take note, we avoid applying Patternfly styles to custom elements directly:
|
||||
*
|
||||
* ```html
|
||||
* <ak-button class="pf-c-button pf-m-primary">Click me</ak-button>
|
||||
* ```
|
||||
*
|
||||
* However, the flow executor requires that the `.pf-c-login` class be applied to the host element.
|
||||
* This allows for some careful enhancements to the login page without depending on the Shadow DOM,
|
||||
* with some caveats:
|
||||
*
|
||||
* - Custom variables should be defined in static.global.css and used here to allow the user to override them as needed.
|
||||
* - The data-layout attribute is applied to this element and the .pf-c-login__main element,
|
||||
* allowing for some layout-specific styles to be applied.
|
||||
* - The pf-c-login__footer is slotted into the flow executor, and requires a
|
||||
* delicate balance of inheriting styles from the login page while ensuring sufficient contrast against the background.
|
||||
*/
|
||||
.pf-c-login {
|
||||
flex: 1 1 auto;
|
||||
|
||||
padding: 0;
|
||||
@@ -63,7 +61,7 @@
|
||||
&::before {
|
||||
display: block;
|
||||
content: "";
|
||||
background-color: var(--ak-c-login--BackgroundColorOverlay, transparent);
|
||||
background-color: var(--ak-c-login--BackgroundColorOverlay, transparent) !important;
|
||||
z-index: -1;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
@@ -81,7 +79,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 35rem) or (max-height: 17.5rem) {
|
||||
--ak-c-login--BackgroundColorOverlay: var(--pf-c-login__main--BackgroundColor);
|
||||
--ak-c-login--BackgroundColorOverlay: var(--ak-c-login__main--BackgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,16 +87,11 @@
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pf-c-login,
|
||||
:host([theme="dark"]) .pf-c-login {
|
||||
--pf-c-login__main--BackgroundColor: var(--pf-global--BackgroundColor--100);
|
||||
}
|
||||
|
||||
/* #region Page Header */
|
||||
|
||||
.pf-c-login__header {
|
||||
grid-area: header;
|
||||
padding-inline: calc(var(--ak-c-login--padding) / 2);
|
||||
padding-inline: calc(var(--ak-c-login--spacer) / 2);
|
||||
align-self: start;
|
||||
|
||||
display: grid;
|
||||
@@ -107,7 +100,7 @@
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Page Footer */
|
||||
/* #region Main Footer */
|
||||
|
||||
/* compatibility-mode-fix */
|
||||
.pf-c-login__main-footer .pf-c-button__icon {
|
||||
@@ -125,10 +118,6 @@
|
||||
/* #region Card Main */
|
||||
|
||||
.pf-c-login__main {
|
||||
--pf-c-login__container--PaddingLeft: 0 !important;
|
||||
--pf-c-login__container--PaddingRight: 0 !important;
|
||||
--ak-c-login__main--BoxShadow: var(--pf-global--BoxShadow--md);
|
||||
|
||||
box-shadow: var(--ak-c-login__main--BoxShadow) !important;
|
||||
|
||||
grid-area: main;
|
||||
@@ -136,17 +125,36 @@
|
||||
|
||||
position: relative;
|
||||
max-width: var(--ak-c-login--MaxWidth);
|
||||
min-height: calc(var(--ak-c-login--MaxWidth) * 0.8);
|
||||
min-height: var(--ak-c-login__main--MinHeight, unset);
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.slotted-content {
|
||||
slot[name="placeholder"] {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that the use of `slot` as attribute selector is intentional.
|
||||
* We're checking for the presence of an element attempting to slot itself as the placeholder.
|
||||
*
|
||||
* This approach allows us to handle some of the lesser-intuitive combinations
|
||||
* of whether we're in a shadow DOM and how to gracefully transition to a
|
||||
* post-JavaScript state without any awkward repaints.
|
||||
|
||||
* We're also interested in whether the slot itself is within the main element,
|
||||
* as it indicates that the placeholder content is slotted and a shadow DOM is present.
|
||||
*
|
||||
* This ensures the height remains consistent from the initial render,
|
||||
* preventing layout shifts when the placeholder is replaced with the actual content.
|
||||
*/
|
||||
&:has([slot="placeholder"]),
|
||||
&:has(slot[name="placeholder"]) {
|
||||
--ak-c-login__main--MinHeight: calc(var(--ak-c-login--MaxWidth) * 0.8);
|
||||
}
|
||||
|
||||
@media (max-width: 35rem) or (max-height: 17.5rem) {
|
||||
--ak-c-login__main--BoxShadow: none;
|
||||
}
|
||||
@@ -156,15 +164,6 @@
|
||||
|
||||
/* #region Main Header */
|
||||
|
||||
.pf-c-login__main-header {
|
||||
padding-inline: var(--ak-c-login--padding);
|
||||
padding-block: clamp(var(--pf-global--spacer--xs), 6dvw, var(--pf-global--spacer--lg));
|
||||
|
||||
.pf-c-title {
|
||||
font-size: clamp(1rem, var(--pf-c-title--m-3xl--FontSize), 7dvw);
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-login__main-header.pf-c-brand {
|
||||
--ak-c-login__main-padding-block-start: clamp(
|
||||
var(--ak-c-login__main--brand-PaddingMin),
|
||||
@@ -172,13 +171,13 @@
|
||||
var(--ak-c-login__main--brand-PaddingMax)
|
||||
);
|
||||
|
||||
padding-inline: calc(var(--ak-c-login--padding) / 4);
|
||||
padding-inline: calc(var(--ak-c-login--spacer) / 4);
|
||||
padding-block-start: calc(
|
||||
var(--ak-c-login__main-padding-block-start) - var(--ak-c-login__footer--PaddingBlock)
|
||||
);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
|
||||
padding-block-end: calc(var(--ak-c-login--padding) / 2);
|
||||
padding-block-end: calc(var(--ak-c-login--spacer) / 2);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -203,7 +202,6 @@
|
||||
|
||||
.pf-c-login__main-body {
|
||||
flex: 1 1 auto;
|
||||
padding-inline: var(--ak-c-login--padding);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -242,7 +240,7 @@
|
||||
/* #region Layout variations */
|
||||
|
||||
.pf-c-login[data-layout$="frame_background"] {
|
||||
--ak-c-login--BackgroundColorOverlay: var(--pf-c-login__main--BackgroundColor);
|
||||
--ak-c-login--BackgroundColorOverlay: var(--ak-c-login__main--BackgroundColor);
|
||||
}
|
||||
|
||||
.pf-c-login[data-layout^="sidebar_left"] {
|
||||
@@ -292,19 +290,13 @@
|
||||
|
||||
.pf-c-login[data-layout^="sidebar"] {
|
||||
--ak-c-login--MaxWidth: 36rem;
|
||||
--ak-c-login--BackgroundColorOverlay: var(--pf-c-login__main--BackgroundColor);
|
||||
--ak-c-login--BackgroundColorOverlay: var(--ak-c-login__main--BackgroundColor);
|
||||
--ak-c-login__footer--Color: var(--ak-c-login__main--Color);
|
||||
|
||||
.pf-c-login__main {
|
||||
height: 100%;
|
||||
justify-content: normal;
|
||||
}
|
||||
|
||||
.pf-c-login__footer {
|
||||
color: inherit;
|
||||
flex: 1 1 auto;
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-login[data-layout^="sidebar_left"] {
|
||||
@@ -328,28 +320,55 @@
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Page Footer */
|
||||
|
||||
/**
|
||||
* The footer must respect a few constraints to ensure it remains legible::after
|
||||
*
|
||||
* - The mobile layout should have the same background color as the login main content.
|
||||
* - Aside from CSS variables footer styles should not be applied in the static.global.css file,
|
||||
* This may seem unnecessary, but PatternFly's own base styles for `pf-c-*` elements
|
||||
*. will override styles in an uphill battle against user overrides.
|
||||
*/
|
||||
.pf-c-login__footer {
|
||||
--pf-global--Color--100: var(--pf-global--Color--light-100) !important;
|
||||
grid-area: footer;
|
||||
flex: 0 0 auto;
|
||||
padding-block: var(--ak-c-login__footer--PaddingBlock);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: end;
|
||||
justify-content: center;
|
||||
padding-inline: var(--pf-global--spacer--xl) !important;
|
||||
padding-block: var(--ak-c-login__footer--PaddingBlock) !important;
|
||||
align-self: end;
|
||||
|
||||
flex: 0 0 auto;
|
||||
min-height: calc((var(--ak-c-login__footer--PaddingBlock) * 2) + 1rem);
|
||||
line-height: var(--pf-global--LineHeight--md);
|
||||
min-height: calc(
|
||||
(var(--ak-c-login__footer--PaddingBlock) * 2) + (var(--pf-global--LineHeight--md) * 1rem)
|
||||
);
|
||||
|
||||
/* Only applicable to the smallest of mobile viewports. */
|
||||
max-width: 100dvw;
|
||||
overflow: hidden;
|
||||
|
||||
color: var(--ak-c-login__footer--Color);
|
||||
|
||||
@media (max-width: 35rem) {
|
||||
color: var(--pf-global--Color--200);
|
||||
--ak-c-login__footer--Color: var(--ak-c-login__main--Color);
|
||||
}
|
||||
|
||||
@media (min-width: 35rem) and (min-height: 17.5rem) {
|
||||
filter: var(--ak-global--background-contrast-Filter);
|
||||
filter: var(--ak-global--BackgroundContrastFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The dark modifier is used in stacked layout to ensure sufficient contrast against the darker background.
|
||||
* This may appear unnecessary, but PF4's own login footer styles are not designed
|
||||
* with our mobile layout in mind. this ensures that the footer remains legible
|
||||
* even when the card is reduced in size and the background contrast is removed.
|
||||
*/
|
||||
.pf-c-login__footer {
|
||||
@media (max-width: 35rem) {
|
||||
--pf-global--Color--100: var(--pf-global--Color--dark-100) !important;
|
||||
--pf-global--Color--200: var(--pf-global--Color--dark-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,18 +15,126 @@
|
||||
@import "#elements/locale/ak-locale-select.css";
|
||||
@import "#flow/FlowExecutor.css";
|
||||
|
||||
.pf-c-login__main-body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
/**
|
||||
* @file Static global styles for authentik.
|
||||
*
|
||||
* Similar the base/globals.css file, this file is only injected in server templates
|
||||
* that may not have the full web component support.
|
||||
* If you're deciding on where to put a style, prefer a more specific file
|
||||
* to avoid unnecessarily increasing the global scope of the style.
|
||||
*/
|
||||
|
||||
.pf-c-form {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 auto;
|
||||
justify-content: end;
|
||||
/* #region Custom login variables */
|
||||
|
||||
:root {
|
||||
--ak-c-login--PaddingMax: 8dvw;
|
||||
|
||||
--ak-c-login--spacer: clamp(
|
||||
var(--pf-global--spacer--md),
|
||||
var(--pf-global--spacer--2xl),
|
||||
var(--ak-c-login--PaddingMax)
|
||||
);
|
||||
|
||||
--ak-c-login--MaxWidth: 35rem;
|
||||
--ak-c-login__main--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
|
||||
--ak-c-login__main--Color: var(--pf-global--Color--dark-100);
|
||||
|
||||
--ak-c-login__main--brand-PaddingMin: var(--pf-global--spacer--xs);
|
||||
--ak-c-login__main--brand-PaddingIdeal: 5rem;
|
||||
--ak-c-login__main--brand-PaddingMax: 15dvh;
|
||||
|
||||
--ak-c-login__main-ColumnWidth: minmax(
|
||||
min(100%, var(--ak-c-login--MaxWidth)),
|
||||
var(--ak-c-login--MaxWidth)
|
||||
);
|
||||
|
||||
--ak-c-login__main-header-PaddingBlock: clamp(
|
||||
var(--pf-global--spacer--xs),
|
||||
6dvw,
|
||||
var(--pf-global--spacer--lg)
|
||||
);
|
||||
|
||||
--ak-c-login__main-header-PaddingInline: var(--ak-c-login--spacer);
|
||||
|
||||
--ak-c-login__main--footer-PaddingMin: var(--pf-global--spacer--xs);
|
||||
--ak-c-login__main--footer-PaddingIdeal: 3rem;
|
||||
--ak-c-login__main--footer-PaddingMax: 9dvh;
|
||||
--ak-c-login__main--BoxShadow: var(--pf-global--BoxShadow--md);
|
||||
|
||||
--ak-c-login__footer--PaddingBlock: var(--pf-global--spacer--md);
|
||||
--ak-c-login__footer--Color: var(--ak-global--BackgroundColorContrast--100);
|
||||
|
||||
--ak-c-login__footer--ColumnGap: min(var(--pf-global--spacer--2xl), 2dvw);
|
||||
--ak-c-login__footer--RowGap: var(--pf-global--spacer--md);
|
||||
|
||||
--ak-c-login__footer--Display: grid;
|
||||
|
||||
--ak-c-login__footer--MaxWidth: var(--ak-c-login--MaxWidth);
|
||||
/* Gracefully degrade to the login max width if CSS size functions are not supported. */
|
||||
--ak-c-login__footer--MaxWidth: min(100dvw, var(--ak-c-login--MaxWidth));
|
||||
|
||||
--ak-c-login__footer--TrackMin: max-content;
|
||||
--ak-c-login__footer--TrackWidth: minmax(
|
||||
var(--ak-c-login__footer--TrackMin),
|
||||
var(--ak-c-login__footer--TrackMax)
|
||||
);
|
||||
|
||||
--ak-c-login__footer--ItemMaxWidth: calc(
|
||||
var(--ak-c-login__footer--MaxWidth) - var(--ak-c-login__footer--ColumnGap)
|
||||
);
|
||||
|
||||
--ak-c-login__footer--ColumnCount: 4;
|
||||
--ak-c-login__footer--TrackMax: calc(
|
||||
(var(--ak-c-login__footer--MaxWidth) / var(--ak-c-login__footer--ColumnCount)) -
|
||||
var(--ak-c-login__footer--ColumnGap)
|
||||
);
|
||||
|
||||
@media (width <= 35rem) {
|
||||
--ak-c-login__footer--TrackWidth: 1fr;
|
||||
--ak-c-login__footer__list-item--FlexBasis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pf-c-login {
|
||||
--ak-c-login__main--BackgroundColor: var(--pf-global--BackgroundColor--dark-100);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region PF4 Login */
|
||||
|
||||
.pf-c-login {
|
||||
--pf-c-login__main-header--PaddingTop: var(--ak-c-login__main-header-PaddingBlock);
|
||||
--pf-c-login__main-header--PaddingBottom: var(--ak-c-login__main-header-PaddingBlock);
|
||||
--pf-c-login__main-header--PaddingLeft: var(--ak-c-login__main-header-PaddingInline);
|
||||
--pf-c-login__main-header--PaddingRight: var(--ak-c-login__main-header-PaddingInline);
|
||||
|
||||
--pf-c-login__main--BackgroundColor: var(--ak-c-login__main--BackgroundColor);
|
||||
|
||||
--pf-c-login__main-body--PaddingLeft: var(--ak-c-login--spacer);
|
||||
--pf-c-login__main-body--PaddingRight: var(--ak-c-login--spacer);
|
||||
--pf-c-login__main-body--PaddingBottom: 0;
|
||||
|
||||
--pf-c-login__main-footer--PaddingBottom: clamp(
|
||||
var(--ak-c-login__main--footer-PaddingMin),
|
||||
var(--ak-c-login__main--footer-PaddingIdeal),
|
||||
var(--ak-c-login__main--footer-PaddingMax)
|
||||
);
|
||||
--pf-c-login__main-footer-band--BackgroundColor: transparent;
|
||||
|
||||
--pf-c-login__footer--c-list--xl--PaddingTop: 0;
|
||||
--pf-c-login__footer--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
--pf-c-login__footer--PaddingRight: var(--pf-global--spacer--lg);
|
||||
|
||||
--pf-c-login__container--PaddingLeft: 0 !important;
|
||||
--pf-c-login__container--PaddingRight: 0 !important;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Form */
|
||||
|
||||
/* Fallback form controls with minimal runtime expectations. */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
@@ -48,3 +156,61 @@
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Flow Links */
|
||||
|
||||
[name="flow-links"] {
|
||||
[part="list"],
|
||||
&::part(list) {
|
||||
--pf-c-list--m-inline--li--MarginRight: 0;
|
||||
|
||||
/* 3 entries is a unique scenario where 2 columns is visually balanced. */
|
||||
&[data-count="3"] {
|
||||
--ak-c-login__footer--ColumnCount: 2;
|
||||
}
|
||||
|
||||
justify-content: center;
|
||||
column-gap: var(--ak-c-login__footer--ColumnGap);
|
||||
row-gap: var(--ak-c-login__footer--RowGap);
|
||||
|
||||
max-width: var(--ak-c-login__footer--MaxWidth);
|
||||
place-items: center;
|
||||
display: var(--ak-c-login__footer--Display);
|
||||
|
||||
grid-template-columns: repeat(
|
||||
var(--ak-c-login__footer--ColumnCount),
|
||||
var(--ak-c-login__footer--TrackWidth)
|
||||
);
|
||||
|
||||
grid-template-rows:
|
||||
[header] max-content
|
||||
[main] max-content
|
||||
[footer];
|
||||
}
|
||||
|
||||
[part="list-item"],
|
||||
&::part(list-item) {
|
||||
/* CSS grid is preferred, but if the custom CSS overrides this, default to something reasonable. */
|
||||
flex: 1 1 var(--ak-c-login__footer__list-item--FlexBasis, auto);
|
||||
text-align: center;
|
||||
max-width: var(--ak-c-login__footer--ItemMaxWidth);
|
||||
|
||||
&[data-kind="text"] {
|
||||
&[data-track-name="start"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
&[data-track-name="end"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[part="list-item-link"],
|
||||
&::part(list-item-link) {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
65
web/types/webcomponents.d.ts
vendored
Normal file
65
web/types/webcomponents.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @file Web component globals applied to the Window object.
|
||||
*
|
||||
* @see https://www.npmjs.com/package/@webcomponents/webcomponentsjs
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
type Booleanish = "true" | "false";
|
||||
|
||||
type WebComponentFlags = Record<string, Booleanish | boolean | Record<string, boolean>>;
|
||||
|
||||
interface WebComponents {
|
||||
/**
|
||||
* Flags that can be set on the `WebComponents` global to control the behavior of web components in the application.
|
||||
* Typically, this is limited to the `webcomponents-loader`.
|
||||
*/
|
||||
flags?: WebComponentFlags;
|
||||
}
|
||||
|
||||
interface ShadyDOM {
|
||||
/**
|
||||
* Forces the use of the Shady DOM polyfill, even in browsers that support native Shadow DOM.
|
||||
* This can be useful for testing or to work around specific issues with native Shadow DOM in certain browsers.
|
||||
*/
|
||||
force?: boolean | Booleanish;
|
||||
/**
|
||||
* Prevents the patching of native Shadow DOM APIs when the Shady DOM polyfill is in use.
|
||||
* This can be useful for debugging or to avoid conflicts with other libraries that also patch these APIs.
|
||||
*/
|
||||
noPatch?: boolean | Booleanish;
|
||||
}
|
||||
|
||||
interface CustomElementRegistry {
|
||||
/**
|
||||
* An indication of whether the polyfill for web components is in use.
|
||||
*/
|
||||
readonly forcePolyfill?: Booleanish | boolean;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
/**
|
||||
* An object representing the state of web component support and configuration in the application.
|
||||
*/
|
||||
WebComponents?: Readonly<WebComponents>;
|
||||
/**
|
||||
* An object representing the configuration for the Shady DOM polyfill,
|
||||
* which provides support for Shadow DOM in browsers that do not natively support it.
|
||||
*/
|
||||
ShadyDOM?: Readonly<ShadyDOM>;
|
||||
/**
|
||||
* A root path for loading web component polyfills. This is only applicable
|
||||
*
|
||||
* @remarks
|
||||
* If you're using the loader on a page that enforces the `trusted-types`
|
||||
* Content Security Policy, you'll need to allow the `webcomponents-loader`
|
||||
* policy name so that the loader can dynamically create and insert a `<script>`
|
||||
* for the polyfill bundle it selects based on feature detection. I
|
||||
* f you set `WebComponents.root` (which is rare), it should be set to a {@linkcode TrustedScriptURL}
|
||||
* for Trusted Types compatibility.
|
||||
*/
|
||||
root?: string | TrustedScriptURL;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ Keys prefixed with `goauthentik.io` are used internally by authentik and are sub
|
||||
|
||||
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../../stages/identification/index.mdx).
|
||||
|
||||
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx) and others will use this value if it is set, and fallback to the request's users when possible.
|
||||
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx), and others will use this value if it is set, and fall back to the request's user when possible.
|
||||
|
||||
#### `prompt_data` (Dictionary)
|
||||
|
||||
@@ -55,8 +55,6 @@ Stores the final redirect URL that the user's browser will be sent to after the
|
||||
|
||||
If _Show matched user_ is disabled, this key will hold the user identifier entered by the user in the identification stage.
|
||||
|
||||
Stores the final redirect URL that the user's browser will be sent to after the flow is finished executing successfully. This is set when an un-authenticated user attempts to access a secured application, and when a user authenticates/enrolls with an external source.
|
||||
|
||||
#### `application` (Application object)
|
||||
|
||||
When an unauthenticated user attempts to access a secured resource, they are redirected to an authentication flow. The application they attempted to access will be stored in the key attached to this object. For example: `application.github`, with `application` being the key and `github` the value.
|
||||
@@ -151,7 +149,7 @@ Type the `pending_user` will be created as. Must be one of `internal`, `external
|
||||
|
||||
##### `user_backend` (string)
|
||||
|
||||
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating in the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
|
||||
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
|
||||
|
||||
##### `auth_method` (string)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
title: Default flows
|
||||
---
|
||||
|
||||
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that override the defaults configured on the brand.
|
||||
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that overrides the defaults configured on the brand.
|
||||
|
||||
If no default flow is selected when the provider is created, to determine which flow should be used authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug` and evaluate policies bound directly to the flows, and the first flow whose policies allow access will be picked.
|
||||
If no default flow is selected when the provider is created, authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands/index.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug`, evaluate policies bound directly to the flows, and pick the first flow whose policies allow access.
|
||||
|
||||
import DefaultFlowList from "../../flow/flow_list/\_defaultflowlist.mdx";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Default
|
||||
This is the default, web-based environment that flows are executed in. All stages are compatible with this environment and no limitations are imposed.
|
||||
|
||||
:::info
|
||||
All flow executors use the same [API](/api/docs/flow-executor), which allows for the implementation of custom flow executors.
|
||||
All flow executors use the same [API](/api/flow-executor/), which allows for the implementation of custom flow executors.
|
||||
:::
|
||||
|
||||
## Layouts
|
||||
|
||||
@@ -6,4 +6,4 @@ The user interface (/if/user/) uses a specialized flow executor to allow individ
|
||||
|
||||
Because the stages in a flow can change during its execution, be aware that configuring this executor to use any stage type other than Prompt or User Write will automatically trigger a redirect to the standard executor.
|
||||
|
||||
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands.md) on the same authentik instance.
|
||||
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands/index.md) on the same authentik instance.
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Flows
|
||||
|
||||
Flows are a major component in authentik. In conjunction with stages and [policies](../../../customize/policies/index.md), flows are at the heart of our system of building blocks, used to define and execute the workflows of authentication, authorization, enrollment, and user settings.
|
||||
|
||||
There are over a dozen default, out-of-the box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
|
||||
There are over a dozen default, out-of-the-box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
|
||||
|
||||
A flow is a method of describing a sequence of stages. A stage represents a single verification or logic step. By connecting a series of stages within a flow (and optionally attaching policies as needed) you can build a highly flexible process for authenticating users, enrolling them, and more.
|
||||
|
||||
@@ -54,7 +54,7 @@ To create a flow, follow these steps:
|
||||
|
||||
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
|
||||
|
||||
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
|
||||
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands/index.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
|
||||
|
||||
## Flow configuration options
|
||||
|
||||
@@ -78,9 +78,9 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
|
||||
|
||||
**Behavior settings**:
|
||||
|
||||
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled, when accessing the flow from a desktop browser. However accessing the flow from a mobile device might necessitate this setting to be enabled.
|
||||
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled when accessing the flow from a desktop browser. However, accessing the flow from a mobile device might necessitate this setting to be enabled.
|
||||
|
||||
The technical reasons for this settings' existence is due to the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
|
||||
The technical reason for this setting's existence is the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
|
||||
|
||||
When the compatibility mode is enabled, authentik uses a polyfill which emulates the Shadow DOM APIs without actually using the feature, and instead a traditional DOM is rendered. This increases support for password managers, especially on mobile devices.
|
||||
|
||||
@@ -95,7 +95,7 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
|
||||
|
||||
- **Layout**: select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.
|
||||
|
||||
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands.md#branding-settings).
|
||||
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands/index.md#branding-settings).
|
||||
|
||||
## Edit or delete a flow
|
||||
|
||||
|
||||
@@ -80,4 +80,4 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
4. Click **Finish**.
|
||||
|
||||
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the users' browser.
|
||||
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the user's browser.
|
||||
|
||||
@@ -66,7 +66,7 @@ return {
|
||||
|
||||
## Verify only
|
||||
|
||||
To only verify the validity of a users' phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
|
||||
To only verify the validity of a user's phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
|
||||
|
||||
## Limiting phone numbers
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Keep in mind that when using Code-based devices (TOTP, Static and SMS), values l
|
||||
|
||||
#### Less-frequent validation
|
||||
|
||||
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the users devices within the selected classes are checked.
|
||||
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the user's devices within the selected classes are checked.
|
||||
|
||||
#### Passwordless authentication
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ See the [Envoy mTLS documentation](https://www.envoyproxy.io/docs/envoy/latest/s
|
||||
|
||||
#### No reverse proxy
|
||||
|
||||
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands.md#client-certificates) for the domain, under **Other global settings**.
|
||||
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands/index.md#client-certificates) for the domain, under **Other global settings**.
|
||||
|
||||
## Stage configuration
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is a generic password prompt which authenticates the current `pending_user`
|
||||
|
||||
## Passwordless login
|
||||
|
||||
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the users device, which is documented here.
|
||||
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the user's device, which is documented here.
|
||||
|
||||
If you want users to be able to pick a passkey from the browser's passkey/autofill UI without entering a username first, configure **Passkey autofill (WebAuthn conditional UI)** in the [Identification stage](../identification/index.mdx#passkey-autofill-webauthn-conditional-ui). This is separate from configuring a dedicated passwordless flow, and can be used alongside normal identification flows.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The device code flow is also known as _device flow_ or _device authorization gra
|
||||
|
||||
### Requirements
|
||||
|
||||
This device flow is only possible if the active [brand](../../../sys-mgmt/brands.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
|
||||
This device flow is only possible if the active [brand](../../../sys-mgmt/brands/index.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
|
||||
|
||||
authentik does not include a default flow for this use case, so it is necessary to create a new one with a **Designation** of `Stage Configuration`.
|
||||
|
||||
@@ -25,6 +25,17 @@ client_id=application_client_id&
|
||||
scope=openid email my-other-scope
|
||||
```
|
||||
|
||||
Alternatively the client id may be sent via the HTTP Authorization header:
|
||||
|
||||
```http
|
||||
POST /application/o/device/ HTTP/1.1
|
||||
Host: authentik.company
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Authorization: Bearer YXBwbGljYXRpb25fY2xpZW50X2lkOg==
|
||||
|
||||
scope=openid email my-other-scope
|
||||
```
|
||||
|
||||
The response contains the following fields:
|
||||
|
||||
- `device_code`: Device code, which is the code kept on the device
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: OAuth2/OpenID Connect front-channel and back-channel logout
|
||||
title: Front-channel and back-channel logout
|
||||
description: Configure front-channel and back-channel logout for OAuth2/OpenID Connect providers
|
||||
authentik_version: "2025.8.0"
|
||||
authentik_preview: true
|
||||
|
||||
@@ -23,7 +23,7 @@ OAuth 2.0 is an authorization protocol that allows an application (the RP) to de
|
||||
1. An authorization request is prepared by the RP and contains parameters for its implementation of OAuth and which data it requires, and then the User's browser is redirected to that URL.
|
||||
2. The RP sends a request to authentik in the background to exchange the access code for an access token (and optionally a refresh token).
|
||||
|
||||
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorised (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
|
||||
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the user's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorized (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
|
||||
|
||||
The image below shows a typical authorization code flow.
|
||||
|
||||
@@ -183,6 +183,28 @@ This does _not_ apply to special scopes, as those are not configurable in the pr
|
||||
- `user:email`: Allows read-only access to `/user`, including email address
|
||||
- `read:org`: Allows read-only access to `/user/teams`, listing all the user's groups as teams.
|
||||
|
||||
### Email scope verification
|
||||
|
||||
In authentik releases prior to 2025.10, the email scope always set the `email_verified` claim to `True`. Since authentik does not have a single authoritative source to determine whether a user's email is actually verified, asserting this claim could have security implications. As of 2025.10, `email_verified` now defaults to `False`.
|
||||
|
||||
Some applications require this claim to be `True` in order to authenticate users. In those cases, you can create a custom email scope mapping (**Customization** > **Property Mappings**) that always returns `email_verified` as `True`:
|
||||
|
||||
```python
|
||||
return {
|
||||
"email": request.user.email,
|
||||
"email_verified": True
|
||||
}
|
||||
```
|
||||
|
||||
For greater security guarantees, verify users' email addresses and store the verification status as a user attribute (for example, `email_verified` set to `True` or `False`). You can then configure the scope mapping to return this value dynamically:
|
||||
|
||||
```python
|
||||
return {
|
||||
"email": request.user.email,
|
||||
"email_verified": request.user.attributes.get("email_verified", False)
|
||||
}
|
||||
```
|
||||
|
||||
## Signing & Encryption
|
||||
|
||||
[JWTs](https://jwt.io/introduction) created by authentik will always be signed.
|
||||
|
||||
@@ -8,6 +8,6 @@ The [WebFinger protocol](https://webfinger.net/) allows for the discovery of inf
|
||||
|
||||
## authentik WebFinger support
|
||||
|
||||
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands.md#external-user-settings).
|
||||
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands/index.md#external-user-settings).
|
||||
|
||||
The WebFinger endpoint is available at: `https://authentik.company/.well-known/webfinger` (where authentik.company is the FQDN of your authentik instance)
|
||||
|
||||
@@ -15,15 +15,7 @@ Scope mappings are used by the OAuth2 provider to map information from authentik
|
||||
:::info Default value for `email_verified`
|
||||
By default, authentik sets the `email_verified` claim to `False`, since it has no way to confirm whether a user's email is verified. Setting this claim to `True` by default could introduce unintended security risks.
|
||||
|
||||
Be aware that some applications might require this claim to be true to successfully authenticate users. In this case you should create a custom email scope mapping that returns `email_verified` as `True`, using the following expression:
|
||||
|
||||
```
|
||||
return {
|
||||
"email": user.email,
|
||||
"email_verified": True,
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that some applications might require this claim to be true to successfully authenticate users. See [Email scope verification](../oauth2/index.mdx#email-scope-verification) for more information.
|
||||
:::
|
||||
|
||||
## Skip objects during synchronization
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user