mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
53 Commits
dependabot
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
1
.github/workflows/release-tag.yml
vendored
1
.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
|
||||
|
||||
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
|
||||
|
||||
#########################
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.2.0-rc1"
|
||||
VERSION = "2026.2.0-rc5"
|
||||
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>
|
||||
|
||||
@@ -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,7 +110,15 @@ 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 used to be 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
|
||||
# New licenses are generated with `algorithm="ES384"` and signed with `secp384r1`.
|
||||
# The last license will run out by March 2027.
|
||||
# TODO: remove this in March 2027.
|
||||
ECAlgorithm._validate_curve = lambda *_: True
|
||||
body = from_dict(
|
||||
LicenseKey,
|
||||
decode(
|
||||
@@ -125,6 +134,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:
|
||||
|
||||
@@ -4,6 +4,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.providers.saml.views.metadata import MetadataDownload
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
@@ -11,6 +12,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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -185,6 +185,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)
|
||||
|
||||
@@ -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__()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.0-rc5 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
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026.2.0-rc1
|
||||
2026.2.0-rc5
|
||||
@@ -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()
|
||||
|
||||
@@ -18,7 +18,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2026.2.0-rc1
|
||||
Default: 2026.2.0-rc5
|
||||
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.0-rc5}
|
||||
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.0-rc5}
|
||||
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.0-rc5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.0-rc5",
|
||||
"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.0-rc5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2026.2.0-rc1"
|
||||
version = "2026.2.0-rc5"
|
||||
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.0-rc5
|
||||
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.0rc5"
|
||||
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.0-rc5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2026.2.0-rc1",
|
||||
"version": "2026.2.0-rc5",
|
||||
"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.0-rc5",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Create a Remote Access Control (RAC) provider
|
||||
---
|
||||
|
||||
For an overview of Remote Access Control (RAC), see the [RAC provider](./index.md) documentation.
|
||||
|
||||
You can also watch our video on YouTube for setting up RAC:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
|
||||
|
||||
## Workflow to create an RAC provider
|
||||
|
||||
Follow this workflow to create and configure an RAC provider:
|
||||
|
||||
1. Create a RAC provider and application pair.
|
||||
2. Create RAC property mappings (that define the access credentials to each remote machine).
|
||||
3. Create endpoints for each remote machine you want to connect to.
|
||||
4. Create an RAC outpost to service the provider.
|
||||
|
||||
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
|
||||
|
||||
### Create a RAC provider and application pair
|
||||
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with Provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to create only the provider by navigating to **Applications** > **Providers** and clicking **Create**.
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application details, and then click **Next**.
|
||||
4. Select the **RAC** provider type, and then click **Next**.
|
||||
5. On the **Configure Remote Access Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
|
||||
|
||||
### Create RAC property mappings
|
||||
|
||||
Next, you need to add property mappings for each remote machine you want to access. RAC property mappings can be used to pass the access credentials and connection settings of the remote machine.
|
||||
|
||||
Refer to the [RAC Credentials Prompt](./rac_credentials_prompt.md) and [RAC SSH Public Key Authentication](./rac-public-key.md) documentation for alternative methods of handling RAC authentication.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings**, and click **Create**.
|
||||
3. Select **RAC Provider Property Mapping** as the property mapping type, and then click **Next**.
|
||||
4. On the **Create RAC Provider Property Mapping** page, provide the following configuration settings:
|
||||
- **Name**: provide a name for the property mapping
|
||||
- Under **General settings**:
|
||||
- **Username**: the username for the remote machine
|
||||
- **Password**: the password for the remote machine
|
||||
- Under **Advanced settings**:
|
||||
- **Expression _(optional)_**: define other connection settings to be used, such as an SSH key. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation.
|
||||
|
||||
5. Click **Finish**.
|
||||
|
||||
### Create endpoints for the provider
|
||||
|
||||
Then, you need to create an endpoint corresponding to each remote machine you want to connect to. Endpoints define the IP address, port, protocol, and other settings used for connecting to a remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the **Edit** button on the RAC provider that you previously created.
|
||||
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
|
||||
- **Provider Name** (endpoint name): define a name for the endpoint
|
||||
- **Protocol**: select the appropriate protocol
|
||||
- **Host**: enter the host name or IP address of the remote machine. Optionally include the port.
|
||||
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation
|
||||
- **Property mappings**: select either the property mapping that you previously created, or use one of the default RAC property mappings
|
||||
- **Advanced settings _(optional)_**: define other connection settings to be used. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation
|
||||
|
||||
5. Click **Create**.
|
||||
|
||||
### Create an RAC outpost
|
||||
|
||||
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Outposts**.
|
||||
3. Click **Create** and set the following values:
|
||||
- **Name**: define a name for the outpost.
|
||||
- **Type**: `RAC`
|
||||
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
|
||||
- **Applications**: select the RAC application that you previously created.
|
||||
- **Advanced settings _(optional)_**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
|
||||
|
||||
4. Click **Create** to save your new outpost.
|
||||
|
||||
## Access the remote machine
|
||||
|
||||
To verify your configuration and access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page, click the **Remote Access** application to start a secure session on the remote machine in your web browser.
|
||||
|
||||
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
title: Create a Remote Access Control (RAC) provider
|
||||
---
|
||||
|
||||
The Remote Access Control (RAC) provider is a highly flexible feature for accessing remote machines.
|
||||
|
||||
For overview information, see the [RAC provider](./index.md) documentation. You can also view our video on YouTube for setting up RAC.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
|
||||
|
||||
## Overview workflow to create an RAC provider
|
||||
|
||||
The typical workflow to create and configure a RAC provider is:
|
||||
|
||||
1. Create an application and provider.
|
||||
2. Create property mappings (that define the access credentials to each remote machine).
|
||||
3. Create an endpoint for each remote machine you want to connect to.
|
||||
4. Create an RAC outpost to service the provider.
|
||||
|
||||
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
|
||||
|
||||
### Create an application and RAC provider
|
||||
|
||||
The first step is to create the RAC application and provider pair.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with provider**.
|
||||
3. Follow these [instructions](../../applications/manage_apps.mdx#create-an-application-and-provider-pair) to create your RAC application and provider.
|
||||
|
||||
### Create RAC property mappings
|
||||
|
||||
Next, you need to add property mappings for each remote machine you want to access. Property mappings allow you to pass information to external applications, and with RAC they are used to pass the host name, IP address, and access credentials of the remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
- **Select Type**: `RAC Provider Property Mapping`
|
||||
- **Create RAC Property Mapping**:
|
||||
- **Name**s: define a name for the property mapping, perhaps include the type of connection (RDP, SSH, VNC)
|
||||
- **General settings**:
|
||||
- **Username**: the username for the remote machine
|
||||
- **Password**: the password for the remote machine
|
||||
- **RDP settings**:
|
||||
- **Ignore server certificate**: select **Enabled** (Depending on the setup of your RDP Server, it might be required to enable this setting.)
|
||||
- **Enable wallpaper**: optional
|
||||
- **Enable font smoothing**: optional
|
||||
- **Enable full window dragging**: optional
|
||||
- Advanced settings:
|
||||
- **Expressions**: optional, using Python you can define custom [expressions](../property-mappings/expression.mdx).
|
||||
|
||||
3. Click **Finish**.
|
||||
|
||||
### Create endpoints for the provider
|
||||
|
||||
Then, you need to create an endpoint for each remote machine. Endpoints are defined within providers; connections between the remote machine and authentik are enabled through communication between the provider's endpoint and the remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the **Edit** button on the RAC provider that you previously created.
|
||||
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
|
||||
- **Name**: define a name for the endpoint, perhaps include the type of connection (RDP, SSH, VNC).
|
||||
- **Protocol**: select the appropriate protocol.
|
||||
- **Host**: enter the host name or IP address of the remote machine.
|
||||
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation.
|
||||
- **Property mapping**: select either the property mapping that you previously created, or use one of the default settings.
|
||||
- **Advance settings**: (_optional_)
|
||||
|
||||
5. Click **Create**.
|
||||
|
||||
### Create an RAC outpost
|
||||
|
||||
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Outposts**.
|
||||
3. Click **Create** and set the following values:
|
||||
- **Name**: define a name for the outpost.
|
||||
- **Type**: `RAC`
|
||||
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
|
||||
- **Applications**: select the RAC application that you previously created.
|
||||
- **Advanced settings (optional)**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
|
||||
|
||||
4. Click Create to save your new outpost.
|
||||
|
||||
## Access the remote machine
|
||||
|
||||
To verify your configuration and then access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page click the **Remote Access** application and authentik then connects you to a secure session on the remote machine, in your web browser.
|
||||
|
||||
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.
|
||||
@@ -2,67 +2,85 @@
|
||||
title: Remote Access Control (RAC) Provider
|
||||
---
|
||||
|
||||
:::info
|
||||
This provider requires the deployment of the [RAC Outpost](../../outposts/index.mdx).
|
||||
:::
|
||||
|
||||
## About the Remote Access Control (RAC) Provider
|
||||
|
||||
The RAC provider allows users to access remote Windows, macOS, and Linux machines via [RDP](https://en.wikipedia.org/wiki/Remote_Desktop_Protocol)/[SSH](https://en.wikipedia.org/wiki/Secure_Shell)/[VNC](https://en.wikipedia.org/wiki/Virtual_Network_Computing). Just like other providers in authentik, the RAC provider is associated with an application that appears on a user's **My applications** page.
|
||||
|
||||
:::info
|
||||
Note that with RAC, you create a single application and associated provider that serves to connect with _all remote machines_ that you want to configure for access via RAC.
|
||||
:::
|
||||
For instructions on creating a RAC provider, refer to the [Create a Remote Access Control (RAC) provider](./create-rac-provider.md) documentation. Alternatively, watch our ["Remote Access Control (RAC) in authentik" video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts).
|
||||
|
||||
For instructions on creating a RAC provider, refer to the [Managing RAC providers](./how-to-rac.md) documentation. You can also view our [video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts) for setting up a RAC.
|
||||
## RAC components
|
||||
|
||||
For an example of how to configure RAC connections settings, refer to the [RAC SSH Public Key Authentication](./rac-public-key.md) documentation.
|
||||
A RAC provider uses several components:
|
||||
|
||||
There are several components used with a RAC provider; let's take a closer look at the high-level configuration layout of these components and how they are managed using endpoints and connections.
|
||||
```mermaid
|
||||
architecture-beta
|
||||
service application(mdi:application-outline)[Application]
|
||||
service provider(mdi:application-cog-outline)[Provider]
|
||||
service endpoint(mdi:network-pos)[Endpoint Settings]
|
||||
service server(mdi:server)[authentik Server]
|
||||
service outpost(mdi:server-plus)[RAC Outpost]
|
||||
|
||||

|
||||
service machine(mdi:desktop-classic)[Remote Machine]
|
||||
|
||||
The provider-application pair, the authentik server, and the authentik API are typical to all configurations. With RAC, there are some new components, namely the endpoints, the outpost, and of course the target remote machines.
|
||||
application:R --> L:provider
|
||||
provider:B -- T:endpoint
|
||||
provider:R --> L:server
|
||||
server:R <--> L:outpost
|
||||
outpost:B <--> T:machine
|
||||
```
|
||||
|
||||
When a user starts the RAC application, the app communicates with the authentik server, which then connects to an instance of the outpost (the exact instance is selected dynamically based on connection load). After the outpost is selected, then the authentik server sends the outpost the instructions (based on the data you defined in the endpoint) required to connect to the remote machine.
|
||||
When a user starts the RAC application, it communicates with the authentik server, which then connects to the RAC outpost and sends instructions (based on the endpoint data you defined) on how to connect to the remote machine.
|
||||
|
||||
After the connection to the remote machine is made, the outpost sends a message back to the authentik server (via websockets), and the web browser opens the websocket connection to the remote machine.
|
||||
After connecting to the remote machine, the outpost sends a message back to the authentik server (via WebSockets), and the web browser opens the WebSocket connection to the remote machine.
|
||||
|
||||
### Endpoints
|
||||
## Endpoints
|
||||
|
||||
Unlike other providers, where one provider-application pair must be created for each resource you wish to access, the RAC provider handles this slightly differently. For each remote machine (computer/server) that should be accessible, you create an _Endpoint_ object within a single RAC provider. (And as mentioned above, a single provider-application pair is used for all remote connections.)
|
||||
Unlike other providers, where an application-provider pair is created for each resource you wish to access, RAC works differently. RAC uses a single application connected to one RAC provider. The RAC provider then has an _Endpoint_ object for each remote machine (computer/server) you want to connect to.
|
||||
|
||||
The _Endpoint_ object specifies the hostname/IP of the machine to connect to, as well as the protocol to use. Additionally it is possible to bind policies to _endpoint_ objects to restrict access. Users must have access to both the application that the RAC Provider is using as well as the individual endpoint.
|
||||
The _Endpoint_ object specifies:
|
||||
|
||||
Configuration details such as credentials can be specified through _settings_, which can be specified on different levels and are all merged together when connecting:
|
||||
- Hostname, IP address, and port of the remote machine
|
||||
- Protocol to use: SSH, RDP, or VNC
|
||||
- RDP connection settings
|
||||
- [RAC Property mappings](#rac-property-mappings) to apply
|
||||
- [Connection settings](#connection-settings) to apply
|
||||
|
||||
1. Default settings
|
||||
2. Provider settings
|
||||
3. Endpoint settings
|
||||
4. Provider property mapping settings
|
||||
5. Endpoint property mapping settings
|
||||
6. Connection settings
|
||||
Additionally, it is possible to bind policies to _Endpoint_ objects to restrict user access. To connect to a remote machine, users must have access to both the application that the RAC provider is using and the corresponding endpoint.
|
||||
|
||||
### Connection settings
|
||||
## Connection management
|
||||
|
||||
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
|
||||
A new connection is created every time an RAC application/endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, you can configure connection expiry in the RAC provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
|
||||
|
||||
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
|
||||
## RAC Property Mappings
|
||||
|
||||
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
You can create RAC property mappings via **Customization** > **Property Mappings**.
|
||||
|
||||
RAC property mappings allow you to configure the following settings:
|
||||
|
||||
- **Username**: the username for the remote machine
|
||||
- **Password**: the password for the remote machine
|
||||
- **Ignore Server certificate**: set whether the validity of the returned RDP server certificate will be ignored
|
||||
- **Enable wallpaper**: enable/disable the desktop wallpaper of the RDP server
|
||||
- **Enable font-smoothing**: enable/disable font-smoothing (anti-aliasing) on the RDP server
|
||||
- **Enable full window dragging**: enable/disable whether the full content of a window is visible while moving it on the RDP server
|
||||
- **Advanced settings**: set [connection settings](#connection-settings) via a Python expression
|
||||
|
||||
## Connection settings
|
||||
|
||||
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
|
||||
For a full list of possible connection configurations, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
|
||||
Connection settings can include `username`, `password`, `domain`, `private-key`, `security`, `enable-audio`, and more.
|
||||
|
||||
RAC connection settings can be set via several methods:
|
||||
For a full list of possible connection settings, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
|
||||
|
||||
1. The settings of the RAC provider
|
||||
2. RAC endpoint settings
|
||||
3. RAC property mappings
|
||||
4. Retrieved from user or group attributes via RAC property mappings
|
||||
RAC connection settings can be set via several methods and are all merged together when connecting:
|
||||
|
||||
For an example of how to set a connection setting see the [RAC SSH public key authentication](./rac-public-key.md) page.
|
||||
1. Default settings
|
||||
2. RAC Provider settings
|
||||
3. RAC Endpoint settings
|
||||
4. RAC Provider property mapping settings
|
||||
5. RAC Endpoint property mapping settings
|
||||
6. The `connection_settings` object in the flow plan
|
||||
|
||||
For examples of how to configure connection settings, see the [RAC SSH public key authentication](./rac-public-key.md) and [RAC Credentials Prompt](./rac_credentials_prompt.md) documentation.
|
||||
|
||||
## Capabilities
|
||||
|
||||
|
||||
@@ -66,8 +66,10 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
return {
|
||||
"private-key": "-----BEGIN SSH PRIVATE KEY-----
|
||||
import textwrap
|
||||
|
||||
private_key = textwrap.dedent("""
|
||||
-----BEGIN SSH PRIVATE KEY-----
|
||||
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
||||
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
||||
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
|
||||
@@ -75,7 +77,12 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
|
||||
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
|
||||
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
|
||||
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
|
||||
-----END SSH PRIVATE KEY-----",
|
||||
-----END SSH PRIVATE KEY-----
|
||||
""")
|
||||
|
||||
return {
|
||||
"username": "<your_username>",
|
||||
"private-key": private_key
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -9,7 +9,7 @@ authentik SAML providers can be created either from scratch or by using SAML met
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to solely create the provider by navigating to **Applications** > **Providers** and clicking **Create**.
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application details, and then click **Next**.
|
||||
4. Select **SAML Provider** as the **Provider Type**, and then click **Next**.
|
||||
5. On the **Configure SAML Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
|
||||
@@ -19,7 +19,7 @@ To create a provider along with the corresponding application that uses it for a
|
||||
If you have exported SAML metadata from your SP, you can optionally create the authentik SAML provider by importing this metadata.
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers** and click **Create** to create a provider.
|
||||
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
|
||||
3. Select **SAML Provider from Metadata** as the **Provider Type**, and then click **Next**.
|
||||
4. On the **Create SAML Provider from Metadata** page, provide the configuration settings along with an SP metadata file and then click **Finish** to create the provider.
|
||||
5. (Optional) Edit the created SAML provider and configure any further settings.
|
||||
@@ -33,7 +33,7 @@ After an authentik SAML provider has been created via any of the above methods,
|
||||
To download the metadata of an authentik SAML provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the name of the provider you want metadata from to open its overview tab.
|
||||
4. In the **Related objects** section, under **Metadata** click on **Download**. This will download the metadata XML file for that provider.
|
||||
|
||||
@@ -42,7 +42,7 @@ To download the metadata of an authentik SAML provider, follow these steps:
|
||||
To view and optionally download the metadata of an authentik SAML provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the name of the provider you want metadata from to open its overview tab.
|
||||
4. Navigate to the **Metadata** tab.
|
||||
5. The metadata for the provider will be shown in a codebox. You can optionally use the **Download** button to obtain the metadata as a file.
|
||||
|
||||
@@ -25,7 +25,7 @@ When you create a new SCIM provider, select which **Authentication Mode** the ap
|
||||
|
||||

|
||||
|
||||
Whichever mode you select you'll need to enter a SCIM base **URL**, for the endpoint.
|
||||
Whichever mode you select, you'll need to enter a SCIM base **URL** for the endpoint.
|
||||
|
||||
### Default authentication method for a SCIM provider
|
||||
|
||||
@@ -54,7 +54,7 @@ Data is synchronized in multiple ways:
|
||||
- When a user/group is created/modified/deleted, that action is sent to all SCIM providers
|
||||
- Periodically (once an hour), all SCIM providers are fully synchronized
|
||||
|
||||
The actual synchronization process is run in the authentik worker. To allow this process to better to scale, a task is started for each 100 users and groups, so when multiple workers are available the workload will be distributed.
|
||||
The actual synchronization process is run in the authentik worker. To allow this process to scale better, a task is started for each 100 users and groups, so when multiple workers are available the workload will be distributed.
|
||||
|
||||
### Attribute mapping
|
||||
|
||||
@@ -70,7 +70,7 @@ By default, service accounts are excluded from being synchronized. This can be c
|
||||
|
||||
Users can be filtered using application policies.
|
||||
|
||||
Only users who can view the scim provider's application are synced by the scim provider.
|
||||
Only users who can view the SCIM provider's application are synced by the SCIM provider.
|
||||
|
||||
#### Group Filters
|
||||
|
||||
@@ -94,7 +94,7 @@ To configure a compatibility mode, select the appropriate option in the **SCIM C
|
||||
|
||||
### Filtering users
|
||||
|
||||
By default service accounts are excluded from being synchronized. This can be configured in the SCIM provider. Additionally, an optional group can be configured to only synchronize the users that are members of the selected group. Changing this group selection does _not_ remove members outside of the group that might have been created previously.
|
||||
By default, service accounts are excluded from being synchronized. This can be configured in the SCIM provider. Additionally, an optional group can be configured to only synchronize the users that are members of the selected group. Changing this group selection does _not_ remove members outside of the group that might have been created previously.
|
||||
|
||||
### Supported options
|
||||
|
||||
@@ -118,7 +118,7 @@ The `ServiceProviderConfig` is cached for 1 hour after it is fetched. The cache
|
||||
|
||||
### Using in conjunction with other providers
|
||||
|
||||
A lot of applications support SCIM in conjunction with another SSO protocol like OAuth/OIDC or SAML. With default settings, the unique user IDs in SCIM and other protocols are identical, which should easily allow applications to link users the are provisioned with users that are logging in.
|
||||
A lot of applications support SCIM in conjunction with another SSO protocol like OAuth/OIDC or SAML. With default settings, the unique user IDs in SCIM and other protocols are identical, which should easily allow applications to link users that are provisioned with users that are logging in.
|
||||
|
||||
Applications can either match users on a unique ID sent by authentik called `externalId`, by their email or username.
|
||||
|
||||
|
||||
@@ -46,17 +46,17 @@ Front-channel logout sends logout requests through the user's browser. authentik
|
||||
|
||||
#### iframe mode (default for OIDC)
|
||||
|
||||
- Loads all provider logout URLs simultaneously in hidden iframes
|
||||
- Provides fast, parallel logout across multiple providers
|
||||
- Required by the OIDC front-channel logout specification
|
||||
- Most SAML providers also support iframe-based logout
|
||||
- Loads all provider logout URLs simultaneously in hidden iframes
|
||||
- Provides fast, parallel logout across multiple providers
|
||||
- Required by the OIDC front-channel logout specification
|
||||
- Most SAML providers also support iframe-based logout
|
||||
|
||||
#### Native Mode (SAML Only)
|
||||
|
||||
- Uses the active browser tab to chain redirects and POST requests sequentially
|
||||
- Provides better compatibility with SAML providers that have iframe restrictions
|
||||
- Each provider redirects the user back to authentik before proceeding to the next provider
|
||||
- Not available for OIDC providers as the specification requires iframe support
|
||||
- Uses the active browser tab to chain redirects and POST requests sequentially
|
||||
- Provides better compatibility with SAML providers that have iframe restrictions
|
||||
- Each provider redirects the user back to authentik before proceeding to the next provider
|
||||
- Not available for OIDC providers as the specification requires iframe support
|
||||
|
||||
:::info
|
||||
Use native front-channel mode for SAML providers if you encounter iframe compatibility issues, such as Content Security Policy (CSP) restrictions or cookie handling problems.
|
||||
@@ -66,10 +66,10 @@ Use native front-channel mode for SAML providers if you encounter iframe compati
|
||||
|
||||
Back-channel logout sends logout requests directly from the authentik server to each provider's logout endpoint via HTTP POST.
|
||||
|
||||
- Does not require user browser interaction
|
||||
- Works even when the user is offline or their browser is closed
|
||||
- Is automatically triggered by administrators terminating a user session (user deactivation or session deletion)
|
||||
- Requires the provider to accept server-to-server POST requests
|
||||
- Does not require user browser interaction
|
||||
- Works even when the user is offline or their browser is closed
|
||||
- Is automatically triggered by administrators terminating a user session (user deactivation or session deletion)
|
||||
- Requires the provider to accept server-to-server POST requests
|
||||
|
||||
**For SAML**: Requires POST SLS binding.
|
||||
**For OIDC**: Requires a `logout_uri` configured for back-channel that accepts logout tokens.
|
||||
@@ -125,8 +125,8 @@ See the [OIDC Front-channel and Back-channel logout documentation](../oauth2/fro
|
||||
|
||||
authentik tracks provider sessions to enable single logout:
|
||||
|
||||
- **SAML**: Creates `SAMLSession` records containing the `SessionIndex`, `NameID`, and `NameID format` for each successful authentication.
|
||||
- **OIDC**: Tracks session identifiers (`sid`) and ID tokens required for logout requests.
|
||||
- **SAML**: Creates `SAMLSession` records containing the `SessionIndex`, `NameID`, and `NameID format` for each successful authentication.
|
||||
- **OIDC**: Tracks session identifiers (`sid`) and ID tokens required for logout requests.
|
||||
|
||||
These session records are automatically created during authentication and deleted after logout or expiration.
|
||||
|
||||
@@ -134,8 +134,8 @@ These session records are automatically created during authentication and delete
|
||||
|
||||
Back-channel logout is always triggered when a user session is terminated via administrative actions:
|
||||
|
||||
- **Session Deletion**: When an administrator manually deletes a user's session through the Admin interface or API, authentik sends back-channel logout requests to all configured providers.
|
||||
- **User Deactivation**: When a user account is deactivated, authentik automatically sends back-channel logout requests to terminate all active sessions across all providers.
|
||||
- **Session Deletion**: When an administrator manually deletes a user's session through the Admin interface or API, authentik sends back-channel logout requests to all configured providers.
|
||||
- **User Deactivation**: When a user account is deactivated, authentik automatically sends back-channel logout requests to terminate all active sessions across all providers.
|
||||
|
||||
These requests are processed asynchronously to avoid blocking administrative operations.
|
||||
|
||||
|
||||
@@ -1,56 +1,42 @@
|
||||
---
|
||||
title: Configure an SSF provider
|
||||
authentik_version: "2025.2.0"
|
||||
description: "How to create and configure an SSF provider in authentik"
|
||||
authentik_enterprise: true
|
||||
authentik_preview: true
|
||||
tags:
|
||||
- backchannel
|
||||
- provider
|
||||
tags: [Shared Signals Framework, SSF, Apple Business Manager, backchannel]
|
||||
---
|
||||
|
||||
The workflow to implement an SSF provider as a [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers) for an application/provider pair is as follows:
|
||||
Follow this workflow to create and configure an SSF provider for an application:
|
||||
|
||||
1. Create the SSF provider (which serves as the backchannel provider).
|
||||
1. Create the SSF provider (which serves as the [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers)).
|
||||
2. Create an OIDC provider (which serves as the protocol provider for the application).
|
||||
3. Create the application, and assign both the OIDC provider and the SSF provider.
|
||||
|
||||
## Create the SSF provider
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. In the modal, select the **Provider Type** of **SSF**, and then click **Next**.
|
||||
|
||||
4. On the **New provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
|
||||
|
||||
5. Click **Finish** to create and save the provider.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
|
||||
3. Select **Shared Signals Framework Provider** as the **Provider Type**, and then click **Next**.
|
||||
4. On the **Create SSF Provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
|
||||
5. Click **Finish** to create the provider.
|
||||
|
||||
## Create the OIDC provider
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. In the modal, select the **Provider Type** of **OIDC**, and then click **Next**.
|
||||
|
||||
4. Define the settings for the provider, and then click **Finish** to save the new provider.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
|
||||
3. Select **OAuth2/OpenID Provider** as the **Provider Type**, and then click **Next**.
|
||||
4. On the **Create OAuth2/OpenID Provider** page, provide the configuration settings and then click **Finish** to create the provider.
|
||||
|
||||
## Create the application
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Applications**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. Define the settings for the application:
|
||||
- **Name**: define a descriptive name of the application.
|
||||
- **Slug**: optionally define the internal application name used in URLs.
|
||||
- **Group**: optionally select a group that you want to have access to this application.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create** to create an application.
|
||||
3. Configure the following required settings for the application:
|
||||
- **Name**: provide a descriptive name of the application.
|
||||
- **Slug**: provide the application slug used in URLs.
|
||||
- **Provider**: select the OIDC provider that you created.
|
||||
- **Backchannel Providers**: select the SSF provider you created.
|
||||
- **Policy engine mode**: define policy-based access.
|
||||
- **UI Settings**: optionally define a launch URL, an icon, and other UI elements.
|
||||
|
||||
- **Backchannel Providers**: select the SSF provider that you created.
|
||||
4. Click **Create** to save the new application.
|
||||
|
||||
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your list of Applications.
|
||||
The new application, with its OIDC provider and the backchannel SSF provider, should now appear in your application list.
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
---
|
||||
title: Shared Signals Framework (SSF) Provider
|
||||
sidebar_label: SSF Provider
|
||||
description: "Overview of SSF and the authentik SSF provider"
|
||||
authentik_version: "2025.2.0"
|
||||
authentik_enterprise: true
|
||||
authentik_preview: true
|
||||
tags: [Shared Signals Framework, SSF, Apple Business Manager]
|
||||
---
|
||||
|
||||
Shared Signals Framework (SSF) is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected, secure webhooks.
|
||||
The Shared Signals Framework (SSF) provider allows you to integrate applications with the Shared Signals Framework protocol.
|
||||
|
||||
## About Shared Signals Framework
|
||||
SSF is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected and secure webhooks.
|
||||
|
||||
In authentik, an SSF provider allows applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
|
||||
The authentik SSF provider allows OIDC applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
|
||||
|
||||
Events in authentik that are tracked via SSF include when an MFA device is added or removed, logouts, sessions being revoked by Admin or user clicking logout, or credentials changed.
|
||||
|
||||
Refer to our documentation to learn how to [create a SSF provider](./create-ssf-provider.md).
|
||||
|
||||
## Example use cases
|
||||
|
||||
One important use case for SFF is to [integrate Apple Business Manager](https://integrations.goauthentik.io/device-management/apple/) or any of the Apple device management platforms with authentik, so that users can enroll their Apple devices using their authentik credentials. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
|
||||
One important use case for SSF is to [integrate Apple Business Manager](https://integrations.goauthentik.io/device-management/apple/) or any of the Apple device management platforms with authentik, so that users can enroll their Apple devices using their authentik credentials. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
|
||||
|
||||
Another use case for SSF is when an Admin wants to know if a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
|
||||
Another use case for SSF is when an administrator wants to know when a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
|
||||
|
||||
Another example use case is when an application uses SSF to subscribe to authorization events because the application needs to know if a user changed their password in authentik. If a user did change their password, then the application receives a POST request to write the fact that the password was changed.
|
||||
|
||||
## About using SSF in authentik
|
||||
## Using the authentik SSF provider
|
||||
|
||||
Let's look at a few details about using SSF in authentik.
|
||||
The SSF provider serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application.
|
||||
|
||||
The SSF provider in authentik serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application. Thus you will still need to [create a typical application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair) (using an OIDC provider), and when creating the application, assign the SSF provider as a backchannel provider.
|
||||
Therefore you still need to [create a typical OIDC application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair), and when creating the application, assign the SSF provider as a [backchannel provider](../../applications/manage_apps#backchannel-providers).
|
||||
|
||||
When an authentik Admin [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
|
||||
When an authentik administrator [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
|
||||
|
||||
### The application (the receiver)
|
||||
|
||||
Within the application, the admin creates an SSF stream (which comprises all the signals that the app wants to subscribe to) and defines the audience, called `aud` in the specification (the URL that identifies the stream). A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can change or delete the stream.
|
||||
Within the application, the administrator creates an SSF stream which lists all the signals that the application wants to subscribe to, and defines the audience (`aud`), which is the URL that identifies the stream. A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can also change or delete the stream.
|
||||
|
||||
Note that authentik doesn't specify which events to subscribe to; instead the application defines which they want to listen for.
|
||||
authentik does not specify which events to subscribe to; instead the application defines which events they want to listen for.
|
||||
|
||||
### authentik (the transmitter)
|
||||
|
||||
To configure authentik as a shared signals transmitter, the authentik Admin [creates a new provider](./create-ssf-provider), selecting the type "SSF", to serve as the backchannelprovider for the application.
|
||||
To configure authentik as a shared signals transmitter, the authentik administrator [creates a new SSF provider](./create-ssf-provider), to serve as the backchannel provider for the application.
|
||||
|
||||
When creating the SSF provider you will need to select a signing key. This is the key that the Security Event Tokens (SET) is signed with.
|
||||
When creating the SSF provider you will need to select a signing key that is used to sign the Security Event Tokens (SET).
|
||||
|
||||
Optionally, you can specify a event retention time period: this value determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
|
||||
Optionally, you can specify an event retention time period, which determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
|
||||
|
||||
:::info
|
||||
:::note SET events
|
||||
Be aware that the SET events are different events than those displayed in the authentik Admin interface under **Events**.
|
||||
:::
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Create a WS-Federation provider
|
||||
---
|
||||
|
||||
An authentik WS-Federation provider is typically created as part of an application/provider pair, using the steps below. You can also create a standalone provider, and then later assign an application to use it.
|
||||
|
||||
## Create a WS-Federation provider and application pair
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application details, and then click **Next**.
|
||||
4. Select **WS-Federation Provider** as the **Provider Type**, and then click **Next**.
|
||||
5. On the **Configure WS-Federation Provider** page, provide a name for the provider, select an authorization flow, and the two required configuration settings:
|
||||
- **Reply URL**: Enter the application callback URL, where the token should be sent. This is the specific endpoint on an RP (application) where an Identity Provider (STS) sends the security token and authentication response after a successful log in.
|
||||
- **Realm**: Enter the identifier (string) of the requesting realm; that is, the Relying Party (RP) or application receiving the token. Realm is similar to the SAML 2.0 Entity ID.
|
||||
6. Click **Submit** to create both the application and the provider.
|
||||
|
||||
## Export authentik WS-Federation provider metadata
|
||||
|
||||
After an authentik WS-Federation provider has been created via any of the above methods, you can access its metadata in one of two ways:
|
||||
|
||||
### Download authentik metadata for a WS-Federation provider
|
||||
|
||||
To download the metadata of an authentik WS-Federation provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
3. Click the name of the provider you want metadata for.
|
||||
4. On the **Overview** tab, in the **Related objects** section, click on **Download** under **Metadata**. This will download the metadata XML file for that provider.
|
||||
|
||||
### Access the Metadata tab for a WS-Federation provider
|
||||
|
||||
To view and optionally download the metadata of an authentik WS-Federation provider, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
3. Click the name of the provider you want metadata for, and then click the **Metadata** tab.
|
||||
4. The metadata for the provider will be shown in a code box. You can optionally use the **Download** button to obtain the metadata as a file.
|
||||
53
website/docs/add-secure-apps/providers/wsfed/index.md
Normal file
53
website/docs/add-secure-apps/providers/wsfed/index.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: WS-Federation Provider
|
||||
---
|
||||
|
||||
The WS-Federation provider is used to integrate with applications and service providers that use [WS-Federation protocol](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adfsod/204de335-ea34-4f9b-ae73-8b7d4c8152d1). WS-Federation is an XML-based identity federation protocol that uses token exchange for federated Single Sign-On (SSO) and IdP authentication, specifically for Windows applications such as SharePoint.
|
||||
|
||||
There are similarities between WS-Federation and SAML protocols, but there are several key differences in terminology, most importantly:
|
||||
|
||||
- WS-Federation term: **STS (Security Token Service)**
|
||||
- SAML term: **IdP (Identity Provider)**
|
||||
|
||||
:::info SAML2 token support
|
||||
Note that we only support the SAML2 token type within WS-Federation providers, and that using the WS-Federation provider with Entra ID is not supported because Entra ID requires a SAML 1.0 token.
|
||||
:::
|
||||
|
||||
## Supported URL request parameters
|
||||
|
||||
The following URL request parameters are supported in the authentik WS-Federation provider:
|
||||
|
||||
- **wa**: This is a required parameter that represents the action being requested, typically wsignin1.0 for signing in. The parameter's value tells the Security Token Service (STS) which operation to execute.
|
||||
- **wtrealm**: The unique identifier (realm) of the Relying Party (RP) or application requesting the security token, for example, urn:my-app:rp. It defines the trust relationship between the RP and the Identity Provider (IdP) and indicates which application is initiating the WS-Federation request. This is a required query parameter that tells the Security Token Service (STS) which relying party the token is intended for.
|
||||
- **wreply**: The target URL to which the Identity Provider (IdP) sends the WS-Federation response containing the security token. This URL is supplied by the Service Provider (SP). authentik verifies that the received `wreply` parameter matches the URL configured by the administrator and stored in the database.
|
||||
- **wctx**: A context value that is used to maintain state between the Relying Party (RP) and the Identity Provider (IdP) across redirects. It serves the same purpose as the `RelayState` parameter in SAML. The RP includes this value in the authentication request, and the IdP returns it unchanged in the response, allowing the RP to validate and restore the original session or request context.
|
||||
|
||||
## WS-Federation bindings and endpoints
|
||||
|
||||
_Bindings_ define how an Identity Provider (IdP) and the WS-Federation STS (Security Token Service), or IdP in SAML terms, communicate; how messages are transported over network protocols, specifying transport (like HTTP), encoding, and security detail that allow WS-Federation to facilitate secure identity sharing across systems. Both the IdP and STS define various endpoints in their metadata, each associated with a specific WS-Federation binding.
|
||||
|
||||
| Endpoint | URL |
|
||||
| -------- | --------------------- |
|
||||
| SSO/SLO | `/application/wsfed/` |
|
||||
|
||||
## WS-Federation metadata
|
||||
|
||||
Using metadata ensures that WS-Federation single sign-on works reliably by exchanging and maintaining identity and connection information. WS-Federation metadata is an XML document that defines how IdPs and SPs securely interact for authentication. It includes information such as endpoints, bindings, certificates, and unique identifiers. The metadata is what you provide the application to configure it for authenticating with authentik.
|
||||
|
||||
You can [export WS-Federation metadata](./create-wsfed-provider.md#export-authentik-ws-federation-provider-metadata) from an authentik WS-Federation provider to an STS to automatically provide important endpoint and certificate information to the SP.
|
||||
|
||||
## Certificates
|
||||
|
||||
The certificates used with WS-Federation to sign Request Security Token Response (RSTR), which contains the assertion, are the same certificates that are used by SAML.
|
||||
|
||||
For details, refer to our [SAML certificates documentation](../saml/index.md#certificates).
|
||||
|
||||
## WS-Federation property mappings
|
||||
|
||||
Property mappings are used during the authentication process to align, or "map", user attributes values between the SP and STS (Security Token Service), the latter being the equivalent of SAML's IdP.
|
||||
|
||||
The same property mappings that are used in WS-Federation are used in SAML. For details, refer to our [SAML property mapping documentation](../saml/index.md#certificates).
|
||||
|
||||
## Attributes for WS-Federation
|
||||
|
||||
WS-Federation and SAML also share the use of the [NameID](../saml/index.md#nameid) and the [AuthnContextClassRef](../saml/index.md#authncontextclassref) attributes.
|
||||
@@ -35,8 +35,8 @@ This container executes background tasks, such as sending emails, the event noti
|
||||
|
||||
#### Persistence
|
||||
|
||||
- `/certs` is used for authentik to import external certs, which in most cases shouldn't be used for SAML, but rather if you use authentik without a reverse proxy, this can be used for example for the [Let's Encrypt integration](../sys-mgmt/certificates.md#lets-encrypt-integration)
|
||||
- `/templates` is used for [custom email templates](../add-secure-apps/flows-stages/stages/email/index.mdx#custom-templates), and as with the other ones fully optional
|
||||
- `/certs` is used for authentik to import external certs, which in most cases shouldn't be used for SAML, but if you use authentik without a reverse proxy, this can be used, for example, for the [Let's Encrypt integration](../sys-mgmt/certificates.md#lets-encrypt-integration).
|
||||
- `/templates` is used for [custom email templates](../add-secure-apps/flows-stages/stages/email/index.mdx#custom-templates), and as with the others is fully optional.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ sidebar_custom_props:
|
||||
termName: Application
|
||||
tags:
|
||||
- Core Concepts
|
||||
shortDescription: An application is what you authenticate into with authentik, and they are displayed on the "My applications" page on the User interface.
|
||||
shortDescription: An application is what you authenticate into with authentik and is displayed on the "My applications" page in the User interface.
|
||||
authentikSpecific: true
|
||||
longDescription: An application is paired with a provider, and with defined policies and other configurations controls user access. It also holds information like UI name, icon, and more.
|
||||
---
|
||||
|
||||
@@ -6,5 +6,5 @@ sidebar_custom_props:
|
||||
- Core Concepts
|
||||
shortDescription: A yes/no gate evaluated by type and settings.
|
||||
authentikSpecific: true
|
||||
longDescription: At a base level a policy is a yes/no gate. It evaluates to True or False depending on the policy kind and settings. For example, a Group Membership policy evaluates to True if the user is a member of the specified group and False if not. Policies can conditionally apply stages, grant or deny access, and support other custom logic.
|
||||
longDescription: At a base level, a policy is a yes/no gate. It evaluates to True or False depending on the policy kind and settings. For example, a Group Membership policy evaluates to True if the user is a member of the specified group and False if not. Policies can conditionally apply stages, grant or deny access, and support other custom logic.
|
||||
---
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user