Compare commits

..

54 Commits

Author SHA1 Message Date
Marcelo Elizeche Landó
8ac746aad6 Merge branch 'main' into pr/15531 2025-08-05 13:56:48 -03:00
Dewi Roberts
9848e4fbe0 website/docs: change azure ad to entra id (#15691)
* Update sidebar, update doc and files

* Update website/docs/users-sources/sources/social-logins/entra-id/index.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/users-sources/sources/social-logins/entra-id/index.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/users-sources/sources/social-logins/entra-id/index.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update sidebar, update doc and files

* Update website/docs/users-sources/sources/social-logins/entra-id/index.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/users-sources/sources/social-logins/entra-id/index.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/users-sources/sources/social-logins/entra-id/index.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Applied suggestions

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-08-05 16:24:30 +01:00
Tana M Berry
a9deefe481 website/docs: add tips for image optimization (#15978)
* add new section for image optimization

* tweaks

---------

Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-05 10:19:25 -05:00
authentik-automation[bot]
d29896cfe1 web: bump API Client version (#15976)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-05 10:59:51 -04:00
Marcelo Elizeche Landó
30670bb547 providers/oauth2: backchannel logout (#15401)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-08-05 14:16:02 +02:00
authentik-automation[bot]
0f64471115 web: bump API Client version (#15953)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-05 14:09:50 +02:00
transifex-integration[bot]
249b22963a translate: Updates for file web/xliff/en.xlf in fr (#15974)
Translate web/xliff/en.xlf in fr

100% translated source file: 'web/xliff/en.xlf'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-08-05 12:01:13 +00:00
transifex-integration[bot]
b3a513273b translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#15973)
Translate locale/en/LC_MESSAGES/django.po in fr

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-08-05 11:55:23 +00:00
authentik-automation[bot]
7ca013d527 core, web: update translations (#15962)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-05 12:39:11 +02:00
dependabot[bot]
2e65e307fe core: bump goauthentik.io/api/v3 from 3.2025064.3 to 3.2025064.5 (#15965)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025064.3 to 3.2025064.5.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2025064.3...v3.2025064.5)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-version: 3.2025064.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 12:37:33 +02:00
Tana M Berry
0c07bad6f6 website/docs: reword Warning in Docker install docs (#15960)
* try again

* tweak

---------

Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-04 17:45:37 -05:00
Teffen Ellis
eb1c56dbeb web: Fix property name mismatch. (#15961) 2025-08-04 22:32:40 +00:00
Mike
766a294e55 website/docs: update instructions for Cloudflare Turnstile setup (#15918)
* Update index.md

Expanded on the instructions to setup Cloudflare Turnstile captcha

Signed-off-by: Mike <mike@cxi.nz>

* Update website/docs/add-secure-apps/flows-stages/stages/captcha/index.md

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Added period

---------

Signed-off-by: Mike <mike@cxi.nz>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-04 22:19:42 +00:00
Dominic R
db84a29ad7 website/integrations: home assistant: fix typo (#15958)
Update index.md

Signed-off-by: Dominic R <dominic@sdko.org>
2025-08-04 15:49:20 -05:00
Teffen Ellis
58e65e4612 web: Fix scroll-event induced tab crash (#15939)
web: Fix issue where native scroll event doesn't trigger before element
expands.
2025-08-04 20:25:05 +00:00
Dominic R
95b2d15476 website/integrations: actual budget: add info about first login fails (#15931)
* Import from PR

* wip

* wip
2025-08-04 15:24:10 -05:00
Dominic R
2bdc5ef8b1 website/integrations: mattermost (#15922)
* Import from PR

* wip

* wip
2025-08-04 15:19:06 -05:00
Dominic R
83cae926f7 website/integrations: fix build (#15957)
* Update index.md

Signed-off-by: Dominic R <dominic@sdko.org>

* Delete website/integrations/services/home-assistant/index.mdx

Signed-off-by: Dominic R <dominic@sdko.org>

* bump build

* frustrating

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-04 15:02:40 -05:00
Simonyi Gergő
213cf44928 root: enhance custom middleware experience (#15919)
* enable custom middleware positioning

Users can now set up their middleware to come before or after other
middleware.

Comes with the added benefit that prometheus middlewares are ensured
to be the very first and very last to run.

* stop treating authentik.enterprise exceptionally in settings

This is the singular case where more apps are added.

* stop treating authentik.core exceptionally in settings

Uhh, fingers crossed? This has history, it goes back to 80d90b91e8
2025-08-04 21:05:05 +02:00
Timo Christeleit
3c97b081b0 website/integrations: add hass-openid instructions (#14672)
* add instructions

* Added tabs for each configuration method, changed some wording, and ran prettier.

* Changed proxy section formatting and some language

* Update website/integrations/services/home-assistant/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Timo Christeleit <timo.christeleit@cavefire.net>

* Update website/integrations/services/home-assistant/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Timo Christeleit <timo.christeleit@cavefire.net>

* Update website/integrations/services/home-assistant/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Timo Christeleit <timo.christeleit@cavefire.net>

* Update website/integrations/services/home-assistant/index.mdx

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Timo Christeleit <timo.christeleit@cavefire.net>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-08-04 09:39:37 -05:00
Jose D. Gomez R.
ba725365ec core: add updated_at field to user (#15571)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-08-04 13:36:09 +00:00
Dominic R
e5e9708ec2 root: Add more opencontainer labels to Dockerfiles (#15923)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-04 13:08:09 +00:00
dependabot[bot]
6a604e42ca core: bump goauthentik.io/api/v3 from 3.2025064.2 to 3.2025064.3 (#15949)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 12:55:18 +00:00
Daniel Adu-Gyan
ab1f87cfd6 core, providers/ldap: add parent/child groups to api and ldap results (#14974) 2025-08-04 14:29:16 +02:00
Teffen Ellis
de9b795c97 web: Make Webdriver optional during install. (#15952) 2025-08-04 12:24:34 +00:00
authentik-automation[bot]
0377e3593e core, web: update translations (#15945)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-04 13:22:17 +02:00
Simonyi Gergő
951c24dab5 packages/django-dramatiq-postgres: fix typo (#15932)
* fix typo

* fix typo
2025-08-04 13:05:57 +02:00
Dechen
4edba39935 Update blueprint 2025-07-26 12:37:55 +04:00
Dechen
92f94c2d0f Refactor tests since we're now using timedelta instead of ints for the cache timeout 2025-07-26 12:16:17 +04:00
Dechen
77d5366387 Update EmailStageForm.ts to use text input with timedelta instead of int. 2025-07-26 12:16:17 +04:00
Dechen
62c2c5576a Refactor email stage rate limiting logic to use timedelta instead of int. 2025-07-26 12:16:17 +04:00
Dechen
d47965ef21 Regenerate schema 2025-07-26 12:16:17 +04:00
Dechen
96bdd608ef Update recovery_cache_timeout to use a TextField with a timedelta string validator. Re-generate migrations. 2025-07-26 12:16:16 +04:00
Dechen
f25817e292 Make rate limiting error message more generic 2025-07-26 12:16:16 +04:00
Dechen
63cd5f949c Use PositiveIntegerField for recovery fields. 2025-07-26 12:16:16 +04:00
Dechen
2b7f8f5619 Hard code the max attempts and cache timeout values since these can be set per stage. 2025-07-26 12:16:16 +04:00
Dechen
d7378dace6 Remove locale changes since these are handled by CI 2025-07-26 12:16:16 +04:00
Dechen
ba6f0a4d6d Lint website 2025-07-26 12:16:16 +04:00
Dechen
b6f91bda12 authentik/stages/email/tests: add tests for rate limiting 2025-07-26 12:16:16 +04:00
Dechen
9f0903eefb locale/en/LC_MESSAGES: revert changes unrelated to this PR 2025-07-26 12:16:16 +04:00
Dechen
258eba1676 locale/en/LC_MESSAGES: revert changes unrelated to this PR 2025-07-26 12:16:16 +04:00
Dechen
3cbc99a253 web/src/admin/stages/email: replace test default with sane defaults 2025-07-26 12:16:16 +04:00
Dechen
4c69403e96 schema: only update schema for new fields 2025-07-26 12:16:16 +04:00
Dechen
2dbc2253e1 authentik/stages/email: refactor to use rate limiting values from EmailStage instance 2025-07-26 12:16:16 +04:00
Dechen
187e4198f4 web/src/admin/stages/email: add input form elements for max attempts and cache timeout 2025-07-26 12:16:16 +04:00
Dechen
ec4aed1442 website/docs/install-config/configuration: update documentation with reference to Email Stage specific values 2025-07-26 12:16:16 +04:00
Dechen
78ef9e9aa0 blueprints/schema: regenerate schema 2025-07-26 12:16:16 +04:00
Dechen
48fc338e33 authentik/stages/email/api: add recovery_max_attempts and recovery_cache_timeout to EmailStageSerializer 2025-07-26 12:16:16 +04:00
Dechen
c3556e09d1 stages/email/models: add recovery related fields to EmailStage and generate migrations 2025-07-26 12:16:16 +04:00
Dechen
a71f2ddd7e website/docs/developer-docs/translation: add a section for backend related translations 2025-07-26 12:16:16 +04:00
Dechen
a1425985b4 locale/en: generate translations for account recovery error message 2025-07-26 12:16:16 +04:00
Dechen
5a49717383 website/docs/install-config/configuration: update docs with new section for Account Recovery Settings 2025-07-26 12:16:16 +04:00
Dechen
cdacd362d1 lib/default: add default configuration for account recovery 2025-07-26 12:16:16 +04:00
Dechen
20f697b43f stages/email/stage: add rate limiting logic for account recovery 2025-07-26 12:16:16 +04:00
89 changed files with 3510 additions and 1975 deletions

View File

@@ -134,11 +134,16 @@ ARG VERSION
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
LABEL org.opencontainers.image.url=https://goauthentik.io
LABEL org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info."
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
LABEL org.opencontainers.image.version=${VERSION}
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info." \
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
org.opencontainers.image.title="authentik server image" \
org.opencontainers.image.url="https://goauthentik.io" \
org.opencontainers.image.vendor="Authentik Security Inc." \
org.opencontainers.image.version=${VERSION}
WORKDIR /

View File

@@ -49,11 +49,28 @@ class GroupMemberSerializer(ModelSerializer):
]
class GroupChildSerializer(ModelSerializer):
"""Stripped down group serializer to show relevant children for groups"""
attributes = JSONDictField(required=False)
class Meta:
model = Group
fields = [
"pk",
"name",
"is_superuser",
"attributes",
"group_uuid",
]
class GroupSerializer(ModelSerializer):
"""Group Serializer"""
attributes = JSONDictField(required=False)
users_obj = SerializerMethodField(allow_null=True)
children_obj = SerializerMethodField(allow_null=True)
roles_obj = ListSerializer(
child=RoleSerializer(),
read_only=True,
@@ -61,7 +78,6 @@ class GroupSerializer(ModelSerializer):
required=False,
)
parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
num_pk = IntegerField(read_only=True)
@property
@@ -71,12 +87,25 @@ class GroupSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_users", "true")).lower() == "true"
@property
def _should_include_children(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_children", "false")).lower() == "true"
@extend_schema_field(GroupMemberSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
if not self._should_include_users:
return None
return GroupMemberSerializer(instance.users, many=True).data
@extend_schema_field(GroupChildSerializer(many=True))
def get_children_obj(self, instance: Group) -> list[GroupChildSerializer] | None:
if not self._should_include_children:
return None
return GroupChildSerializer(instance.children, many=True).data
def validate_parent(self, parent: Group | None):
"""Validate group parent (if set), ensuring the parent isn't itself"""
if not self.instance or not parent:
@@ -126,11 +155,17 @@ class GroupSerializer(ModelSerializer):
"attributes",
"roles",
"roles_obj",
"children",
"children_obj",
]
extra_kwargs = {
"users": {
"default": list,
},
"children": {
"required": False,
"default": list,
},
# TODO: This field isn't unique on the database which is hard to backport
# hence we just validate the uniqueness here
"name": {"validators": [UniqueValidator(Group.objects.all())]},
@@ -203,11 +238,15 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
Prefetch("users", queryset=User.objects.all().only("id"))
)
if self.serializer_class(context={"request": self.request})._should_include_children:
base_qs = base_qs.prefetch_related("children")
return base_qs
@extend_schema(
parameters=[
OpenApiParameter("include_users", bool, default=True),
OpenApiParameter("include_children", bool, default=False),
]
)
def list(self, request, *args, **kwargs):
@@ -216,6 +255,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
parameters=[
OpenApiParameter("include_users", bool, default=True),
OpenApiParameter("include_children", bool, default=False),
]
)
def retrieve(self, request, *args, **kwargs):

View File

@@ -16,6 +16,7 @@ from django.utils.translation import gettext as _
from django_filters.filters import (
BooleanFilter,
CharFilter,
IsoDateTimeFilter,
ModelMultipleChoiceFilter,
MultipleChoiceFilter,
UUIDFilter,
@@ -241,6 +242,7 @@ class UserSerializer(ModelSerializer):
"type",
"uuid",
"password_change_date",
"last_updated",
]
extra_kwargs = {
"name": {"allow_blank": True},
@@ -331,6 +333,14 @@ class UsersFilter(FilterSet):
method="filter_attributes",
)
date_joined__lt = IsoDateTimeFilter(field_name="date_joined", lookup_expr="lt")
date_joined = IsoDateTimeFilter(field_name="date_joined")
date_joined__gt = IsoDateTimeFilter(field_name="date_joined", lookup_expr="gt")
last_updated__lt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="lt")
last_updated = IsoDateTimeFilter(field_name="last_updated")
last_updated__gt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="gt")
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
uuid = UUIDFilter(field_name="uuid")
@@ -376,6 +386,8 @@ class UsersFilter(FilterSet):
fields = [
"username",
"email",
"date_joined",
"last_updated",
"name",
"is_active",
"is_superuser",
@@ -390,10 +402,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"""User Viewset"""
queryset = User.objects.none()
ordering = ["username"]
ordering = ["username", "date_joined", "last_updated"]
serializer_class = UserSerializer
filterset_class = UsersFilter
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
search_fields = [
"username",
"name",
"is_active",
"email",
"uuid",
"attributes",
"date_joined",
"last_updated",
]
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.11 on 2025-07-15 15:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_core", "0049_alter_token_options"),
]
operations = [
migrations.AddField(
model_name="user",
name="last_updated",
field=models.DateTimeField(auto_now=True),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["last_updated"], name="authentik_c_last_up_ed7486_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["date_joined"], name="authentik_c_date_jo_58c256_idx"),
),
]

View File

@@ -274,6 +274,8 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
ak_groups = models.ManyToManyField("Group", related_name="users")
password_change_date = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
objects = UserManager()
class Meta:
@@ -293,6 +295,8 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
models.Index(fields=["uuid"]),
models.Index(fields=["path"]),
models.Index(fields=["type"]),
models.Index(fields=["date_joined"]),
models.Index(fields=["last_updated"]),
]
def __str__(self):

View File

@@ -390,3 +390,72 @@ class TestUsersAPI(APITestCase):
self.assertFalse(
AuthenticatedSession.objects.filter(session__session_key=session_id).exists()
)
def test_sort_by_last_updated(self):
"""Test API sorting by last_updated"""
User.objects.all().delete()
admin = create_test_admin_user()
self.client.force_login(admin)
user = create_test_user()
admin.first_name = "Sample change"
admin.last_name = "To trigger an update"
admin.save()
# Ascending
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"ordering": "last_updated",
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["pk"], user.pk)
# Descending
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"ordering": "-last_updated",
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["pk"], admin.pk)
def test_sort_by_date_joined(self):
"""Test API sorting by date_joined"""
User.objects.all().delete()
admin = create_test_admin_user()
self.client.force_login(admin)
user = create_test_user()
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"ordering": "date_joined",
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["pk"], admin.pk)
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"ordering": "-date_joined",
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["pk"], user.pk)

View File

@@ -55,6 +55,7 @@ class TestEnterpriseAudit(APITestCase):
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
diff.pop("last_updated")
self.assertEqual(
diff,
{
@@ -116,6 +117,7 @@ class TestEnterpriseAudit(APITestCase):
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
diff.pop("last_updated")
self.assertEqual(
diff,
{

View File

@@ -301,6 +301,7 @@ class SessionEndStage(ChallengeStageView):
"flow_slug": self.request.brand.flow_invalidation.slug,
},
)
return SessionEndChallenge(data=data)
# This can never be reached since this challenge is created on demand and only the

View File

@@ -70,6 +70,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"signing_key",
"encryption_key",
"redirect_uris",
"backchannel_logout_uri",
"sub_mode",
"property_mappings",
"issuer_mode",

View File

@@ -1,5 +1,8 @@
"""OAuth/OpenID Constants"""
from django.db import models
from django.utils.translation import gettext_lazy as _
GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
GRANT_TYPE_IMPLICIT = "implicit"
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec
@@ -51,3 +54,23 @@ AMR_MFA = "mfa"
AMR_OTP = "otp"
AMR_WEBAUTHN = "user"
AMR_SMART_CARD = "sc"
class SubModes(models.TextChoices):
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
USER_ID = "user_id", _("Based on user ID")
USER_UUID = "user_uuid", _("Based on user UUID")
USER_USERNAME = "user_username", _("Based on the username")
USER_EMAIL = (
"user_email",
_("Based on the User's Email. This is recommended over the UPN method."),
)
USER_UPN = (
"user_upn",
_(
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
"Use this method only if you have different UPN and Mail domains."
),
)

View File

@@ -4,10 +4,8 @@ from dataclasses import asdict, dataclass, field
from hashlib import sha256
from typing import TYPE_CHECKING, Any
from django.db import models
from django.http import HttpRequest
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from authentik.core.models import default_token_duration
from authentik.events.signals import get_login_event
@@ -18,6 +16,7 @@ from authentik.providers.oauth2.constants import (
AMR_PASSWORD,
AMR_SMART_CARD,
AMR_WEBAUTHN,
SubModes,
)
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
@@ -30,26 +29,6 @@ def hash_session_key(session_key: str) -> str:
return sha256(session_key.encode("ascii")).hexdigest()
class SubModes(models.TextChoices):
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
USER_ID = "user_id", _("Based on user ID")
USER_UUID = "user_uuid", _("Based on user UUID")
USER_USERNAME = "user_username", _("Based on the username")
USER_EMAIL = (
"user_email",
_("Based on the User's Email. This is recommended over the UPN method."),
)
USER_UPN = (
"user_upn",
_(
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
"Use this method only if you have different UPN and Mail domains."
),
)
@dataclass(slots=True)
class IDToken:
"""The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.11 on 2025-07-04 03:23
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0028_migrate_session"),
]
operations = [
migrations.AddField(
model_name="oauth2provider",
name="backchannel_logout_uri",
field=models.TextField(
blank=True,
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="Back-Channel Logout URI",
),
),
migrations.AlterField(
model_name="oauth2provider",
name="_redirect_uris",
field=models.JSONField(default=list, verbose_name="Redirect URIs"),
),
]

View File

@@ -6,7 +6,7 @@ import json
from dataclasses import asdict, dataclass
from functools import cached_property
from hashlib import sha256
from typing import Any
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse, urlunparse
from cryptography.hazmat.primitives.asymmetric.ec import (
@@ -42,11 +42,14 @@ from authentik.core.models import (
)
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
from authentik.lib.models import SerializerModel
from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.providers.oauth2.id_token import IDToken, SubModes
from authentik.providers.oauth2.constants import SubModes
from authentik.sources.oauth.models import OAuthSource
if TYPE_CHECKING:
from authentik.providers.oauth2.id_token import IDToken
LOGGER = get_logger()
@@ -193,9 +196,14 @@ class OAuth2Provider(WebfingerProvider, Provider):
default=generate_client_secret,
)
_redirect_uris = models.JSONField(
default=dict,
default=list,
verbose_name=_("Redirect URIs"),
)
backchannel_logout_uri = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
verbose_name=_("Back-Channel Logout URI"),
blank=True,
)
include_claims_in_id_token = models.BooleanField(
default=True,
@@ -480,13 +488,15 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
return f"Access Token for {self.provider_id} for user {self.user_id}"
@property
def id_token(self) -> IDToken:
def id_token(self) -> "IDToken":
"""Load ID Token from json"""
from authentik.providers.oauth2.id_token import IDToken
raw_token = json.loads(self._id_token)
return from_dict(IDToken, raw_token)
@id_token.setter
def id_token(self, value: IDToken):
def id_token(self, value: "IDToken"):
self.token = value.to_access_token(self.provider)
self._id_token = json.dumps(asdict(value))
@@ -531,13 +541,15 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
return f"Refresh Token for {self.provider_id} for user {self.user_id}"
@property
def id_token(self) -> IDToken:
def id_token(self) -> "IDToken":
"""Load ID Token from json"""
from authentik.providers.oauth2.id_token import IDToken
raw_token = json.loads(self._id_token)
return from_dict(IDToken, raw_token)
@id_token.setter
def id_token(self, value: IDToken):
def id_token(self, value: "IDToken"):
self._id_token = json.dumps(asdict(value))
@property

View File

@@ -1,17 +1,34 @@
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
from authentik.providers.oauth2.tasks import backchannel_logout_notification_dispatch
LOGGER = get_logger()
@receiver(pre_delete, sender=AuthenticatedSession)
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
sender, instance: AuthenticatedSession, **_
):
"""Revoke tokens upon user logout"""
AccessToken.objects.filter(
LOGGER.debug("Sending back-channel logout notifications signal!", session=instance)
access_tokens = AccessToken.objects.filter(
user=instance.user,
session__session__session_key=instance.session.session_key,
).delete()
)
backchannel_logout_notification_dispatch.send(
revocations=[
(token.provider_id, token.id_token.iss, token.session.user.uid)
for token in access_tokens
],
)
access_tokens.delete()
@receiver(post_save, sender=User)

View File

@@ -0,0 +1,68 @@
"""OAuth2 Provider Tasks"""
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.lib.utils.http import get_http_session
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.utils import create_logout_token
from authentik.tasks.models import Task
LOGGER = get_logger()
@actor(description=_("Send a back-channel logout request to the registered client"))
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
"""Send a back-channel logout request to the registered client
Args:
provider_pk: The OAuth2 provider's primary key
iss: The issuer URL for the logout token
sub: The subject identifier to include in the logout token
Returns:
bool: True if the request was sent successfully, False otherwise
"""
self: Task = CurrentTask.get_task()
LOGGER.debug("Sending back-channel logout request", provider_pk=provider_pk, sub=sub)
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
if provider is None:
return
# Generate the logout token
logout_token = create_logout_token(iss, provider, None, sub)
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
backchannel_logout_uri = provider.backchannel_logout_uri
if not backchannel_logout_uri:
self.info("No back-channel logout URI found for provider")
return
# Send the back-channel logout request
response = get_http_session().post(
backchannel_logout_uri,
data={"logout_token": logout_token},
headers={"Content-Type": "application/x-www-form-urlencoded"},
allow_redirects=True,
)
response.raise_for_status()
self.info("Back-channel logout successful", sub=sub)
return True
@actor(description=_("Handle backchannel logout notifications dispatched via signal"))
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
"""Handle backchannel logout notifications dispatched via signal"""
for revocation in revocations:
provider_pk, iss, sub = revocation
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
send_backchannel_logout_request.send_with_options(
args=(provider_pk, iss, sub),
rel_obj=provider,
)

View File

@@ -81,4 +81,46 @@ class TestAPI(APITestCase):
},
)
self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]})
def test_backchannel_logout_uri_validation(self):
"""Test backchannel_logout_uri API validation"""
response = self.client.post(
reverse("authentik_api:oauth2provider-list"),
data={
"name": generate_id(),
"authorization_flow": create_test_flow().pk,
"invalidation_flow": create_test_flow().pk,
"redirect_uris": [
{"matching_mode": "strict", "url": "http://goauthentik.io"},
],
"backchannel_logout_uri": "invalid-url",
},
)
self.assertEqual(response.status_code, 400)
def test_backchannel_logout_uri_create_and_retrieve(self):
"""Test creating and retrieving backchannel logout URI"""
response = self.client.post(
reverse("authentik_api:oauth2provider-list"),
data={
"name": generate_id(),
"authorization_flow": create_test_flow().pk,
"invalidation_flow": create_test_flow().pk,
"redirect_uris": [
{"matching_mode": "strict", "url": "http://goauthentik.io"},
],
"backchannel_logout_uri": "http://goauthentik.io/logout",
},
)
self.assertEqual(response.status_code, 201)
provider_data = response.json()
self.assertEqual(provider_data["backchannel_logout_uri"], "http://goauthentik.io/logout")
# Test retrieving the provider
provider_pk = provider_data["pk"]
response = self.client.get(
reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider_pk})
)
self.assertEqual(response.status_code, 200)
retrieved_data = response.json()
self.assertEqual(retrieved_data["backchannel_logout_uri"], "http://goauthentik.io/logout")

View File

@@ -0,0 +1,223 @@
"""Test OAuth2 Back-Channel Logout implementation"""
from unittest.mock import Mock, patch
import jwt
from django.test import RequestFactory
from django.utils import timezone
from dramatiq.results.errors import ResultFailure
from requests import Response
from requests.exceptions import HTTPError, Timeout
from authentik.core.models import Application, AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import hash_session_key
from authentik.providers.oauth2.models import (
AccessToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,
RefreshToken,
)
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.utils import create_logout_token
class TestBackChannelLogout(OAuthTestCase):
"""Test Back-Channel Logout functionality"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.user = create_test_admin_user()
self.app = Application.objects.create(name=generate_id(), slug="test-app")
self.provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
redirect_uris=[
RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver/callback"),
],
signing_key=self.keypair,
)
self.app.provider = self.provider
self.app.save()
def _create_session(self, session_key=None):
"""Create a session with the given key or a generated one"""
session_key = session_key or f"session-{generate_id()}"
session = Session.objects.create(
session_key=session_key,
expires=timezone.now() + timezone.timedelta(hours=1),
last_ip="255.255.255.255",
)
auth_session = AuthenticatedSession.objects.create(
session=session,
user=self.user,
)
return auth_session
def _create_token(
self, provider, user, session=None, token_type="access", token_id=None
): # nosec
"""Create a token of the specified type"""
token_id = token_id or f"{token_type}-token-{generate_id()}"
kwargs = {
"provider": provider,
"user": user,
"session": session,
"token": token_id,
"_id_token": "{}",
"auth_time": timezone.now(),
}
if token_type == "access": # nosec
return AccessToken.objects.create(**kwargs)
else: # refresh
return RefreshToken.objects.create(**kwargs)
def _create_provider(self, name=None):
"""Create an OAuth2 provider"""
name = name or f"provider-{generate_id()}"
provider = OAuth2Provider.objects.create(
name=name,
authorization_flow=create_test_flow(),
redirect_uris=[
RedirectURI(RedirectURIMatchingMode.STRICT, f"http://{name}/callback"),
],
signing_key=self.keypair,
)
return provider
def _create_logout_token(
self,
provider: OAuth2Provider | None = None,
session_id: str | None = None,
sub: str | None = None,
):
"""Create a logout token with the given parameters"""
provider = provider or self.provider
# Create a token with the same issuer that the view will expect
# Use the same request object that will be used in the test
request = self.factory.post("/backchannel_logout")
return create_logout_token(
iss=provider.get_issuer(request),
provider=provider,
session_key=session_id,
sub=sub,
)
def _decode_token(self, token, provider=None):
"""Helper to decode and validate a JWT token"""
provider = provider or self.provider
key, alg = provider.jwt_key
if alg != "HS256":
key = provider.signing_key.public_key
return jwt.decode(
token, key, algorithms=[alg], options={"verify_exp": False, "verify_aud": False}
)
def test_create_logout_token_variants(self):
"""Test creating logout tokens with different combinations of parameters"""
# Test case 1: With session_id only
session_id = "test-session-123"
token1 = self._create_logout_token(session_id=session_id)
decoded1 = self._decode_token(token1)
self.assertIn("iss", decoded1)
self.assertEqual(decoded1["aud"], self.provider.client_id)
self.assertIn("iat", decoded1)
self.assertIn("jti", decoded1)
self.assertEqual(decoded1["sid"], hash_session_key(session_id))
self.assertIn("events", decoded1)
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded1["events"])
self.assertNotIn("sub", decoded1)
# Test case 2: With sub only
sub = "user-123"
token2 = self._create_logout_token(sub=sub)
decoded2 = self._decode_token(token2)
self.assertEqual(decoded2["sub"], sub)
self.assertIn("events", decoded2)
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded2["events"])
self.assertNotIn("sid", decoded2)
# Test case 3: With both session_id and sub
token3 = self._create_logout_token(session_id=session_id, sub=sub)
decoded3 = self._decode_token(token3)
self.assertEqual(decoded3["sid"], hash_session_key(session_id))
self.assertEqual(decoded3["sub"], sub)
self.assertIn("events", decoded3)
@patch("authentik.providers.oauth2.tasks.get_http_session")
def test_send_backchannel_logout_request_scenarios(self, mock_get_session):
"""Test various scenarios for backchannel logout request task"""
# Setup provider with backchannel logout URI
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
self.provider.save()
# Setup mock session and response
mock_session = Mock()
mock_get_session.return_value = mock_session
mock_response = Mock(spec=Response)
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None # No exception for successful request
mock_session.post.return_value = mock_response
result = send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
)
self.assertTrue(result)
mock_session.post.assert_called_once()
call_args = mock_session.post.call_args
self.assertIn("logout_token", call_args[1]["data"])
self.assertEqual(
call_args[1]["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Scenario 2: Failed request (400 response) - should raise exception
mock_session.post.reset_mock()
error_response = Mock(spec=Response)
error_response.status_code = 400
error_response.raise_for_status.side_effect = HTTPError("HTTP 400")
mock_session.post.return_value = error_response
with self.assertRaises(ResultFailure):
send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
).get_result()
# Scenario 3: No URI configured
mock_session.post.reset_mock()
self.provider.backchannel_logout_uri = ""
self.provider.save()
result = send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
).get_result()
self.assertIsNone(result)
mock_session.post.assert_not_called()
# Scenario 4: No sub provided - should fail
result = send_backchannel_logout_request.send(
self.provider.pk, "http://testserver"
).get_result()
self.assertIsNone(result)
# Scenario 5: Non-existent provider
result = send_backchannel_logout_request.send(
99999, "http://testserver", sub="test-user-uid"
).get_result()
self.assertIsNone(result)
# Scenario 6: Request timeout
mock_session.post.side_effect = Timeout("Request timed out")
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
self.provider.save()
with self.assertRaises(ResultFailure):
send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
).get_result()

View File

@@ -11,9 +11,9 @@ from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import (
AccessToken,
IDToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,

View File

@@ -10,11 +10,11 @@ from django.utils import timezone
from authentik.core.models import Application, AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import (
AccessToken,
ClientTypes,
DeviceToken,
IDToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,

View File

@@ -11,9 +11,9 @@ from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import (
AccessToken,
IDToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,

View File

@@ -1,8 +1,10 @@
"""OAuth2/OpenID Utils"""
import re
import uuid
from base64 import b64decode
from binascii import Error
from time import time
from typing import Any
from urllib.parse import urlparse
@@ -14,6 +16,7 @@ from structlog.stdlib import get_logger
from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER
from authentik.events.models import Event, EventAction
from authentik.providers.oauth2.errors import BearerTokenError
from authentik.providers.oauth2.id_token import hash_session_key
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
LOGGER = get_logger()
@@ -211,3 +214,36 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
) -> None:
self.allowed_schemes = allowed_schemes or ["http", "https", "ftp"]
super().__init__(redirect_to, *args, **kwargs)
def create_logout_token(
iss: str,
provider: OAuth2Provider,
session_key: str | None = None,
sub: str | None = None,
) -> str:
"""Create a logout token for Back-Channel Logout
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
"""
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
# Create the logout token payload
payload = {
"iss": str(iss),
"aud": provider.client_id,
"iat": int(time()),
"jti": str(uuid.uuid4()),
"events": {
"http://schemas.openid.net/event/backchannel-logout": {},
},
}
# Add either sub or sid (or both)
if sub:
payload["sub"] = sub
if session_key:
payload["sid"] = hash_session_key(session_key)
# Encode the token
return provider.encode(payload)

View File

@@ -9,7 +9,8 @@ from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.providers.oauth2.errors import TokenIntrospectionError
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.utils import TokenResponse, authenticate_provider
LOGGER = get_logger()

View File

@@ -72,6 +72,8 @@ class ProviderInfoView(View):
"device_authorization_endpoint": self.request.build_absolute_uri(
reverse("authentik_providers_oauth2:device")
),
"backchannel_logout_supported": True,
"backchannel_logout_session_supported": True,
"response_types_supported": [
ResponseTypes.CODE,
ResponseTypes.ID_TOKEN,

View File

@@ -75,7 +75,9 @@ TENANT_APPS = [
"pgtrigger",
"authentik.admin",
"authentik.api",
"authentik.core",
"authentik.crypto",
"authentik.enterprise",
"authentik.events",
"authentik.flows",
"authentik.outposts",
@@ -246,10 +248,12 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
MIDDLEWARE_FIRST = [
"django_prometheus.middleware.PrometheusBeforeMiddleware",
]
MIDDLEWARE = [
"django_tenants.middleware.default.DefaultTenantMiddleware",
"authentik.root.middleware.LoggingMiddleware",
"django_prometheus.middleware.PrometheusBeforeMiddleware",
"authentik.root.middleware.ClientIPMiddleware",
"authentik.stages.user_login.middleware.BoundSessionMiddleware",
"authentik.core.middleware.AuthenticationMiddleware",
@@ -262,6 +266,8 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"authentik.core.middleware.ImpersonateMiddleware",
]
MIDDLEWARE_LAST = [
"django_prometheus.middleware.PrometheusAfterMiddleware",
]
@@ -497,7 +503,9 @@ _DISALLOWED_ITEMS = [
"SHARED_APPS",
"TENANT_APPS",
"INSTALLED_APPS",
"MIDDLEWARE_FIRST",
"MIDDLEWARE",
"MIDDLEWARE_LAST",
"AUTHENTICATION_BACKENDS",
"SPECTACULAR_SETTINGS",
"REST_FRAMEWORK",
@@ -515,16 +523,35 @@ SILENCED_SYSTEM_CHECKS = [
]
def _update_settings(app_path: str):
def subtract_list(a: list, b: list) -> list:
return [item for item in a if item not in b]
def _filter_and_update(apps: list[str]) -> None:
for _app in set(apps):
if not _app.startswith("authentik"):
continue
_update_settings(f"{_app}.settings")
def _update_settings(app_path: str) -> None:
try:
settings_module = importlib.import_module(app_path)
CONFIG.log("debug", "Loaded app settings", path=app_path)
SHARED_APPS.extend(getattr(settings_module, "SHARED_APPS", []))
TENANT_APPS.extend(getattr(settings_module, "TENANT_APPS", []))
new_shared_apps = subtract_list(getattr(settings_module, "SHARED_APPS", []), SHARED_APPS)
new_tenant_apps = subtract_list(getattr(settings_module, "TENANT_APPS", []), TENANT_APPS)
SHARED_APPS.extend(new_shared_apps)
TENANT_APPS.extend(new_tenant_apps)
_filter_and_update(new_shared_apps + new_tenant_apps)
MIDDLEWARE_FIRST.extend(getattr(settings_module, "MIDDLEWARE_FIRST", []))
MIDDLEWARE.extend(getattr(settings_module, "MIDDLEWARE", []))
AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", []))
SPECTACULAR_SETTINGS.update(getattr(settings_module, "SPECTACULAR_SETTINGS", {}))
REST_FRAMEWORK.update(getattr(settings_module, "REST_FRAMEWORK", {}))
for _attr in dir(settings_module):
if not _attr.startswith("__") and _attr not in _DISALLOWED_ITEMS:
globals()[_attr] = getattr(settings_module, _attr)
@@ -539,26 +566,13 @@ if DEBUG:
SHARED_APPS.insert(SHARED_APPS.index("django.contrib.staticfiles"), "daphne")
enable_debug_trace(True)
TENANT_APPS.append("authentik.core")
CONFIG.log("info", "Booting authentik", version=__version__)
# Attempt to load enterprise app, if available
try:
importlib.import_module("authentik.enterprise.apps")
CONFIG.log("info", "Enabled authentik enterprise")
TENANT_APPS.append("authentik.enterprise")
_update_settings("authentik.enterprise.settings")
except ImportError:
pass
# Load subapps's settings
for _app in set(SHARED_APPS + TENANT_APPS):
if not _app.startswith("authentik"):
continue
_update_settings(f"{_app}.settings")
_filter_and_update(SHARED_APPS + TENANT_APPS)
_update_settings("data.user_settings")
MIDDLEWARE = list(OrderedDict.fromkeys(MIDDLEWARE_FIRST + MIDDLEWARE + MIDDLEWARE_LAST))
SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))
INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))

View File

@@ -44,6 +44,8 @@ class EmailStageSerializer(StageSerializer):
"subject",
"template",
"activate_user_on_success",
"recovery_max_attempts",
"recovery_cache_timeout",
]
extra_kwargs = {"password": {"write_only": True}}

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.11 on 2025-07-23 11:26
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_email", "0005_alter_emailstage_token_expiry"),
]
operations = [
migrations.AddField(
model_name="emailstage",
name="recovery_cache_timeout",
field=models.TextField(
default="minutes=5",
help_text="The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
migrations.AddField(
model_name="emailstage",
name="recovery_max_attempts",
field=models.PositiveIntegerField(default=5),
),
]

View File

@@ -16,6 +16,8 @@ from authentik.flows.models import Stage
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_string_validator
EMAIL_RECOVERY_MAX_ATTEMPTS = 5
LOGGER = get_logger()
@@ -70,6 +72,17 @@ class EmailStage(Stage):
use_ssl = models.BooleanField(default=False)
timeout = models.IntegerField(default=10)
from_address = models.EmailField(default="system@authentik.local")
recovery_max_attempts = models.PositiveIntegerField(default=EMAIL_RECOVERY_MAX_ATTEMPTS)
recovery_cache_timeout = models.TextField(
default="minutes=5",
validators=[timedelta_string_validator],
help_text=_(
"The time window used to count recent account recovery attempts. "
"If the number of attempts exceed recovery_max_attempts within "
"this period, further attempts will be rate-limited. "
"(Format: hours=1;minutes=2;seconds=3)."
),
)
activate_user_on_success = models.BooleanField(
default=False, help_text=_("Activate users upon completion of stage.")

View File

@@ -1,9 +1,12 @@
"""authentik multi-stage authentication engine"""
from datetime import timedelta
import math
from datetime import UTC, datetime, timedelta
from hashlib import sha256
from uuid import uuid4
from django.contrib import messages
from django.core.cache import cache
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.template.exceptions import TemplateSyntaxError
@@ -27,6 +30,8 @@ from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
EMAIL_RECOVERY_CACHE_KEY = "goauthentik.io/stages/email/stage/"
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
@@ -170,10 +175,66 @@ class EmailStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return super().challenge_invalid(response)
def _get_cache_key(self) -> str:
"""Return the cache key used for rate limiting email recovery attempts."""
user = self.get_pending_user()
user_email_hashed = sha256(user.email.lower().encode("utf-8")).hexdigest()
return EMAIL_RECOVERY_CACHE_KEY + user_email_hashed
def _is_rate_limited(self) -> int | None:
"""Check whether the email recovery attempt should be rate limited.
If the request should be rate limited, update the cache and return the
remaining time in minutes before the user is allowed to try again.
Otherwise, return None."""
cache_key = self._get_cache_key()
attempts = cache.get(cache_key, [])
stage = self.executor.current_stage
stage.refresh_from_db()
max_attempts = stage.recovery_max_attempts
cache_timeout_delta = timedelta_from_string(stage.recovery_cache_timeout)
_now = now()
start_window = _now - cache_timeout_delta
# Convert unix timestamps to datetime objects for comparison
recent_attempts_in_window = [
datetime.fromtimestamp(attempt, UTC)
for attempt in attempts
if datetime.fromtimestamp(attempt, UTC) > start_window
]
if len(recent_attempts_in_window) >= max_attempts:
retry_after = (min(recent_attempts_in_window) + cache_timeout_delta) - _now
minutes_left = max(1, math.ceil(retry_after.total_seconds() / 60))
return minutes_left
recent_attempts_in_window.append(_now)
# Convert datetime objects back to unix timestamps to update cache
recent_attempts_in_window = [attempt.timestamp() for attempt in recent_attempts_in_window]
cache.set(
cache_key,
recent_attempts_in_window,
int(cache_timeout_delta.total_seconds()),
)
return None
def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse:
if minutes_left := self._is_rate_limited():
error = _(
"Too many account verification attempts. Please try again after {minutes} minutes."
).format(minutes=minutes_left)
messages.error(self.request, error)
return super().challenge_invalid(response)
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
messages.error(self.request, _("No pending user."))
return super().challenge_invalid(response)
self.send_email()
messages.success(self.request, _("Email Successfully sent."))
# We can't call stage_ok yet, as we're still waiting

View File

@@ -1,7 +1,9 @@
"""email tests"""
from hashlib import sha256
from unittest.mock import MagicMock, PropertyMock, patch
from django.contrib import messages
from django.core import mail
from django.core.mail.backends.locmem import EmailBackend
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
@@ -9,6 +11,7 @@ from django.test import RequestFactory
from django.urls import reverse
from django.utils.http import urlencode
from authentik.brands.models import Brand
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
@@ -17,6 +20,7 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
@@ -291,3 +295,173 @@ class TestEmailStage(FlowTestCase):
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
)
def test_get_cache_key(self):
"""Test to ensure that the correct cache key is returned."""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = self.factory.post(url)
request.user = self.user
request.session = session
executor = FlowExecutorView(request=request, flow=self.flow)
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
cache_key = stage_view._get_cache_key()
expected_hash = sha256(self.user.email.lower().encode("utf-8")).hexdigest()
expected_cache_key = "goauthentik.io/stages/email/stage/" + expected_hash
self.assertEqual(cache_key, expected_cache_key)
def test_is_rate_limited_returns_none(self):
"""Test to ensure None is returned if the request shouldn't be rate limited."""
self.stage.recovery_max_attempts = 2
self.stage.recovery_cache_timeout = "minutes=10"
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = self.factory.post(url)
request.user = self.user
request.session = session
executor = FlowExecutorView(request=request, flow=self.flow)
executor.current_stage = self.stage
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
result = stage_view._is_rate_limited()
self.assertIsNone(result)
def test_is_rate_limited_returns_remaining_time(self):
"""Test to ensure the remaining time is returned if the request
should be rate limited."""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = self.factory.post(url)
request.user = self.user
request.session = session
executor = FlowExecutorView(request=request, flow=self.flow)
executor.current_stage = self.stage
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
test_cases = [
# 2 attempts within 2 minutes
(2, "seconds=120", 2),
# 4 attempts within 5 minutes
(4, "minutes=5", 5),
# 6 attempts within 5 minutes. Although 299 seconds is less than
# 5 minutes, the user is intentionally shown "5 minutes". This is
# because an initial rate limiting message like "Try again after 4 minutes"
# can be confusing.
(6, "seconds=299", 5),
]
for test_case in test_cases:
max_attempts, cache_timeout, minutes_remaining = test_case
with self.subTest(
f"Test recovery with {max_attempts} max attempts and "
f"{cache_timeout} cache timeout seconds"
):
self.stage.recovery_max_attempts = max_attempts
self.stage.recovery_cache_timeout = cache_timeout
self.stage.save()
# Simulate multiple requests
for _ in range(max_attempts):
stage_view._is_rate_limited()
# The following request should be rate-limited
result = stage_view._is_rate_limited()
self.assertEqual(result, minutes_remaining)
def _challenge_invalid_helper(self):
"""Helper to test the challenge_invalid() method."""
self.stage.recovery_max_attempts = 1
self.stage.recovery_cache_timeout = "seconds=300"
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = get_request(url, user=self.user)
request.session = session
request.brand = Brand.objects.create(domain="foo-domain.com", default=True)
executor = FlowExecutorView(request=request, flow=self.flow)
executor.current_stage = self.stage
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
challenge_response = stage_view.get_response_instance(data={})
challenge_response.is_valid()
return challenge_response, stage_view, request
def test_challenge_invalid_not_rate_limited(self):
"""Tests that the request is not rate limited and email is sent."""
challenge_response, stage_view, request = self._challenge_invalid_helper()
with patch.object(stage_view, "send_email") as mock_send_email:
result = stage_view.challenge_invalid(challenge_response)
self.assertEqual(result.status_code, 200)
mock_send_email.assert_called_once()
message_list = list(messages.get_messages(request))
self.assertEqual(len(message_list), 1)
self.assertEqual(
"Email Successfully sent.",
message_list[-1].message,
)
def test_challenge_invalid_returns_error_if_rate_limited(self):
"""Tests that an error is returned if the request is rate limited. Ensure
that an email is not sent."""
challenge_response, stage_view, request = self._challenge_invalid_helper()
# Initial request that shouldn't be rate limited
stage_view.challenge_invalid(challenge_response)
with patch.object(stage_view, "send_email") as mock_send_email:
# This next request should be rate limited
result = stage_view.challenge_invalid(challenge_response)
self.assertEqual(result.status_code, 200)
mock_send_email.assert_not_called()
message_list = list(messages.get_messages(request))
self.assertEqual(len(message_list), 2)
self.assertEqual(
"Too many account verification attempts. Please try again after 5 minutes.",
message_list[-1].message,
)

View File

@@ -61,6 +61,8 @@ entries:
subject: authentik
template: email/password_reset.html
activate_user_on_success: true
recovery_max_attempts: 5
recovery_cache_timeout: minutes=5
- identifiers:
name: default-recovery-user-write
id: default-recovery-user-write

View File

@@ -4689,6 +4689,14 @@
"format": "uuid"
},
"title": "Roles"
},
"children": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Children"
}
},
"required": []
@@ -8465,6 +8473,10 @@
},
"title": "Redirect uris"
},
"backchannel_logout_uri": {
"type": "string",
"title": "Back-Channel Logout URI"
},
"sub_mode": {
"type": "string",
"enum": [
@@ -14324,6 +14336,18 @@
"type": "boolean",
"title": "Activate user on success",
"description": "Activate users upon completion of stage."
},
"recovery_max_attempts": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"title": "Recovery max attempts"
},
"recovery_cache_timeout": {
"type": "string",
"minLength": 1,
"title": "Recovery cache timeout",
"description": "The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3)."
}
},
"required": []

2
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025064.2
goauthentik.io/api/v3 v3.2025064.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.16.0

4
go.sum
View File

@@ -185,8 +185,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025064.2 h1:WFXe12hfsRe29EkLCxWCvrdK6peAkCA6ftdEh04hKLg=
goauthentik.io/api/v3 v3.2025064.2/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
goauthentik.io/api/v3 v3.2025064.5 h1:dP+zHANvdff0Md7oWMvy6KtaY8HbCLVaA3RTrdxLhck=
goauthentik.io/api/v3 v3.2025064.5/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=

View File

@@ -17,6 +17,7 @@ type LDAPGroup struct {
Uid string
GidNumber string
Member []string
MemberOf []string
IsSuperuser bool
IsVirtualGroup bool
Attributes map[string]interface{}
@@ -38,6 +39,7 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
"ak-superuser": {strconv.FormatBool(lg.IsSuperuser)},
"objectClass": objectClass,
"member": lg.Member,
"memberOf": lg.MemberOf,
"cn": {lg.CN},
"uid": {lg.Uid},
"sAMAccountName": {lg.CN},
@@ -52,7 +54,8 @@ func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup {
CN: g.Name,
Uid: string(g.Pk),
GidNumber: si.GetGroupGidNumber(g),
Member: si.UsersForGroup(g),
Member: si.MembersForGroup(g),
MemberOf: si.MemberOfForGroup(g),
IsVirtualGroup: false,
IsSuperuser: *g.IsSuperuser,
Attributes: g.Attributes,

View File

@@ -155,7 +155,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if needGroups {
errs.Go(func() error {
gapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_group")
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()).IncludeUsers(true), parsedFilter, false)
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()).IncludeUsers(true).IncludeChildren(true), parsedFilter, false)
if skip {
req.Log().Trace("Skip backend request")
return nil

View File

@@ -165,7 +165,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
for _, u := range g.UsersObj {
if flag.UserPk == u.Pk {
// TODO: Is there a better way to clone this object?
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.ParentName, []api.GroupMember{u}, []api.Role{})
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.ParentName, []api.GroupMember{u}, []api.Role{}, []api.GroupChild{})
fg.SetUsers([]int32{flag.UserPk})
if g.Parent.IsSet() {
if p := g.Parent.Get(); p != nil {

View File

@@ -32,7 +32,8 @@ type LDAPServerInstance interface {
GetUserGidNumber(api.User) string
GetGroupGidNumber(api.Group) string
UsersForGroup(api.Group) []string
MembersForGroup(api.Group) []string
MemberOfForGroup(api.Group) []string
GetFlags(dn string) *flags.UserFlags
SetFlags(dn string, flags *flags.UserFlags)

View File

@@ -15,12 +15,27 @@ func (pi *ProviderInstance) GroupsForUser(user api.User) []string {
return groups
}
func (pi *ProviderInstance) UsersForGroup(group api.Group) []string {
func (pi *ProviderInstance) MembersForGroup(group api.Group) []string {
users := make([]string, len(group.UsersObj))
for i, user := range group.UsersObj {
users[i] = pi.GetUserDN(user.Username)
}
return users
children := make([]string, len(group.ChildrenObj))
for i, child := range group.ChildrenObj {
children[i] = pi.GetGroupDN(child.Name)
}
return append(users, children...)
}
func (pi *ProviderInstance) MemberOfForGroup(group api.Group) []string {
if group.ParentName.IsSet() {
parent := group.ParentName.Get()
if parent != nil {
return []string{pi.GetGroupDN(*group.ParentName.Get())}
}
}
return []string{}
}
func (pi *ProviderInstance) GetUserDN(user string) string {

View File

@@ -37,11 +37,16 @@ ARG VERSION
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
LABEL org.opencontainers.image.url=https://goauthentik.io
LABEL org.opencontainers.image.description="goauthentik.io LDAP outpost, see https://goauthentik.io for more info."
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
LABEL org.opencontainers.image.version=${VERSION}
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
org.opencontainers.image.description="goauthentik.io LDAP outpost, see https://goauthentik.io for more info." \
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
org.opencontainers.image.title="authentik LDAP outpost image" \
org.opencontainers.image.url="https://goauthentik.io" \
org.opencontainers.image.vendor="Authentik Security Inc." \
org.opencontainers.image.version=${VERSION}
RUN apt-get update && \
apt-get upgrade -y && \

Binary file not shown.

View File

@@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
"POT-Creation-Date: 2025-07-28 16:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@@ -33,6 +33,10 @@ msgstr ""
msgid "Version history"
msgstr "Historique des versions"
#: authentik/admin/tasks.py
msgid "Update latest version info."
msgstr "Mettre à jour les dernières informations de version."
#: authentik/admin/tasks.py
#, python-brace-format
msgid "New version {version} available!"
@@ -88,10 +92,25 @@ msgstr "Instances du plan"
msgid "authentik Export - {date}"
msgstr "Export authentik - {date}"
#: authentik/blueprints/v1/tasks.py authentik/crypto/tasks.py
#, python-brace-format
msgid "Successfully imported {count} files."
msgstr "{count} fichiers importés avec succès."
#: authentik/blueprints/v1/tasks.py
msgid "Find blueprints as `blueprints_find` does, but return a safe dict."
msgstr ""
"Cherche les plans comme le fait `blueprints_find`, mais renvoie un safe "
"dict."
#: authentik/blueprints/v1/tasks.py
msgid "Find blueprints and check if they need to be created in the database."
msgstr ""
"Cherche les plans et vérifie s'ils doivent être créés dans la base de "
"données."
#: authentik/blueprints/v1/tasks.py
msgid "Apply single blueprint."
msgstr "Applique un seul plan."
#: authentik/blueprints/v1/tasks.py
msgid "Remove blueprints which couldn't be fetched."
msgstr "Supprime les plans qui n'ont pas pu être récupérés."
#: authentik/brands/models.py
msgid ""
@@ -129,10 +148,6 @@ msgstr "Marques"
msgid "User does not have access to application."
msgstr "L'utilisateur n'a pas accès à l'application."
#: authentik/core/api/devices.py
msgid "Extra description not available"
msgstr "Description supplémentaire indisponible"
#: authentik/core/api/groups.py
msgid "Cannot set group as parent of itself."
msgstr "Impossible de définir le groupe en tant que parent de lui-même."
@@ -379,6 +394,10 @@ msgstr "Jetons"
msgid "View token's key"
msgstr "Voir la clé du jeton"
#: authentik/core/models.py
msgid "Set a token's key"
msgstr "Définir la clé d'un jeton"
#: authentik/core/models.py
msgid "Property Mapping"
msgstr "Mappage de propriété"
@@ -434,6 +453,14 @@ msgstr "{source} liée avec succès !"
msgid "Source is not configured for enrollment."
msgstr "La source n'est pas configurée pour l'inscription."
#: authentik/core/tasks.py
msgid "Remove expired objects."
msgstr "Supprime les objets expirés"
#: authentik/core/tasks.py
msgid "Remove temporary users created by SAML Sources."
msgstr "Supprime les utilisateurs temporaires créés par les sources SAML."
#: authentik/core/templates/if/error.html
msgid "Go home"
msgstr "Retourner à l'accueil"
@@ -486,6 +513,12 @@ msgstr "Paire de clé/certificat"
msgid "Certificate-Key Pairs"
msgstr "Paires de clé/certificat"
#: authentik/crypto/tasks.py
msgid "Discover, import and update certificates from the filesystem."
msgstr ""
"Découvre, importe et met à jour les certificats depuis le système de "
"fichiers."
#: authentik/enterprise/api.py
msgid "Enterprise is required to create/update this object."
msgstr "Entreprise est requis pour créer/mettre à jour cet objet."
@@ -538,6 +571,18 @@ msgstr "Politiques d'unicité des mots de passe"
msgid "User Password History"
msgstr "Historique des mots de passe utilisateur"
#: authentik/enterprise/policies/unique_password/tasks.py
msgid ""
"Check if any UniquePasswordPolicy exists, and if not, purge the password "
"history table."
msgstr ""
"Vérifie si une politique de mot de passe unique existe et, si ce n'est pas "
"le cas, purge la table de l'historique des mots de passe."
#: authentik/enterprise/policies/unique_password/tasks.py
msgid "Remove user password history that are too old."
msgstr "Supprime l'historique des mots de passe utilisateur trop anciens."
#: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature."
msgstr "Entreprise est requis pour accéder à cette fonctionnalité."
@@ -586,6 +631,42 @@ msgstr "Mappage de propriété Google Workspace"
msgid "Google Workspace Provider Mappings"
msgstr "Mappages de propriété Google Workspace"
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Sync Google Workspace provider objects."
msgstr "Synchronise les objets du fournisseur Google Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Full sync for Google Workspace provider."
msgstr "Synchronisation complète pour le fournisseur Google Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Sync a direct object (user, group) for Google Workspace provider."
msgstr ""
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur Google"
" Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid ""
"Dispatch syncs for a direct object (user, group) for Google Workspace "
"providers."
msgstr ""
"Déclenche des synchronisations pour un objet direct (utilisateur, groupe) "
"pour les fournisseurs Google Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Sync a related object (memberships) for Google Workspace provider."
msgstr ""
"Synchronise un objet lié (appartenances) pour le fournisseur Google "
"Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid ""
"Dispatch syncs for a related object (memberships) for Google Workspace "
"providers."
msgstr ""
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
"fournisseurs Google Workspace."
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider User"
msgstr "Utilisateur du fournisseur Microsoft Entra"
@@ -614,6 +695,42 @@ msgstr "Mappage de propriété Microsoft Entra"
msgid "Microsoft Entra Provider Mappings"
msgstr "Mappages de propriété Microsoft Entra"
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Sync Microsoft Entra provider objects."
msgstr "Synchronise les objets du fournisseur Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Full sync for Microsoft Entra provider."
msgstr "Synchronisation complète pour le fournisseur Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Sync a direct object (user, group) for Microsoft Entra provider."
msgstr ""
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur "
"Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid ""
"Dispatch syncs for a direct object (user, group) for Microsoft Entra "
"providers."
msgstr ""
"Déclenche les synchronisations pour un objet direct (utilisateur, groupe) "
"pour les fournisseurs Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Sync a related object (memberships) for Microsoft Entra provider."
msgstr ""
"Synchronise un objet lié (appartenances) pour le fournisseur Microsoft "
"Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid ""
"Dispatch syncs for a related object (memberships) for Microsoft Entra "
"providers."
msgstr ""
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
"fournisseurs Microsoft Entra."
#: authentik/enterprise/providers/ssf/models.py
#: authentik/providers/oauth2/models.py
msgid "Signing Key"
@@ -652,8 +769,12 @@ msgid "SSF Stream Events"
msgstr "Évènements du flux SSF"
#: authentik/enterprise/providers/ssf/tasks.py
msgid "Failed to send request"
msgstr "Échec de l'envoi de la requête"
msgid "Dispatch SSF events."
msgstr "Distribue les événements SSF."
#: authentik/enterprise/providers/ssf/tasks.py
msgid "Send an SSF event."
msgstr "Envoye un événement SSF."
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
@@ -725,10 +846,9 @@ msgstr "Étape Source"
msgid "Source Stages"
msgstr "Étapes Source"
#: authentik/events/api/tasks.py
#, python-brace-format
msgid "Successfully started task {name}."
msgstr "La tâche {name} a été démarrée avec succès."
#: authentik/enterprise/tasks.py
msgid "Update enterprise license status."
msgstr "Mettre à jour le statut de licence entreprise."
#: authentik/events/models.py
msgid "Event"
@@ -840,6 +960,15 @@ msgstr ""
"Définir à quel groupe d'utilisateur cette notification doit être envoyée et "
"affichée. Si laissé vide, les notifications ne seront pas envoyées."
#: authentik/events/models.py
msgid ""
"When enabled, notification will be sent to user the user that triggered the "
"event.When destination_group is configured, notification is sent to both."
msgstr ""
"Lorsque cette option est activée, une notification est envoyée à "
"l'utilisateur qui a déclenché l'événement. Si destination_group est "
"configuré, la notification est envoyée aux deux."
#: authentik/events/models.py
msgid "Notification Rule"
msgstr "Règle de Notification"
@@ -856,10 +985,6 @@ msgstr "Mappage de Webhook"
msgid "Webhook Mappings"
msgstr "Mappages de Webhook"
#: authentik/events/models.py
msgid "Run task"
msgstr "Lancer la tâche"
#: authentik/events/models.py
msgid "System Task"
msgstr "Tâches du système"
@@ -868,9 +993,31 @@ msgstr "Tâches du système"
msgid "System Tasks"
msgstr "Tâches du système"
#: authentik/events/system_tasks.py
msgid "Task has not been run yet."
msgstr "Tâche pas encore exécutée."
#: authentik/events/tasks.py
msgid "Dispatch new event notifications."
msgstr "Envoye les notifications d'un nouvel événement."
#: authentik/events/tasks.py
msgid ""
"Check if policies attached to NotificationRule match event and dispatch "
"notification tasks."
msgstr ""
"Vérifier si les politiques attachées à une règle de notifications "
"correspondent à l'événement et déclenche les tâches de notification."
#: authentik/events/tasks.py
msgid "Send notification."
msgstr "Envoye une notification."
#: authentik/events/tasks.py
msgid "Cleanup events for GDPR compliance."
msgstr "Nettoye les événements pour la conformité au RGPD."
#: authentik/events/tasks.py
msgid "Cleanup seen notifications and notifications whose event expired."
msgstr ""
"Nettoye les notifications vues et les notifications dont l'événement a "
"expiré."
#: authentik/flows/api/flows.py
#, python-brace-format
@@ -1051,32 +1198,6 @@ msgstr ""
"Si activé, le fournisseur ne changera ou ne créera pas d'objets auprès du "
"système distant."
#: authentik/lib/sync/outgoing/tasks.py
msgid "Starting full provider sync"
msgstr "Démarrage d'une synchronisation complète du fournisseur"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing users"
msgstr "Synchronisation des utilisateurs"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "Synchronisation des groupes"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of {object_type}"
msgstr "Synchronisation de la page {page} de {object_type}"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Dropping mutating request due to dry run"
msgstr "Abandon de la requête de mutation en raison d'une simulation"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Stopping sync due to error: {error}"
msgstr "Arrêt de la synchronisation due à l'erreur : {error}"
#: authentik/lib/utils/time.py
#, python-format
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
@@ -1183,6 +1304,32 @@ msgstr "Avant-poste"
msgid "Outposts"
msgstr "Avant-postes"
#: authentik/outposts/tasks.py
msgid "Update cached state of service connection."
msgstr "Met à jour l'état mis en cache de la connexion de service."
#: authentik/outposts/tasks.py
msgid "Create/update/monitor/delete the deployment of an Outpost."
msgstr "Crée/met à jour/surveille/supprime le déploiement d'un avant-poste."
#: authentik/outposts/tasks.py
msgid "Ensure that all Outposts have valid Service Accounts and Tokens."
msgstr ""
"S'assure que tous les avant-postes ont des comptes de service et des jetons "
"valides."
#: authentik/outposts/tasks.py
msgid "Send update to outpost"
msgstr "Envoye une mise à jour à un avant-poste"
#: authentik/outposts/tasks.py
msgid "Checks the local environment and create Service connections."
msgstr "Vérifie l'environnement local et crée les connexions de service."
#: authentik/outposts/tasks.py
msgid "Terminate session on all outposts."
msgstr "Met fin à la session sur tous les avant-postes."
#: authentik/policies/denied.py
msgid "Access denied"
msgstr "Accès refusé"
@@ -1901,6 +2048,10 @@ msgstr "Fournisseur Proxy"
msgid "Proxy Providers"
msgstr "Fournisseur de Proxy"
#: authentik/providers/proxy/tasks.py
msgid "Terminate session on Proxy outpost."
msgstr "Met fin à la session sur l'avant-poste Proxy."
#: authentik/providers/rac/models.py authentik/stages/user_login/models.py
msgid ""
"Determines how long a session lasts. Default of 0 means that the sessions "
@@ -2245,6 +2396,35 @@ msgstr "Mappage fournisseur SCIM"
msgid "SCIM Provider Mappings"
msgstr "Mappages fournisseur SCIM"
#: authentik/providers/scim/tasks.py
msgid "Sync SCIM provider objects."
msgstr "Synchronise les objets du fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Full sync for SCIM provider."
msgstr "Synchronisation complète pour le fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Sync a direct object (user, group) for SCIM provider."
msgstr ""
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Dispatch syncs for a direct object (user, group) for SCIM providers."
msgstr ""
"Déclenche les synchronisations pour un objet direct (utilisateur, groupe) "
"pour les fournisseurs SCIM."
#: authentik/providers/scim/tasks.py
msgid "Sync a related object (memberships) for SCIM provider."
msgstr "Synchronise un objet lié (appartenances) pour le fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Dispatch syncs for a related object (memberships) for SCIM providers."
msgstr ""
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
"fournisseurs SCIM."
#: authentik/rbac/models.py
msgid "Role"
msgstr "Rôle"
@@ -2399,6 +2579,14 @@ msgstr "Connexion du groupe à la source Kerberos"
msgid "Group Kerberos Source Connections"
msgstr "Connexions du groupe à la source Kerberos"
#: authentik/sources/kerberos/tasks.py
msgid "Check connectivity for Kerberos sources."
msgstr "Vérifie la connectivité des sources Kerberos."
#: authentik/sources/kerberos/tasks.py
msgid "Sync Kerberos source."
msgstr "Synchronise la source Kerberos."
#: authentik/sources/kerberos/views.py
msgid "SPNEGO authentication required"
msgstr "Authentification SPNEGO requise"
@@ -2566,6 +2754,18 @@ msgstr "Connexions du groupe à la source LDAP"
msgid "Password does not match Active Directory Complexity."
msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory."
#: authentik/sources/ldap/tasks.py
msgid "Check connectivity for LDAP source."
msgstr "Vérifie la connectivité des sources LDAP."
#: authentik/sources/ldap/tasks.py
msgid "Sync LDAP source."
msgstr "Synchronise la source LDAP."
#: authentik/sources/ldap/tasks.py
msgid "Sync page for LDAP source."
msgstr "Synchronise une page pour la source LDAP."
#: authentik/sources/oauth/clients/oauth2.py
msgid "No token received."
msgstr "Pas de jeton reçu."
@@ -2715,6 +2915,14 @@ msgstr "Source d'OAuth Azure AD"
msgid "Azure AD OAuth Sources"
msgstr "Source d'OAuth Azure AD"
#: authentik/sources/oauth/models.py
msgid "Entra ID OAuth Source"
msgstr "Source d'OAuth Entra ID"
#: authentik/sources/oauth/models.py
msgid "Entra ID OAuth Sources"
msgstr "Sources d'OAuth Entra ID"
#: authentik/sources/oauth/models.py
msgid "OpenID OAuth Source"
msgstr "Source d'OAuth OpenID"
@@ -2771,6 +2979,14 @@ msgstr "Connexion du groupe à la source OAuth"
msgid "Group OAuth Source Connections"
msgstr "Connexions du groupe à la source OAuth"
#: authentik/sources/oauth/tasks.py
msgid ""
"Update OAuth sources' config from well_known, and JWKS info from the "
"configured URL."
msgstr ""
"Met à jour la configuration des sources OAuth à partir de well_known, et les"
" informations JWKS à partir de l'URL configurée."
#: authentik/sources/oauth/views/callback.py
#, python-brace-format
msgid "Authentication failed: {reason}"
@@ -2829,6 +3045,10 @@ msgstr "Connexion du groupe à la source Plex"
msgid "Group Plex Source Connections"
msgstr "Connexions du groupe à la source OAuth"
#: authentik/sources/plex/tasks.py
msgid "Check the validity of a Plex source."
msgstr "Vérifie la validité d'une source Plex."
#: authentik/sources/saml/models.py
msgid "Redirect Binding"
msgstr "Liaison de Redirection"
@@ -3273,6 +3493,13 @@ msgstr "Type d'appareil WebAuthn"
msgid "WebAuthn Device types"
msgstr "Types d'appareil WebAuthn"
#: authentik/stages/authenticator_webauthn/tasks.py
msgid ""
"Background task to import FIDO Alliance MDS blob and AAGUIDs into database."
msgstr ""
"Tâche de fond pour importer le blob MDS de la FIDO Alliance et les AAGUID "
"dans la base de données."
#: authentik/stages/captcha/models.py
msgid "Public key, acquired your captcha Provider."
msgstr "Clé publique, acquise auprès de votre fournisseur captcha."
@@ -3399,6 +3626,10 @@ msgstr "Email envoyé."
msgid "Email Successfully sent."
msgstr "Couriel envoyé avec succès."
#: authentik/stages/email/tasks.py
msgid "Send email."
msgstr "Envoye un courriel."
#: authentik/stages/email/templates/email/account_confirmation.html
#: authentik/stages/email/templates/email/account_confirmation.txt
msgid "Welcome!"
@@ -3867,6 +4098,16 @@ msgstr ""
"souvenir de moi ne sera pas proposée. (Format: "
"hours=-1;minutes=-2;seconds=-3)"
#: authentik/stages/user_login/models.py
msgid ""
"When set to a non-zero value, authentik will save a cookie with a longer "
"expiry,to remember the device the user is logging in from. (Format: "
"hours=-1;minutes=-2;seconds=-3)"
msgstr ""
"Si cette valeur est différente de zéro, authentik enregistrera un cookie "
"avec une expiration plus longue, afin de se souvenir de l'appareil à partir "
"duquel l'utilisateur se connecte. (Format : hours=-1;minutes=-2;seconds=-3)"
#: authentik/stages/user_login/models.py
msgid "User Login Stage"
msgstr "Étape de connexion utlisateur"
@@ -3918,6 +4159,38 @@ msgid "Failed to update user. Please try again later."
msgstr ""
"Échec de mise à jour de l'utilisateur. Merci de réessayer ultérieurement,"
#: authentik/tasks/models.py
msgid "Tenant this task belongs to"
msgstr "Tenant auquel cette tâche appartient"
#: authentik/tasks/models.py
msgid "Retry failed task"
msgstr "Relancer la tâche échouée"
#: authentik/tasks/models.py
msgid "Worker status"
msgstr "État du worker"
#: authentik/tasks/models.py
msgid "Worker statuses"
msgstr "États du worker"
#: authentik/tasks/schedules/models.py
msgid "Unique schedule identifier"
msgstr "Identifiant unique des planifications"
#: authentik/tasks/schedules/models.py
msgid "User schedule identifier"
msgstr "Identifiant utilisateur des planifications"
#: authentik/tasks/schedules/models.py
msgid "Manually trigger a schedule"
msgstr "Déclencher manuellement une planification"
#: authentik/tasks/tasks.py
msgid "Remove old worker statuses."
msgstr "Supprime les anciens statuts des workers."
#: authentik/tenants/models.py
msgid ""
"Schema name must start with t_, only contain lowercase letters and numbers "
@@ -4010,3 +4283,76 @@ msgstr "Domaine"
#: authentik/tenants/models.py
msgid "Domains"
msgstr "Domaines"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Queue name"
msgstr "Nom de la file"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Dramatiq actor name"
msgstr "Nom de l'acteur Dramatiq"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Message body"
msgstr "Corps du message"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task status"
msgstr "État de la tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task last modified time"
msgstr "Heure de dernière modification de la tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task result"
msgstr "Résultat de la tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Result expiry time"
msgstr "Délai d'expiration du résultat"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task"
msgstr "Tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Tasks"
msgstr "Tâches"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
#, python-format
msgid "%(value)s is not a valid crontab"
msgstr "%(value)s n'est pas un crontab valide"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Dramatiq actor to call"
msgstr "Acteur Dramatiq à invoquer"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Args to send to the actor"
msgstr "Args à passer à l'acteur"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Kwargs to send to the actor"
msgstr "Kwargs à passer à l'acteur"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Options to send to the actor"
msgstr "Options à passer à l'acteur"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "When to schedule tasks"
msgstr "Quand planifier les tâches"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Pause this schedule"
msgstr "Mettre cette planification en pause"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Schedule"
msgstr "Planification"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Schedules"
msgstr "Planifications"

View File

@@ -186,19 +186,19 @@ class MetricsMiddleware(Middleware):
"The total number of dead-lettered tasks.",
self.labels,
)
self.inprogress_messages = Gauge(
f"{self.prefix}_tasks_inprogress",
self.in_progress_messages = Gauge(
f"{self.prefix}_tasks_in_progress",
"The number of tasks in progress.",
self.labels,
multiprocess_mode="livesum",
)
self.inprogress_delayed_messages = Gauge(
f"{self.prefix}_tasks_delayed_inprogress",
self.in_progress_delayed_messages = Gauge(
f"{self.prefix}_tasks_delayed_in_progress",
"The number of delayed tasks in memory.",
self.labels,
)
self.messages_durations = Histogram(
f"{self.prefix}_tasks_duration_miliseconds",
f"{self.prefix}_tasks_duration_milliseconds",
"The time spent processing tasks.",
self.labels,
buckets=(
@@ -244,15 +244,15 @@ class MetricsMiddleware(Middleware):
def before_delay_message(self, broker: Broker, message: Message):
self.delayed_messages.add(message.message_id)
self.inprogress_delayed_messages.labels(*self._make_labels(message)).inc()
self.in_progress_delayed_messages.labels(*self._make_labels(message)).inc()
def before_process_message(self, broker: Broker, message: Message):
labels = self._make_labels(message)
if message.message_id in self.delayed_messages:
self.delayed_messages.remove(message.message_id)
self.inprogress_delayed_messages.labels(*labels).dec()
self.in_progress_delayed_messages.labels(*labels).dec()
self.inprogress_messages.labels(*labels).inc()
self.in_progress_messages.labels(*labels).inc()
self.message_start_times[message.message_id] = current_millis()
def after_process_message(
@@ -269,7 +269,7 @@ class MetricsMiddleware(Middleware):
message_duration = current_millis() - message_start_time
self.messages_durations.labels(*labels).observe(message_duration)
self.inprogress_messages.labels(*labels).dec()
self.in_progress_messages.labels(*labels).dec()
self.total_messages.labels(*labels).inc()
if exception is not None:
self.total_errored_messages.labels(*labels).inc()

View File

@@ -53,11 +53,16 @@ ARG VERSION
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
LABEL org.opencontainers.image.url=https://goauthentik.io
LABEL org.opencontainers.image.description="goauthentik.io Proxy outpost image, see https://goauthentik.io for more info."
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
LABEL org.opencontainers.image.version=${VERSION}
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
org.opencontainers.image.description="goauthentik.io Proxy outpost image, see https://goauthentik.io for more info." \
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
org.opencontainers.image.title="authentik proxy outpost image" \
org.opencontainers.image.url="https://goauthentik.io" \
org.opencontainers.image.vendor="Authentik Security Inc." \
org.opencontainers.image.version=${VERSION}
RUN apt-get update && \
apt-get upgrade -y && \

View File

@@ -37,11 +37,16 @@ ARG VERSION
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
LABEL org.opencontainers.image.url=https://goauthentik.io
LABEL org.opencontainers.image.description="goauthentik.io RAC outpost, see https://goauthentik.io for more info."
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
LABEL org.opencontainers.image.version=${VERSION}
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
org.opencontainers.image.description="goauthentik.io RAC outpost, see https://goauthentik.io for more info." \
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
org.opencontainers.image.title="authentik RAC outpost image" \
org.opencontainers.image.url="https://goauthentik.io" \
org.opencontainers.image.vendor="Authentik Security Inc." \
org.opencontainers.image.version=${VERSION}
USER root
RUN apt-get update && \

View File

@@ -37,11 +37,16 @@ ARG VERSION
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
LABEL org.opencontainers.image.url=https://goauthentik.io
LABEL org.opencontainers.image.description="goauthentik.io Radius outpost, see https://goauthentik.io for more info."
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
LABEL org.opencontainers.image.version=${VERSION}
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
org.opencontainers.image.description="goauthentik.io Radius outpost, see https://goauthentik.io for more info." \
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
org.opencontainers.image.title="authentik RADIUS outpost image" \
org.opencontainers.image.url="https://goauthentik.io" \
org.opencontainers.image.vendor="Authentik Security Inc." \
org.opencontainers.image.version=${VERSION}
RUN apt-get update && \
apt-get upgrade -y && \

View File

@@ -4718,6 +4718,11 @@ paths:
schema:
type: string
description: Attributes
- in: query
name: include_children
schema:
type: boolean
default: false
- in: query
name: include_users
schema:
@@ -4840,6 +4845,11 @@ paths:
format: uuid
description: A UUID string identifying this Group.
required: true
- in: query
name: include_children
schema:
type: boolean
default: false
- in: query
name: include_users
schema:
@@ -5654,6 +5664,21 @@ paths:
schema:
type: string
description: Attributes
- in: query
name: date_joined
schema:
type: string
format: date-time
- in: query
name: date_joined__gt
schema:
type: string
format: date-time
- in: query
name: date_joined__lt
schema:
type: string
format: date-time
- in: query
name: email
schema:
@@ -5688,6 +5713,21 @@ paths:
name: is_superuser
schema:
type: boolean
- in: query
name: last_updated
schema:
type: string
format: date-time
- in: query
name: last_updated__gt
schema:
type: string
format: date-time
- in: query
name: last_updated__lt
schema:
type: string
format: date-time
- in: query
name: name
schema:
@@ -44805,6 +44845,15 @@ components:
activate_user_on_success:
type: boolean
description: Activate users upon completion of stage.
recovery_max_attempts:
type: integer
maximum: 2147483647
minimum: 0
recovery_cache_timeout:
type: string
description: 'The time window used to count recent account recovery attempts.
If the number of attempts exceed recovery_max_attempts within this period,
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
required:
- component
- meta_model_name
@@ -44865,6 +44914,16 @@ components:
activate_user_on_success:
type: boolean
description: Activate users upon completion of stage.
recovery_max_attempts:
type: integer
maximum: 2147483647
minimum: 0
recovery_cache_timeout:
type: string
minLength: 1
description: 'The time window used to count recent account recovery attempts.
If the number of attempts exceed recovery_max_attempts within this period,
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
required:
- name
Endpoint:
@@ -46478,13 +46537,50 @@ components:
items:
$ref: '#/components/schemas/Role'
readOnly: true
children:
type: array
items:
type: string
format: uuid
children_obj:
type: array
items:
$ref: '#/components/schemas/GroupChild'
readOnly: true
nullable: true
required:
- children_obj
- name
- num_pk
- parent_name
- pk
- roles_obj
- users_obj
GroupChild:
type: object
description: Stripped down group serializer to show relevant children for groups
properties:
pk:
type: string
format: uuid
readOnly: true
title: Group uuid
name:
type: string
is_superuser:
type: boolean
description: Users added to this group will be superusers.
attributes:
type: object
additionalProperties: {}
group_uuid:
type: string
format: uuid
readOnly: true
required:
- group_uuid
- name
- pk
GroupKerberosSourceConnection:
type: object
description: Group Source Connection
@@ -46806,6 +46902,11 @@ components:
items:
type: string
format: uuid
children:
type: array
items:
type: string
format: uuid
required:
- name
GroupSAMLSourceConnection:
@@ -49421,6 +49522,10 @@ components:
type: array
items:
$ref: '#/components/schemas/RedirectURI'
backchannel_logout_uri:
type: string
title: Back-Channel Logout URI
format: uri
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -49528,6 +49633,10 @@ components:
type: array
items:
$ref: '#/components/schemas/RedirectURIRequest'
backchannel_logout_uri:
type: string
title: Back-Channel Logout URI
format: uri
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -53304,6 +53413,16 @@ components:
activate_user_on_success:
type: boolean
description: Activate users upon completion of stage.
recovery_max_attempts:
type: integer
maximum: 2147483647
minimum: 0
recovery_cache_timeout:
type: string
minLength: 1
description: 'The time window used to count recent account recovery attempts.
If the number of attempts exceed recovery_max_attempts within this period,
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
PatchedEndpointDeviceRequest:
type: object
description: Serializer for Endpoint authenticator devices
@@ -53689,6 +53808,11 @@ components:
items:
type: string
format: uuid
children:
type: array
items:
type: string
format: uuid
PatchedGroupSAMLSourceConnectionRequest:
type: object
description: Group Source Connection
@@ -54432,6 +54556,10 @@ components:
type: array
items:
$ref: '#/components/schemas/RedirectURIRequest'
backchannel_logout_uri:
type: string
title: Back-Channel Logout URI
format: uri
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -61094,11 +61222,16 @@ components:
type: string
format: date-time
readOnly: true
last_updated:
type: string
format: date-time
readOnly: true
required:
- avatar
- date_joined
- groups_obj
- is_superuser
- last_updated
- name
- password_change_date
- pk

View File

@@ -15,6 +15,7 @@ export function addCommands(browser) {
/**
* @this {HTMLElement}
*/
// @ts-ignore
function () {
this.focus();
@@ -28,6 +29,7 @@ export function addCommands(browser) {
/**
* @this {HTMLElement}
*/
// @ts-ignore
function () {
this.blur();

2718
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -94,7 +94,7 @@
"@floating-ui/dom": "^1.7.3",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^7.0.0",
"@goauthentik/api": "^2025.6.4-1754241870",
"@goauthentik/api": "^2025.6.4-1754396177",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
@@ -128,16 +128,11 @@
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@wdio/browser-runner": "^9.18.4",
"@wdio/cli": "9.15",
"@wdio/spec-reporter": "^9.15.0",
"@web/test-runner": "^0.20.2",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chromedriver": "^138.0.5",
"codemirror": "^6.0.2",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.44.0",
@@ -194,7 +189,11 @@
"@esbuild/linux-x64": "^0.25.4",
"@rollup/rollup-darwin-arm64": "^4.46.2",
"@rollup/rollup-linux-arm64-gnu": "^4.46.2",
"@rollup/rollup-linux-x64-gnu": "^4.46.2"
"@rollup/rollup-linux-x64-gnu": "^4.46.2",
"@wdio/browser-runner": "^9.18.4",
"@wdio/cli": "^9.18.4",
"@wdio/spec-reporter": "^9.18.0",
"@web/test-runner": "^0.20.2"
},
"wireit": {
"build": {

View File

@@ -113,6 +113,19 @@ export const redirectUriHelp = html`${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
const backchannelLogoutUriHelpMessages = [
msg(
"URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.",
),
msg(
"These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.",
),
];
export const backchannelLogoutUriHelp = html`${backchannelLogoutUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
type ShowClientSecret = (show: boolean) => void;
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
@@ -174,6 +187,7 @@ export function renderForm(
>
</ak-hidden-text-input>
<ak-form-element-horizontal
flow-direction="row"
label=${msg("Redirect URIs/Origins (RegEx)")}
name="redirectUris"
>
@@ -192,6 +206,17 @@ export function renderForm(
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal
flow-direction="row"
label=${msg("Back-Channel Logout URI")}
>
<ak-text-input
name="backchannelLogoutUri"
value="${provider?.backchannelLogoutUri ?? ""}"
placeholder=${msg("URL")}
></ak-text-input>
${backchannelLogoutUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->

View File

@@ -4,6 +4,7 @@ import "#components/events/ObjectChangelog";
import "#elements/CodeMirror";
import "#elements/EmptyState";
import "#elements/Tabs";
import "#elements/tasks/TaskList";
import "#elements/ak-mdx/index";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/index";
@@ -19,6 +20,7 @@ import {
ClientTypeEnum,
CoreApi,
CoreUsersListRequest,
ModelEnum,
OAuth2Provider,
OAuth2ProviderSetupURLs,
PropertyMappingPreview,
@@ -170,6 +172,7 @@ export class OAuth2ProviderViewPage extends AKElement {
if (!this.provider) {
return html``;
}
const [appLabel, modelName] = ModelEnum.AuthentikProvidersOauth2Oauth2provider.split(".");
return html` ${this.provider?.assignedApplicationName
? html``
: html`<div slot="header" class="pf-c-banner pf-m-warning">
@@ -246,6 +249,18 @@ export class OAuth2ProviderViewPage extends AKElement {
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Back-Channel Logout URI")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text pf-m-monospace">
${this.provider.backchannelLogoutUri}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
@@ -355,6 +370,18 @@ export class OAuth2ProviderViewPage extends AKElement {
</form>
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
>
<div class="pf-c-card pf-l-grid__item pf-m-12-col-on-2xl">
<div class="pf-c-card__title">${msg("Tasks")}</div>
<ak-task-list
.relObjAppLabel=${appLabel}
.relObjModel=${modelName}
.relObjId="${this.provider.pk}"
></ak-task-list>
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
>

View File

@@ -232,6 +232,36 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
})}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Account Recovery Max Attempts")}
required
name="recoveryMaxAttempts"
>
<input
type="number"
value="${this.instance?.recoveryMaxAttempts ?? 5}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Account Recovery Cache Timeout")}
required
name="recoveryCacheTimeout"
>
<input
type="text"
value="${ifDefined(this.instance?.recoveryCacheTimeout || "minutes=5")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The time window used to count recent account recovery attempts.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
</div>
</ak-form-group>
${this.renderConnectionSettings()}`;

View File

@@ -35,7 +35,7 @@ export class AkToggleGroup extends CustomEmitterElement(AKElement) {
`,
];
/*
/**
* The value (causes highlighting, value is returned)
*
* @attr

View File

@@ -46,7 +46,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
/* The array of key/value pairs this pane is currently showing */
@property({ type: Array })
readonly options: DualSelectPair[] = [];
public readonly options?: DualSelectPair[];
/**
* A set (set being easy for lookups) of keys with all the pairs selected,
@@ -54,7 +54,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
* can be marked and their clicks ignored.
*/
@property({ type: Object })
readonly selected: Set<string> = new Set();
public readonly selected: Set<string> = new Set();
//#endregion
@@ -75,11 +75,17 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
//#region Refs
protected listRef = createRef<HTMLDivElement>();
#listRef = createRef<HTMLDivElement>();
#scrollAnimationFrame = -1;
#scrollIntoView = (): void => {
this.#listRef.value?.scrollTo(0, 0);
};
//#region Lifecycle
connectedCallback() {
public overrideconnectedCallback() {
super.connectedCallback();
for (const [attr, value] of hostAttributes) {
@@ -89,9 +95,11 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
}
}
protected updated(changed: PropertyValues<this>) {
if (changed.has("options")) {
this.listRef.value?.scrollTo(0, 0);
protected override updated(changed: PropertyValues<this>) {
if (changed.has("options") && this.options?.length) {
cancelAnimationFrame(this.#scrollAnimationFrame);
this.#scrollAnimationFrame = requestAnimationFrame(this.#scrollIntoView);
}
}
@@ -118,10 +126,9 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
this.toMove.add(key);
}
this.dispatchCustomEvent(
DualSelectEventType.MoveChanged,
Array.from(this.toMove.values()).sort(),
);
const moved = [...this.toMove].sort();
this.dispatchCustomEvent(DualSelectEventType.MoveChanged, moved);
this.dispatchCustomEvent(DualSelectEventType.Move);
@@ -145,7 +152,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
render() {
return html`
<div ${ref(this.listRef)} class="pf-c-dual-list-selector__menu">
<div ${ref(this.#listRef)} class="pf-c-dual-list-selector__menu">
<ul class="pf-c-dual-list-selector__list">
${map(this.options, ([key, label]) => {
const selected = classMap({

View File

@@ -100,9 +100,11 @@ export const globalVariables = css`
--pf-c-dual-list-selector__list-item-row--BackgroundColor: var(
--ak-dark-background-light-ish
);
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
--ak-dark-background-lighter;
--pf-c-dual-list-selector__list-item-row--focus-within--BackgroundColor: var(
--ak-dark-background-darker
);
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
--pf-global--BackgroundColor--400
);

View File

@@ -1,7 +1,7 @@
import { AKElement } from "#elements/Base";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
import { css, CSSResult, html, PropertyValues, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
@@ -20,15 +20,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
*/
@customElement("ak-form-group")
export class AKFormGroup extends AKElement {
@property({ type: Boolean, reflect: true })
public open = false;
@property({ type: String, reflect: true })
public label = msg("Details");
@property({ type: String, reflect: true })
public description?: string;
static styles: CSSResult[] = [
PFBase,
PFForm,
@@ -46,27 +37,6 @@ export class AKFormGroup extends AKElement {
}
details {
&::details-content {
height: 0;
overflow: clip;
transition-behavior: normal, allow-discrete;
transition-duration: var(--pf-global--TransitionDuration);
transition-timing-function: var(--pf-global--TimingFunction);
transition-property: height, content-visibility;
@media (prefers-reduced-motion) {
transition-duration: 0;
}
}
@supports (interpolate-size: allow-keywords) {
interpolate-size: allow-keywords;
&[open]::details-content {
height: auto;
}
}
&::details-content {
padding-inline-start: var(
--pf-c-form__field-group--GridTemplateColumns--toggle
@@ -102,12 +72,39 @@ export class AKFormGroup extends AKElement {
`,
];
formRef = createRef<HTMLFormElement>();
//region Properties
scrollAnimationFrame = -1;
@property({ type: Boolean, reflect: true })
public open = false;
scrollIntoView = (): void => {
this.formRef.value?.scrollIntoView({
@property({ type: String, reflect: true })
public label = msg("Details");
@property({ type: String, reflect: true })
public description?: string;
//#endregion
//#region Lifecycle
public override updated(changedProperties: PropertyValues<this>): void {
const previousOpen = changedProperties.get("open");
if (typeof previousOpen !== "boolean") return;
if (this.open && this.open !== previousOpen) {
cancelAnimationFrame(this.#scrollAnimationFrame);
this.#scrollAnimationFrame = requestAnimationFrame(this.#scrollIntoView);
}
}
#detailsRef = createRef<HTMLDetailsElement>();
#scrollAnimationFrame = -1;
#scrollIntoView = (): void => {
this.#detailsRef.value?.scrollIntoView({
behavior: "smooth",
});
};
@@ -117,19 +114,16 @@ export class AKFormGroup extends AKElement {
*/
public toggle = (event: Event): void => {
event.preventDefault();
cancelAnimationFrame(this.scrollAnimationFrame);
this.open = !this.open;
if (this.open) {
this.scrollAnimationFrame = requestAnimationFrame(this.scrollIntoView);
}
};
//#region Render
public render(): TemplateResult {
return html`
<details
${ref(this.formRef)}
${ref(this.#detailsRef)}
?open=${this.open}
?aria-expanded="${this.open}"
role="group"
@@ -167,6 +161,8 @@ export class AKFormGroup extends AKElement {
</details>
`;
}
//#endregion
}
declare global {

View File

@@ -59,6 +59,18 @@ export class HorizontalFormElement extends AKElement {
var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth)
var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
}
.pf-c-form__group-label {
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
}
.pf-c-form__label[aria-required] .pf-c-form__label-text::after {
content: "*";
user-select: none;
margin-left: var(--pf-c-form__label-required--MarginLeft);
font-size: var(--pf-c-form__label-required--FontSize);
color: var(--pf-c-form__label-required--Color);
}
`,
];
@@ -96,9 +108,11 @@ export class HorizontalFormElement extends AKElement {
@property({ type: String })
public name = "";
//#endregion
//#region Lifecycle
@property({
type: String,
attribute: "flow-direction",
})
public flowDirection: "row" | "column" = "column";
firstUpdated(): void {
this.updated();
@@ -124,7 +138,12 @@ export class HorizontalFormElement extends AKElement {
render(): TemplateResult {
this.updated();
return html`<div class="pf-c-form__group" role="group" aria-label="${this.label}">
return html`<div
class="pf-c-form__group"
role="group"
aria-label="${this.label}"
data-flow-direction="${this.flowDirection}"
>
<div class="pf-c-form__group-label">
<label
id="group-label"

View File

@@ -623,7 +623,7 @@ export abstract class Table<T extends object>
<ak-table-pagination
class="pf-c-toolbar__item pf-m-pagination"
.pages=${this.data?.pagination}
.pageChangeHandler=${handler}
.onPageChange=${handler}
>
</ak-table-pagination>
`;

View File

@@ -15,13 +15,13 @@ export type TablePageChangeListener = (page: number) => void;
@customElement("ak-table-pagination")
export class TablePagination extends AKElement {
@property({ type: String })
label?: string;
public label?: string;
@property({ attribute: false })
pages?: Pagination;
public pages?: Pagination;
@property({ attribute: false })
onPageChange?: TablePageChangeListener;
public onPageChange?: TablePageChangeListener;
static styles: CSSResult[] = [
PFBase,

View File

@@ -10008,6 +10008,12 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -7886,6 +7886,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -10056,6 +10056,12 @@ El valor de este campo se compara con el atributo de pertenencia del usuario.</t
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="fr" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s4caed5b7a7e5d89b">
@@ -596,9 +596,9 @@
</trans-unit>
<trans-unit id="saa0e2675da69651b">
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>L'URL "
<x id="0" equiv-text="${this.url}"/>" n'a pas été trouvée.</target>
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>L'URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot; n'a pas été trouvée.</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@@ -1525,7 +1525,7 @@
</trans-unit>
<trans-unit id="s33ed903c210a6209">
<source>Token to authenticate with. Currently only bearer authentication is supported.</source>
<target>Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge.</target>
<target>Jeton d'authentification à utiliser. Actuellement, seule l'authentification &quot;bearer authentication&quot; est prise en charge.</target>
</trans-unit>
<trans-unit id="sfc8bb104e2c05af8">
@@ -1693,8 +1693,8 @@
</trans-unit>
<trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test".</target>
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source>
<target>Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome &quot;fa-test&quot;.</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@@ -2752,7 +2752,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s33683c3b1dbaf264">
<source>To use SSL instead, use 'ldaps://' and disable this option.</source>
<target>Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option.</target>
<target>Pour utiliser SSL à la base, utilisez &quot;ldaps://&quot; et désactviez cette option.</target>
</trans-unit>
<trans-unit id="s2221fef80f4753a2">
@@ -3112,7 +3112,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s3198c384c2f68b08">
<source>Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually.</source>
<target>Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement.</target>
<target>Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID &quot;transient&quot; et que l'utilisateur ne se déconnecte pas manuellement.</target>
</trans-unit>
<trans-unit id="sb32e9c1faa0b8673">
@@ -3249,7 +3249,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s9f8aac89fe318acc">
<source>Optionally set the 'FriendlyName' value of the Assertion attribute.</source>
<target>Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel)</target>
<target>Indiquer la valeur &quot;FriendlyName&quot; de l'attribut d'assertion (optionnel)</target>
</trans-unit>
<trans-unit id="s851c108679653d2a">
@@ -3730,10 +3730,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source>
<target>Êtes-vous sûr de vouloir mettre à jour
<x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>"?</target>
<x id="0" equiv-text="${this.objectLabel}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@@ -4788,8 +4788,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sdf1d8edef27236f0">
<source>A "roaming" authenticator, like a YubiKey</source>
<target>Un authentificateur "itinérant", comme une YubiKey</target>
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source>
<target>Un authentificateur &quot;itinérant&quot;, comme une YubiKey</target>
</trans-unit>
<trans-unit id="sfffba7b23d8fb40c">
@@ -5094,7 +5094,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s5170f9ef331949c0">
<source>Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.</source>
<target>Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data".</target>
<target>Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable &quot;prompt_data&quot;.</target>
</trans-unit>
<trans-unit id="s36cb242ac90353bc">
@@ -5147,8 +5147,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<target>Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici.</target>
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<target>Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de &quot;rester connecté&quot;, ce qui prolongera sa session jusqu'à la durée spécifiée ici.</target>
</trans-unit>
<trans-unit id="s542a71bb8f41e057">
@@ -7104,7 +7104,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="sff0ac1ace2d90709">
<source>Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).</source>
<target>Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous).</target>
<target>Utilisez ce fournisseur avec l'option &quot;auth_request&quot; de Nginx ou &quot;forwardAuth&quot; de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, &quot;/outpost.goauthentik.io&quot; doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous).</target>
</trans-unit>
<trans-unit id="scb58b8a60cad8762">
<source>Default relay state</source>
@@ -7396,7 +7396,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<target>Utilisateur créé et ajouté au groupe <x id="0" equiv-text="${this.group.name}"/> avec succès</target>
</trans-unit>
<trans-unit id="s824e0943a7104668">
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<source>This user will be added to the group &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source>
<target>Cet utilisateur sera ajouté au groupe &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;.</target>
</trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca">
@@ -8658,7 +8658,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<target>Synchroniser le groupe</target>
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
<source><x id="0" equiv-text="${p.name}"/> (&quot;<x id="1" equiv-text="${p.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${p.type}"/>)</source>
<target><x id="0" equiv-text="${p.name}"/> (&amp;quot;<x id="1" equiv-text="${p.fieldKey}"/>&amp;quot;, de type <x id="2" equiv-text="${p.type}"/>)</target>
</trans-unit>
<trans-unit id="s25bacc19d98b444e">
@@ -8906,8 +8906,8 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<target>URLs de redirection autorisées après un flux d'autorisation réussi. Indiquez également toute origine ici pour les flux implicites.</target>
</trans-unit>
<trans-unit id="s4c49d27de60a532b">
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
<target>Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir.</target>
<source>To allow any redirect URI, set the mode to Regex and the value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur &quot;.*&quot;. Soyez conscient des possibles implications de sécurité que cela peut avoir.</target>
</trans-unit>
<trans-unit id="sa52bf79fe1ccb13e">
<source>Federated OIDC Sources</source>
@@ -9652,8 +9652,8 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<target>Comment effectuer l'authentification lors d'une demande de jeton pour le flux authorization_code</target>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
<target>Activer "Se souvenir de moi sur cet appareil"</target>
<source>Enable &quot;Remember me on this device&quot;</source>
<target>Activer &quot;Se souvenir de moi sur cet appareil&quot;</target>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
@@ -9801,7 +9801,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="se630f2ccd39bf9e6">
<source>If no group is selected and 'Send notification to event user' is disabled the rule is disabled. </source>
<target>Si aucun groupe n'est sélectionné et "Envoyer la notification à l'utilisateur associé à l'évènement" est désactivé, cette règle est désactivée.</target>
<target>Si aucun groupe n'est sélectionné et &quot;Envoyer la notification à l'utilisateur associé à l'évènement&quot; est désactivé, cette règle est désactivée.</target>
</trans-unit>
<trans-unit id="s47966b2a708694e2">
<source>Send notification to event user</source>
@@ -9809,7 +9809,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="sd30f00ff2135589c">
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
<target>Lorsque cette option est activée, une notification sera envoyée à l'utilisateur qui a déclenché l'événement en plus des utilisateurs du groupe ci-dessus. L'utilisateur associé à l'événement sera toujours le premier utilisateur. Pour envoyer une notification uniquement à l'utilisateur de l'événement, activez l'option "Envoyer une seule fois" dans le transport de notification.</target>
<target>Lorsque cette option est activée, une notification sera envoyée à l'utilisateur qui a déclenché l'événement en plus des utilisateurs du groupe ci-dessus. L'utilisateur associé à l'événement sera toujours le premier utilisateur. Pour envoyer une notification uniquement à l'utilisateur de l'événement, activez l'option &quot;Envoyer une seule fois&quot; dans le transport de notification.</target>
</trans-unit>
<trans-unit id="sbd65aeeb8a3b9bbc">
<source>Maximum registration attempts</source>
@@ -9869,7 +9869,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="sf1a3d030efd11f28">
<source>Open about dialog</source>
<target>Ouvrir la boîte de dialogue "À propos"</target>
<target>Ouvrir la boîte de dialogue &quot;À propos&quot;</target>
</trans-unit>
<trans-unit id="s95b96d7ead27527f">
<source>Product name</source>
@@ -9909,124 +9909,172 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="s334b3924d2bd5d55">
<source>Kerberos Source</source>
<target>Source Kerberos</target>
</trans-unit>
<trans-unit id="sbcdf61483337948a">
<source>Successfully updated schedule.</source>
<target>Planification mise à jour avec succès.</target>
</trans-unit>
<trans-unit id="sd372fe5b712f6b30">
<source>Crontab</source>
<target>Crontab</target>
</trans-unit>
<trans-unit id="s95bc8722b2708f8b">
<source>Paused</source>
<target>Mis en pause</target>
</trans-unit>
<trans-unit id="se41af830054194bc">
<source>Pause this schedule</source>
<target>Mettre cette planification en pause</target>
</trans-unit>
<trans-unit id="sd091e43d5e99dea4">
<source>Waiting to run</source>
<target>En attente de lancement</target>
</trans-unit>
<trans-unit id="s85994a70cd39166c">
<source>Running</source>
<target>En cours d'exécution</target>
</trans-unit>
<trans-unit id="sc7637275f670c938">
<source>Queue</source>
<target>File</target>
</trans-unit>
<trans-unit id="sb91d1be10eb2c7da">
<source>Last updated</source>
<target>Mis à jour pour la dernière fois</target>
</trans-unit>
<trans-unit id="s911c2c952e64b223">
<source>Show only standalone tasks</source>
<target>Afficher uniquement les tâches non liées</target>
</trans-unit>
<trans-unit id="s14053a4609676b8b">
<source>Exclude successful tasks</source>
<target>Exclure les tâches réussies</target>
</trans-unit>
<trans-unit id="s8ff646d0515aab2a">
<source>Retry task</source>
<target>Réessayer la tâche</target>
</trans-unit>
<trans-unit id="s9b8dccb514a0e34c">
<source>Schedule</source>
<target>Planification</target>
</trans-unit>
<trans-unit id="sf92789320708efed">
<source>Next run</source>
<target>Prochaine exécution</target>
</trans-unit>
<trans-unit id="s1aecc6c0d6cbf4ed">
<source>Last status</source>
<target>Dernier état</target>
</trans-unit>
<trans-unit id="s6089c283e28012fb">
<source>Show only standalone schedules</source>
<target>Afficher uniquement les tâches non liées</target>
</trans-unit>
<trans-unit id="s48006fb6e0b1860a">
<source>Run scheduled task now</source>
<target>Exécuter la tâche planifiée maintenant</target>
</trans-unit>
<trans-unit id="s6492593d534108e1">
<source>Update Schedule</source>
<target>Mettre à jour la planification</target>
</trans-unit>
<trans-unit id="sf2d616b20d62240d">
<source>Schedules</source>
<target>Planifications</target>
</trans-unit>
<trans-unit id="sc797fd9076cc136d">
<source>Tasks</source>
<target>Tâches</target>
</trans-unit>
<trans-unit id="s3f63421908094590">
<source>Current status</source>
<target>État actuel</target>
</trans-unit>
<trans-unit id="s4412853e1655ebb3">
<source>Sync is currently running.</source>
<target>La synchronisation est en cours d'exécution.</target>
</trans-unit>
<trans-unit id="s08661004803c26b0">
<source>Sync is not currently running.</source>
<target>La synchronisation n'est pas en cours d'exécution.</target>
</trans-unit>
<trans-unit id="s81d16aa9ec62262c">
<source>Last successful sync</source>
<target>Dernière synchronisation réussie</target>
</trans-unit>
<trans-unit id="sd1eb8f8acdbcd1d3">
<source>No successful sync found.</source>
<target>Pas de synchronisation réussie trouvée.</target>
</trans-unit>
<trans-unit id="s6b0eeb3de1789c6e">
<source>Last sync status</source>
<target>Dernier état de synchronisation</target>
</trans-unit>
<trans-unit id="sac44b0f4dc14c227">
<source>Current execution logs</source>
<target>Journaux d'exécution courant</target>
</trans-unit>
<trans-unit id="s5e13dff03b580216">
<source>Previous executions logs</source>
<target>Journaux d'exécution précédents</target>
</trans-unit>
<trans-unit id="s6abb1cd87fe0114e">
<source>Home</source>
<target>Accueil</target>
</trans-unit>
<trans-unit id="se58e6ed983bf34b0">
<source>Collapse navigation</source>
<target>Réduire la navigation</target>
</trans-unit>
<trans-unit id="sc6ef25894ed00175">
<source>Expand navigation</source>
<target>Développer la navigation</target>
</trans-unit>
<trans-unit id="s148b5e365440a7c1">
<source>Table pagination</source>
<target>Pagination du tableau</target>
</trans-unit>
<trans-unit id="s5d929ff1619ac0c9">
<source>Search</source>
<target>Rechercher</target>
</trans-unit>
<trans-unit id="sd2c2366d13599d8c">
<source>Table actions</source>
<target>Actions du tableau</target>
</trans-unit>
<trans-unit id="s3d195621e562d805">
<source>Select row</source>
<target>Sélectionner la ligne</target>
</trans-unit>
<trans-unit id="s572d21b6a41e24fa">
<source>Table of <x id="0" equiv-text="${this.label}"/></source>
<target>Tableau de <x id="0" equiv-text="${this.label}"/></target>
</trans-unit>
<trans-unit id="sa25b60b4fac481aa">
<source>Table content</source>
<target>Contenu du tableau</target>
</trans-unit>
<trans-unit id="s5eba8fa19126f70a">
<source>Learn more about the enterprise license.</source>
<target>En apprendre plus sur les licences entreprise.</target>
</trans-unit>
<trans-unit id="s9db1679f3b234d4e">
<source>Search for providers…</source>
<target>Rechercher des fournisseurs…</target>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
<target>Modifier le fournisseur</target>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
<target>Politique NameID par défaut</target>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
<target>Configure la politique NameID par défaut utilisée pour les connexions initiées par l'IDP et lorsqu'une assertion entrante ne spécifie pas de politique NameID (s'applique également lors de l'utilisation d'un mappage NameID personnalisé).</target>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View File

@@ -10012,6 +10012,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -9334,6 +9334,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -9238,6 +9238,12 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -9655,6 +9655,12 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -9664,4 +9664,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body></file></xliff>

View File

@@ -9746,6 +9746,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -9719,6 +9719,12 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -6511,6 +6511,12 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10013,6 +10013,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -7592,6 +7592,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -9314,6 +9314,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s76790480b7b28ad2">
<source>Edit provider</source>
</trans-unit>
<trans-unit id="s9839619155ed2cf6">
<source>Default NameID Policy</source>
</trans-unit>
<trans-unit id="s0c48c5f754275796">
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
</trans-unit>
</body>
</file>

View File

@@ -43,11 +43,20 @@ See https://docs.hcaptcha.com/switch
### Cloudflare Turnstile
See https://developers.cloudflare.com/turnstile/get-started/migrating-from-recaptcha
See https://developers.cloudflare.com/turnstile/get-started/migrating-from-recaptcha.
#### Configuration options
- Interactive: Enabled if the Turnstile instance is configured as visible or managed
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Flows and Stages** > **Stages** and click **Create**.
3. Select **Captcha Stage** and click **Next**.
4. Provide a descriptive name for the stage (e.g. `authentication-captcha`) and configure the following required settings based on the values of your [Cloudflare Turnstile Widget](https://developers.cloudflare.com/turnstile/concepts/widget/):
- Under **Stage-specific settings**:
- **Public Key**: set to the **Turnstile Site Key** value from the widget.
- **Private Key**: set to the **Turnstile Secret Key** value from the widget.
- **Enable Interactive**: Enable this option if the Turnstile instance is configured as **Invisible** or **Managed**.
- Leave both score thresholds at their default, as they are not supported for Turnstile.
- JS URL: `https://challenges.cloudflare.com/turnstile/v0/api.js`
- API URL: `https://challenges.cloudflare.com/turnstile/v0/siteverify`

View File

@@ -37,12 +37,13 @@ The following fields are currently sent for users:
- `ak-active`: "true" if the account is active, otherwise "false"
- `ak-superuser`: "true" if the account is part of a group with superuser permissions, otherwise "false"
The following fields are current set for groups:
The following fields are currently set for groups:
- `cn`: The group's name
- `uid`: Unique group identifier
- `gidNumber`: A unique numeric identifier for the group
- `member`: A list of all DNs of the groups members
- `member`: A list of all DNs of the group's members, including groups which have this group as their parent group
- `memberOf`: The DN of the parent group if this group has a parent group
- `objectClass`: A list of these strings:
- "group"
- "goauthentik.io/ldap/group"

View File

@@ -5,6 +5,6 @@ slug: /branding
You can configure several differently "branded" options depending on the associated domain, even though objects such as applications, providers, etc, are still global. This can be handy to use the same authentik instance, but branded differently for different domains.
The main settings that control your instance's appearance and behaviour are the _default flows_ and the _branding settings_.
The main settings that control your instance's appearance and behaviour are the [_branding settings_](../sys-mgmt/brands.md#branding-settings) and the the [_default flows_](../sys-mgmt/brands.md#default-flows). Review our tips for using images and icons in the [Image optimization](../sys-mgmt/brands.md#image-optimization) section.
To create or modify a brand, open the Admin interface and navigate to **System** > **Brands**. For complete instructions refer to our [Brands documentation](../sys-mgmt/brands.md).

View File

@@ -22,6 +22,8 @@ To simplify translation you can use https://www.transifex.com/authentik/authenti
- Make (again, any recent version should work)
- Docker
### Frontend
Run `npm i` in the `/web` folder to install all dependencies.
Ensure the language code is in the `lit-localize.json` file in `web/`:
@@ -42,3 +44,17 @@ Afterwards, run `make web-i18n-extract` to generate a base .xlf file.
The .xlf files can be edited by any text editor, or using a tool such as [POEdit](https://poedit.net/).
To see the change, run `make web-watch` in the root directory of the repository.
### Backend
Backend translations are handled by `core-i18n-extract`.
Use Django's translation utility to declare the string, e.g.:
```python
from django.utils.translation import gettext as _
_("New text to be translated.")
```
Afterwards, run `make core-i18n-extract` to generate the updated translation files.

View File

@@ -100,10 +100,7 @@ See [Configuration](../configuration/configuration.mdx) to change the internal p
## Startup
:::warning
The server assumes to have local timezone as UTC.
All internals are handled in UTC; whenever a time is displayed to the user in UI, the time shown is localized.
Do not update or mount `/etc/timezone` or `/etc/localtime` in the authentik containers.
This will not give any advantages. It will cause problems with OAuth and SAML authentication, e.g. [see this GitHub issue](https://github.com/goauthentik/authentik/issues/3005).
All internal operations use UTC. Times displayed in the UI are automatically localized for the user. Do not update or mount `/etc/timezone` or `/etc/localtime` in the authentik containers; it will cause problems with OAuth and SAML authentication, as seen this [GitHub issue](https://github.com/goauthentik/authentik/issues/3005).
:::
Afterward, run these commands to finish:

View File

@@ -50,9 +50,9 @@ Instead, the following metrics are now available:
- `authentik_tasks_errors_total`
- `authentik_tasks_retries_total`
- `authentik_tasks_rejected_total`
- `authentik_tasks_inprogress`
- `authentik_tasks_delayed_inprogress`
- `authentik_tasks_duration_miliseconds`
- `authentik_tasks_in_progress`
- `authentik_tasks_delayed_in_progress`
- `authentik_tasks_duration_milliseconds`
## New features

View File

@@ -551,7 +551,7 @@ const items = [
},
items: [
"users-sources/sources/social-logins/apple/index",
"users-sources/sources/social-logins/azure-ad/index",
"users-sources/sources/social-logins/entra-id/index",
"users-sources/sources/social-logins/discord/index",
"users-sources/sources/social-logins/facebook/index",
"users-sources/sources/social-logins/github/index",

View File

@@ -72,3 +72,23 @@ When using the [Mutual TLS Stage](../add-secure-apps/flows-stages/stages/mtls/in
#### Attributes
Attributes such as locale, theme settings (light/dark mode), and custom attributes can be set to a per-brand default value here. Any custom attributes can be retrieved via [`group_attributes()`](../users-sources/user/user_ref.mdx#object-properties).
## Image optimization
When you use images and icons for a brand's logo, favicon, etc., be aware of the following optimization tips:
- Use an SVG version of the image.
- Trim excess whitespace from around the logo. You can use an SVG editor such as Inkscape, Sketch, or Adobe Illustrator.
- Adjust the viewBox: Ensure the SVGs `viewBox` attribute tightly wraps the actual logo content. This helps in scaling the logo appropriately.
- Remove fixed dimensions: delete any fixed width and height attributes from the SVG. This allows the logo to scale responsively within its container.
- Check if your SVG needs `preserveAspectRatio` to retain its shape when resized.
- Wordmark logos: aim for an aspect ratio of approximately 7:1 (width to height).
- Icon logos: use a 1:1 aspect ratio, ensuring the icon fills the entire viewBox and is centered.
- The SVG tool [SVGOMG](https://svgomg.net/) is useful for trimming any excess metadata that might affect how the browser rasterizes the image.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -1,135 +0,0 @@
---
title: Azure AD
support_level: community
---
## Preparation
The following placeholders are used in this guide:
- `authentik.company` is the FQDN of the authentik install.
## Azure setup
1. Navigate to [portal.azure.com](https://portal.azure.com), and open the _App registration_ service
2. Register a new application
Under _Supported account types_, select whichever account type applies to your use-case.
![](./aad_01.png)
3. Take note of the _Application (client) ID_ value.
If you selected _Single tenant_ in the _Supported account types_ prompt, also note the _Directory (tenant) ID_ value.
4. Navigate to _Certificates & secrets_ in the sidebar, and to the _Client secrets_ tab.
5. Add a new secret, with an identifier of your choice, and select any expiration. Currently the secret in authentik has to be rotated manually or via API, so it is recommended to choose at least 12 months.
6. Note the secret's value in the _Value_ column.
## authentik Setup
In authentik, create a new _Azure AD OAuth Source_ in Resources -> Sources.
Use the following settings:
- Name: `Azure AD`
- Slug: `azure-ad` (this must match the URL being used above)
- Consumer key: `*Application (client) ID* value from above`
- Consumer secret: `*Value* of the secret from above`
If you kept the default _Supported account types_ selection of _Single tenant_, then you must change the URL below as well:
- OIDC Well-known URL: `https://login.microsoftonline.com/*Directory (tenant) ID* from above/v2.0/.well-known/openid-configuration`
![](./authentik_01.png)
Save, and you now have Azure AD as a source.
:::note
For more details on how-to have the new source display on the Login Page see [here](../../index.md#add-sources-to-default-login-page).
:::
### Automatic user enrollment and attribute mapping
Using the following process you can auto-enroll your users without interaction, and directly control the mapping Azure attribute to authentik.
attribute.
1. Create a new _Expression Policy_ (see [here](../../../../customize/policies/index.md) for details).
2. Use _azure-ad-mapping_ as the name.
3. Add the following code and adjust to your needs.
```python
# save existing prompt data
current_prompt_data = context.get('prompt_data', {})
# make sure we are used in an oauth flow
if 'oauth_userinfo' not in context:
ak_logger.warning(f"Missing expected oauth_userinfo in context. Context{context}")
return False
oauth_data = context['oauth_userinfo']
# map fields directly to user left hand are the field names provided by
# the microsoft graph api on the right the user field names as used by authentik
required_fields_map = {
'name': 'username',
'upn': 'email',
'given_name': 'name'
}
missing_fields = set(required_fields_map.keys()) - set(oauth_data.keys())
if missing_fields:
ak_logger.warning(f"Missing expected fields. Missing fields {missing_fields}.")
return False
for oauth_field, user_field in required_fields_map.items():
current_prompt_data[user_field] = oauth_data[oauth_field]
# Define fields that should be mapped as extra user attributes
attributes_map = {
'upn': 'upn',
'family_name': 'sn',
'name': 'name'
}
missing_attributes = set(attributes_map.keys()) - set(oauth_data.keys())
if missing_attributes:
ak_logger.warning(f"Missing attributes: {missing_attributes}.")
return False
# again make sure not to overwrite existing data
current_attributes = current_prompt_data.get('attributes', {})
for oauth_field, user_field in attributes_map.items():
current_attributes[user_field] = oauth_data[oauth_field]
current_prompt_data['attributes'] = current_attributes
context['prompt_data'] = current_prompt_data
return True
```
4. Create a new enrollment flow _azure-ad-enrollment_ (see [here](../../../../add-secure-apps/flows-stages/flow/index.md) for details).
5. Add the policy _default-source-enrollment-if-sso_ to the flow. To do so open the newly created flow.
Click on the tab **Policy/Group/User Bindings**. Click on **Bind existing policy** and choose _default-source-enrollment-if-sso_
from the list.
6. Bind the stages _default-source-enrollment-write_ (order 0) and _default-source-enrollment-login_ (order 10) to the flow.
7. Bind the policy _azure-ad-mapping_ to the stage _default-source-enrollment-write_. To do so open the flow _azure-ad-enrollment_
open the tab **Stage Bindings**, open the dropdown menu for the stage _default-source-enrollment-write_ and click on **Bind existing policy**
Select _azure-ad-mapping_.
8. Open the source _azure-ad_. Click on edit.
9. Open **Flow settings** and choose _azure-ad-enrollment_ as enrollment flow.
Try to login with a **_new_** user. You should see no prompts and the user should have the correct information.
### Machine-to-machine authentication:ak-version[2024.12]
If using [Machine-to-Machine](../../../../add-secure-apps/providers/oauth2/client_credentials.mdx#jwt-authentication) authentication, some specific steps need to be considered.
When getting the JWT token from Azure AD, set the scope to the Application ID URI, and _not_ the Graph URL; otherwise the JWT will be in an invalid format.
```http
POST /<azure-ad-tenant-id>/oauth2/v2.0/token/ HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=<application_client_id>&
scope=api://<application_client_id>/.default&
client_secret=<application_client_secret>
```
The JWT returned from the request above can be used with authentik to exchange it for an authentik JWT.
:::note
For instructions on how to display the new source on the authentik login page, refer to the [Add sources to default login page documentation](../../index.md#add-sources-to-default-login-page).
:::

View File

@@ -0,0 +1,94 @@
---
title: Entra ID
support_level: community
---
## Preparation
The following placeholders are used in this guide:
- `authentik.company` is the FQDN of the authentik install.
## Entra ID configuration
1. Log in to [Entra ID](https://entra.microsoft.com) using a [global administrator](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#global-administrator) account.
2. Navigate to **Applications** > **App registrations**.
3. Click **New registration** and set the following required configurations:
- **Name**: provide a descriptive name (e.g. `authentik`).
- Under **Supported account types**: select the account type that applies to your use-case (e.g. `Accounts in this organizational directory only (Default Directory only - Single tenant)`).
- Under **Redirect URI**:
- **Platform**: `Web`
- **URI**: `https://authentik.company/source/oauth/callback/entra-id/
4. Click **Register**. Once the registration is complete, the **Overview** tab of the newly created authentik app will open. Take note of the `Application (client) ID`. If you selected `Accounts in this organizational directory only (Default Directory only - Single tenant)` as the **Supported account types**, also note the `Directory (tenant) ID`. These values will be needed later when configuring authentik.
5. In the leftmost sidebar, navigate to **Certificates & secrets**.
6. Select the **Client secrets** tab and click **New Secret**. Configure the following required settings:
- **Description**: provide a description for the secret (e.g. `authentik secret`.
- **Expires**: choose an expiration period. As authentik does not yet support automatic secret rotation, either manual rotation or API-based updates are required. As a result, a duration of at least 12 months is recommended.
7. Copy the secret's value from the **Value** column.
:::note
The secret value is only displayed once at the time of creation. Make sure to copy and store it securely, as it cannot be retrieved later.
:::
8. In the sidebar, navigate to **API Permissions**, then click **Add a permission** and select **Microsoft Graph** as the API.
9. Select **Delegated permissions** as the permission type and assign the following permissions:
- Under **OpenID Permissions**: select `email`, `profile`, and `openid`.
- Under **Group Member** _(optional)_: if you need authentik to sync group membership information from Entra ID, select the `GroupMember.Read.All` permission.
10. Click **Add permissions**.
11. _(optional)_ If the `GroupMember.Read.All` permission has been selected, under **Configured permissions**, click **Grant admin consent for default directory**.
## authentik configuration
To support the integration of Entra ID with authentik, you need to create an Entra ID OAuth source in authentik.
### Create Entra ID OAuth source
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Directory** > **Federation and Social login**, click **Create**, and then configure the following settings:
- **Select type**: select **Entra ID OAuth Source** as the source type.
- **Create Entra ID OAuth Source**: provide a name, a slug which must match the slug used in the Entra ID `Redirect URI`, and the following required configurations:
- Under **Protocol Settings**:
- **Consumer key**: `Application (client) ID` from Entra ID.
- **Consumer secret**: value of the secret created in Entra ID.
- **Scopes**_(optional)_: if you need authentik to sync group membership information from Entra ID, add the `https://graph.microsoft.com/GroupMember.Read.All` scope.
- Under **URL Settings**:
- For **Single tenant** Entra ID applications:
- **Authorization URL**: `https://login.microsoftonline.com/<directory_(tenant)_id>/oauth2/v2.0/authorize`
- **Access token URL**: `https://login.microsoftonline.com/<directory_(tenant)_id>/oauth2/v2.0/token`
- **Profile URL**: `https://graph.microsoft.com/v1.0/me`
- **OIDC JWKS URL**: `https://login.microsoftonline.com/<directory_(tenant)_id>/discovery/v2.0/keys`
- For **Multi tenant** Entra ID applications:
- **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize`
- **Access token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token`
- **Profile URL**: `https://graph.microsoft.com/v1.0/me`
- **OIDC JWKS URL**: `https://login.microsoftonline.com/common/discovery/v2.0/keys`
3. Click **Save**.
:::note
When group membership information is synced from Entra ID, authentik creates all groups that a user is a member of.
:::
### Machine-to-machine authentication :ak-version[2024.12]
If using [Machine-to-Machine](../../../../add-secure-apps/providers/oauth2/client_credentials.mdx#jwt-authentication) authentication, some specific steps need to be considered.
When getting the JWT token from Entra ID, set the scope to the **Application ID URI**, and _not_ the Graph URL; otherwise the JWT will be in an invalid format.
```http
POST /<entra_tenant_id>/oauth2/v2.0/token/ HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=<application_client_id>&
scope=api://<application_client_id>/.default&
client_secret=<application_client_secret>
```
The JWT returned from the request above can be used in authentik and exchanged for an authentik JWT.
:::note
For instructions on how to display the new source on the authentik login page, refer to the [Add sources to default login page documentation](../../index.md#add-sources-to-default-login-page).
:::

View File

@@ -0,0 +1,109 @@
---
title: Integrate with Mattermost Team Edition
sidebar_label: Mattermost Team Edition
support_level: community
---
## What is Mattermost Team Edition
> Mattermost is an open source, real-time collaboration platform. It provides chat, audio/video calling, screen sharing, and a plugin architecture for extending its capabilities. Mattermost Team Edition is the free, open-source version of the product.
>
> -- https://mattermost.com/
:::info
Mattermost Team Edition does not natively support generic single sign-on. However, you can manually configure Mattermost to use its GitLab integration for authentication via authentiks OAuth2/OpenID Provider. This requires editing the `config.json` file directly, as the necessary settings are not available through the web interface. If you are using a hosted version of Mattermost without filesystem access, you will not be able to complete this setup. Once configured, Mattermost will display a login button with the GitLab icon, but authentication will be handled entirely by authentik. GitLab itself is not required or used in any way.
:::
## Preparation
The following placeholders are used in this guide:
- `mattermost.company` is the FQDN of the Mattermost installation.
- `authentik.company` is the FQDN of the authentik installation.
:::note
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
:::
## authentik configuration
To support the integration of Mattermost Team Edition with authentik, you need to create property mappings and an application/provider pair in authentik.
### Create property mappings
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Property Mappings** and click **Create**. Create a **Scope Mapping** with the following settings:
- **Name**: `mattermost-username`
- **Scope Name**: `username`
- **Description**: `Maps the user's authentik username to the username field for Mattermost authentication.`
- **Expression**:
```python
return {
"username": request.user.username,
}
```
:::note
The following `id` property mapping is optional. If omitted, Mattermost will generate user IDs based on email addresses, resulting in names such as `person-example.com` for `person@example.com`. Since these IDs serve as nicknames, this format may be undesirable.
:::
3. If desired, click **Create** again, and create another **Scope Mapping** with the following settings:
- **Name**: `mattermost-id`
- **Scope Name**: `id`
- **Description**: `Maps the user's Mattermost ID or primary key to the id field for Mattermost authentication.`
- **Expression**:
```python
return {
"id": request.user.attributes.get("mattermostId", request.user.pk),
}
```
### Create an application and provider in authentik
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. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
- **Application**: provide a descriptive name, an optional group for the type of application, and the policy engine mode.
- **Choose a Provider type**: select **OAuth2/OpenID Connect** as the provider type.
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
- Note the **Client ID**,**Client Secret**, and **slug** values because they will be required later.
- Set a `Strict` redirect URI to `https://mattermost.company/signup/gitlab/complete`.
- Select any available signing key.
- Under **Advanced protocol settings**, add the scopes you just created to the list of selected scopes.
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
3. Click **Submit** to save the new application and provider.
## Mattermost Team Edition configuration
To support the integration of Mattermost Team Edition with authentik, you'll need to update the `config.json` file of your Mattermost deployment:
1. Modify the `GitLabSettings` section to look like the following:
```json showLineNumbers title="/opt/mattermost/config/config.json"
"GitLabSettings": {
"Enable": true,
"Secret": "<client_secret>",
"Id": "<client_id>",
"Scope": "",
"AuthEndpoint": "https://authentik.company/application/o/authorize/",
"TokenEndpoint": "https://authentik.company/application/o/token/",
"UserAPIEndpoint": "https://authentik.company/application/o/userinfo/",
"DiscoveryEndpoint": "https://authentik.company/application/o/<application_slug>/.well-known/openid-configuration",
"ButtonText": "Log in with authentik",
"ButtonColor": "#000000"
},
```
2. Log in to Mattermost as an administrator and navigate to the System Console. Go to **Authentication** > **Signup** options (`https://mattermost.company/admin_console/authentication/signup`) and make sure that **Enable Account Creation** is set to **true**.
3. Restart Mattermost to apply the changes.
## Configuration verification
To verify the integration of authentik with Mattermost Team Edition, log out and attempt to log back in. You should see a button called "Log in with authentik" on the login page, and a successful login should redirect you back to Mattermost Team Edition without any errors.
## Additional Resources
- [Mattermost on Github](https://github.com/mattermost/mattermost)
- [Mattermost GitLab Authentication documentation](https://docs.mattermost.com/configure/authentication-configuration-settings.html#gitlab-oauth-2-0-settings)
- [Related blog post, in German, explaining this technique](https://ayedo.de/posts/mattermost-self-hosted-sso-mit-authentik/)

View File

@@ -95,12 +95,27 @@ Alternatively, it is possible to configure OpenID Connect via the UI.
</TabItem>
</Tabs>
:::warning
The first user to log into Actual Budget via OpenID will become the owner and administrator with the highest privileges for the budget. You should also note that users are not created automatically in Actual Budget. The owner must manually add users.
## User Provisioning
To do so, navigate to **Server online** > **User Directory**, and create users matching exiting authentik usernames. Then, grant access to the budget via the **User Access** tab.
:::warning
Users are not created automatically in Actual Budget and must be manually provisioned before OIDC authentication, otherwise you will receive a password error.
:::
To provision users:
1. Log in as a local (non-OIDC) admin.
2. Navigate to **Server online** > **User Directory**.
3. Create users matching existing authentik usernames.
4. Grant access to the budget via the **User Access** tab.
### Admin user configuration
For admin users with OIDC integration:
1. Provision an account with the `admin` role.
2. To configure this admin as an owner, transfer ownership via **User Access Management**.
3. You may need to reset the `admin` role afterward if it doesn't appear in the **User Directory**.
## Configuration verification
To confirm that authentik is properly configured with Actual Budget, visit your Actual Budget installation, select the OpenID login method from the dropdown menu, and click **Sign in with OpenID**.

View File

@@ -10,14 +10,8 @@ support_level: community
>
> -- https://www.home-assistant.io/
:::caution
You might run into CSRF errors, this is caused by a technology Home-assistant uses and not authentik, see [this GitHub issue](https://github.com/goauthentik/authentik/issues/884#issuecomment-851542477).
:::
:::caution
Only prefixes starting with `/auth` need to be proxied (excluding prefixes starting with `/auth/token`), see [this GitHub issue](https://github.com/BeryJu/hass-auth-header/issues/212). This can be configured in the reverse proxy (e.g. nginx, Traefik) or in authentik Provider's **Unauthorized Paths**.
:::
:::note
For Home Assistant to work with authentik, a custom integration needs to be installed for Home Assistant.
To integrate Home Assistant with authentik, a custom integration needs to be installed in Home Assistant.
:::
## Preparation
@@ -31,44 +25,133 @@ The following placeholders are used in this guide:
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
:::
## Configuration methods
It is possible to configure Home Assistant to use OIDC or a proxy provider for authentication. Below are the steps to configure each method.
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
<Tabs
defaultValue="oidc"
values={[
{ label: "OIDC", value: "oidc" },
{ label: "Proxy Provider", value: "proxy" }
]}
> <TabItem value="oidc">
## authentik configuration
1. Create a **Proxy Provider** under **Applications** > **Providers** using the following settings:
- **Name**: Home Assistant
- **Authentication flow**: default-authentication-flow
- **Authorization flow**: default-provider-authorization-explicit-consent
- **External Host**: Set this to the external URL you will be accessing Home Assistant from
- **Internal Host**: `http://hass.company:8123`
To support the integration of Home Assistant with authentik you need to create an application/provider pair in authentik.
2. Create an **Application** under **Applications** > **Applications** using the following settings:
- **Name**: Home Assistant
- **Slug**: homeassistant
- **Provider**: Home Assistant (the provider you created in step 1)
### Create an application and provider in authentik
3. Create an outpost deployment for the provider you've created above, as described [here](https://docs.goauthentik.io/docs/add-secure-apps/outposts/). Deploy this Outpost either on the same host or a different host that can access Home Assistant. The outpost will connect to authentik and configure itself.
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. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
- **Choose a Provider type**: select **OAuth2/OpenID** as the provider type.
- Note the **Client ID**,**Client Secret**, and **slug** values because they will be required later.
- **Signing Key**: Select any available signing key.
- **Redirect URIs**:
- Strict: `http://hass.company:8123/auth/openid/callback`
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
3. Click **Submit** to save the new application and provider.
## Home Assistant configuration
1. Install hass-openid following the instructions at https://github.com/cavefire/hass-openid
2. To support the integration of Home Assistant with authentik, you'll need to update the `configuration.yaml` file of your Home Assistant deployment:
```yaml showLineNumbers title="/config/configuration.yaml"
openid:
client_id: <authentik_client_ID>
client_secret: <authentik_client_secret>
configure_url: "https://authentik.company/application/o/<application_slug>/.well-known/openid-configuration"
scope: "openid profile email"
username_field: "preferred_username"
block_login: false
```
3. Restart Home Assistant
:::note
You must create OIDC users in Home Assistant before they can log in using OIDC.
:::
## Configuration verification
To verify the integration with Home Assistant, log out and attempt to log back in using the **OpenID/OAuth2 authentication** button. You should be redirected to the authentik login page. Once authenticated, you should be redirected to the Home Assistant dashboard.
</TabItem>
<TabItem value="proxy">
:::caution
Using a proxy provider might produce CSRF errors. This is caused by a technology that Home Assistant uses and not authentik. For more information see [this GitHub issue](https://github.com/goauthentik/authentik/issues/884#issuecomment-851542477).
:::
:::caution
Only prefixes starting with `/auth` need to be proxied (excluding prefixes starting with `/auth/token`). See [this GitHub issue](https://github.com/BeryJu/hass-auth-header/issues/212). This can be configured in the reverse proxy (e.g. nginx, Traefik) or in authentik Provider's **Unauthorized Paths**.
:::
## authentik configuration
To support the integration of Home Assistant using `hass-auth-headers` with authentik, you need to create an application/provider pair in authentik.
### Create an application and provider in authentik
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. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
- **Choose a Provider type**: select **Proxy** as the provider type.
- **External Host**: Set this to the external URL you will be accessing Home Assistant from.
- **Internal Host**: `http://hass.company:8123`
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
3. Click **Submit** to save the new application and provider.
4. Create an outpost deployment for the provider you've created above, as described [here](https://docs.goauthentik.io/docs/add-secure-apps/outposts/). Deploy this Outpost either on the same host or a different host that can access Home Assistant. The outpost will connect to authentik and configure itself.
## Home Assistant configuration
1. Configure [trusted_proxies](https://www.home-assistant.io/integrations/http/#trusted_proxies) for the HTTP integration with the IP(s) of the Host(s) authentik is running on.
2. If you don't already have it set up, https://github.com/BeryJu/hass-auth-header, using the installation guide.
3. There are two ways to configure the custom component.
1. To match on the user's authentik username, use the following configuration:
```yaml
auth_header:
username_header: X-authentik-username
```
2. Alternatively, you can associate an existing Home Assistant username to an authentik username.
1. Within authentik, navigate to **Directory** > **Users**.
2. Select **Edit** for the user then add the following configuration to the **Attributes** section. Be sure to replace `hassusername` with the Home Assistant username.
:::note
This configuration will add an additional header for the authentik user which will contain the Home Assistant username and allow Home Assistant to authenticate based on that.
:::
```yaml
additionalHeaders:
X-ak-hass-user: hassusername
```
3. Then configure the Home Assistant custom component to use this header:
```yaml
auth_header:
username_header: X-ak-hass-user
```
3. There are two ways to configure the custom component:
### Match on user's authentik username
To match on the user's authentik username, use the following configuration:
```yaml
auth_header:
username_header: X-authentik-username
```
### Associate existing Home Assistant username
Alternatively, you can associate an existing Home Assistant username to an authentik username.
1. Within authentik, navigate to **Directory** > **Users**.
2. Select **Edit** for the user then add the following configuration to the **Attributes** section. Be sure to replace `hassusername` with the Home Assistant username.
:::note
This configuration adds an extra header for the authentik user, containing the Home Assistant username, which allows Home Assistant to authenticate the user accordingly.
:::
```yaml
additionalHeaders:
X-ak-hass-user: hassusername
```
3. Then configure the Home Assistant custom component to use this header:
```yaml
auth_header:
username_header: X-ak-hass-user
```
</TabItem>
</Tabs>