diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py
index 76b02903f..f40aa3165 100644
--- a/authentik/core/api/applications.py
+++ b/authentik/core/api/applications.py
@@ -52,6 +52,9 @@ class ApplicationSerializer(ModelSerializer):
launch_url = SerializerMethodField()
provider_obj = ProviderSerializer(source="get_provider", required=False, read_only=True)
+ backchannel_providers_obj = ProviderSerializer(
+ source="backchannel_providers", required=False, read_only=True, many=True
+ )
meta_icon = ReadOnlyField(source="get_meta_icon")
@@ -75,6 +78,8 @@ class ApplicationSerializer(ModelSerializer):
"slug",
"provider",
"provider_obj",
+ "backchannel_providers",
+ "backchannel_providers_obj",
"launch_url",
"open_in_new_tab",
"meta_launch_url",
@@ -86,6 +91,7 @@ class ApplicationSerializer(ModelSerializer):
]
extra_kwargs = {
"meta_icon": {"read_only": True},
+ "backchannel_providers": {"required": False},
}
diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py
index 096e926f4..67b2427fc 100644
--- a/authentik/core/api/providers.py
+++ b/authentik/core/api/providers.py
@@ -1,5 +1,7 @@
"""Provider API Views"""
from django.utils.translation import gettext_lazy as _
+from django_filters.filters import BooleanFilter
+from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
@@ -20,6 +22,8 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
assigned_application_slug = ReadOnlyField(source="application.slug")
assigned_application_name = ReadOnlyField(source="application.name")
+ assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
+ assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
component = SerializerMethodField()
@@ -40,6 +44,8 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
"component",
"assigned_application_slug",
"assigned_application_name",
+ "assigned_backchannel_application_slug",
+ "assigned_backchannel_application_name",
"verbose_name",
"verbose_name_plural",
"meta_model_name",
@@ -49,6 +55,22 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
}
+class ProviderFilter(FilterSet):
+ """Filter for groups"""
+
+ application__isnull = BooleanFilter(
+ field_name="application",
+ lookup_expr="isnull",
+ )
+ backchannel_only = BooleanFilter(
+ method="filter_backchannel_only",
+ )
+
+ def filter_backchannel_only(self, queryset, name, value):
+ """Only return backchannel providers"""
+ return queryset.filter(is_backchannel=value)
+
+
class ProviderViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
@@ -60,9 +82,7 @@ class ProviderViewSet(
queryset = Provider.objects.none()
serializer_class = ProviderSerializer
- filterset_fields = {
- "application": ["isnull"],
- }
+ filterset_class = ProviderFilter
search_fields = [
"name",
"application__name",
@@ -78,6 +98,8 @@ class ProviderViewSet(
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: Provider
+ if subclass._meta.abstract:
+ continue
data.append(
{
"name": subclass._meta.verbose_name,
diff --git a/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py b/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py
new file mode 100644
index 000000000..1eb158813
--- /dev/null
+++ b/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py
@@ -0,0 +1,49 @@
+# Generated by Django 4.1.7 on 2023-04-30 17:56
+
+import django.db.models.deletion
+from django.apps.registry import Apps
+from django.db import DatabaseError, InternalError, ProgrammingError, migrations, models
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+
+def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
+ from authentik.core.models import BackchannelProvider
+
+ for model in BackchannelProvider.__subclasses__():
+ try:
+ for obj in model.objects.all():
+ obj.is_backchannel = True
+ obj.save()
+ except (DatabaseError, InternalError, ProgrammingError):
+ # The model might not have been migrated yet/doesn't exist yet
+ # so we don't need to worry about backporting the data
+ pass
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("authentik_core", "0028_provider_authentication_flow"),
+ ("authentik_providers_ldap", "0002_ldapprovider_bind_mode"),
+ ("authentik_providers_scim", "0006_rename_parent_group_scimprovider_filter_group"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="provider",
+ name="backchannel_application",
+ field=models.ForeignKey(
+ default=None,
+ help_text="Accessed from applications; optional backchannel providers for protocols like LDAP and SCIM.",
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="backchannel_providers",
+ to="authentik_core.application",
+ ),
+ ),
+ migrations.AddField(
+ model_name="provider",
+ name="is_backchannel",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.RunPython(backport_is_backchannel),
+ ]
diff --git a/authentik/core/models.py b/authentik/core/models.py
index b6505e5ea..3be7aefae 100644
--- a/authentik/core/models.py
+++ b/authentik/core/models.py
@@ -270,6 +270,20 @@ class Provider(SerializerModel):
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
+ backchannel_application = models.ForeignKey(
+ "Application",
+ default=None,
+ null=True,
+ on_delete=models.CASCADE,
+ help_text=_(
+ "Accessed from applications; optional backchannel providers for protocols "
+ "like LDAP and SCIM."
+ ),
+ related_name="backchannel_providers",
+ )
+
+ is_backchannel = models.BooleanField(default=False)
+
objects = InheritanceManager()
@property
@@ -292,6 +306,26 @@ class Provider(SerializerModel):
return str(self.name)
+class BackchannelProvider(Provider):
+ """Base class for providers that augment other providers, for example LDAP and SCIM.
+ Multiple of these providers can be configured per application, they may not use the application
+ slug in URLs as an application may have multiple instances of the same
+ type of Backchannel provider
+
+ They can use the application's policies and metadata"""
+
+ @property
+ def component(self) -> str:
+ raise NotImplementedError
+
+ @property
+ def serializer(self) -> type[Serializer]:
+ raise NotImplementedError
+
+ class Meta:
+ abstract = True
+
+
class Application(SerializerModel, PolicyBindingModel):
"""Every Application which uses authentik for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to
diff --git a/authentik/core/signals.py b/authentik/core/signals.py
index 4120855dc..ca6d2c527 100644
--- a/authentik/core/signals.py
+++ b/authentik/core/signals.py
@@ -6,11 +6,11 @@ from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
-from django.db.models.signals import post_save, pre_delete
+from django.db.models.signals import post_save, pre_delete, pre_save
from django.dispatch import receiver
from django.http.request import HttpRequest
-from authentik.core.models import Application, AuthenticatedSession
+from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider
# Arguments: user: User, password: str
password_changed = Signal()
@@ -54,3 +54,11 @@ def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSe
"""Delete session when authenticated session is deleted"""
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
+
+
+@receiver(pre_save)
+def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
+ """Ensure backchannel providers have is_backchannel set to true"""
+ if not isinstance(instance, BackchannelProvider):
+ return
+ instance.is_backchannel = True
diff --git a/authentik/core/tests/test_applications_api.py b/authentik/core/tests/test_applications_api.py
index 547f9b9a6..2bed87237 100644
--- a/authentik/core/tests/test_applications_api.py
+++ b/authentik/core/tests/test_applications_api.py
@@ -139,6 +139,8 @@ class TestApplicationsAPI(APITestCase):
"verbose_name": "OAuth2/OpenID Provider",
"verbose_name_plural": "OAuth2/OpenID Providers",
},
+ "backchannel_providers": [],
+ "backchannel_providers_obj": [],
"launch_url": f"https://goauthentik.io/{self.user.username}",
"meta_launch_url": "https://goauthentik.io/%(username)s",
"open_in_new_tab": True,
@@ -189,6 +191,8 @@ class TestApplicationsAPI(APITestCase):
"verbose_name": "OAuth2/OpenID Provider",
"verbose_name_plural": "OAuth2/OpenID Providers",
},
+ "backchannel_providers": [],
+ "backchannel_providers_obj": [],
"launch_url": f"https://goauthentik.io/{self.user.username}",
"meta_launch_url": "https://goauthentik.io/%(username)s",
"open_in_new_tab": True,
@@ -210,6 +214,8 @@ class TestApplicationsAPI(APITestCase):
"policy_engine_mode": "any",
"provider": None,
"provider_obj": None,
+ "backchannel_providers": [],
+ "backchannel_providers_obj": [],
"slug": "denied",
},
],
diff --git a/authentik/core/tests/test_models.py b/authentik/core/tests/test_models.py
index 1cf2cff36..a08dbb5af 100644
--- a/authentik/core/tests/test_models.py
+++ b/authentik/core/tests/test_models.py
@@ -53,9 +53,8 @@ def provider_tester_factory(test_model: type[Stage]) -> Callable:
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract: # pragma: no cover
- model_class = test_model.__bases__[0]()
- else:
- model_class = test_model()
+ return
+ model_class = test_model()
self.assertIsNotNone(model_class.component)
return tester
diff --git a/authentik/providers/ldap/api.py b/authentik/providers/ldap/api.py
index 64876b424..52c4cdc60 100644
--- a/authentik/providers/ldap/api.py
+++ b/authentik/providers/ldap/api.py
@@ -1,5 +1,5 @@
"""LDAPProvider API Views"""
-from rest_framework.fields import CharField, ListField
+from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@@ -54,9 +54,15 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
class LDAPOutpostConfigSerializer(ModelSerializer):
"""LDAPProvider Serializer"""
- application_slug = CharField(source="application.slug")
+ application_slug = SerializerMethodField()
bind_flow_slug = CharField(source="authorization_flow.slug")
+ def get_application_slug(self, instance: LDAPProvider) -> str:
+ """Prioritise backchannel slug over direct application slug"""
+ if instance.backchannel_application:
+ return instance.backchannel_application.slug
+ return instance.application.slug
+
class Meta:
model = LDAPProvider
fields = [
diff --git a/authentik/providers/ldap/models.py b/authentik/providers/ldap/models.py
index ad90777ec..581d50230 100644
--- a/authentik/providers/ldap/models.py
+++ b/authentik/providers/ldap/models.py
@@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
-from authentik.core.models import Group, Provider
+from authentik.core.models import BackchannelProvider, Group
from authentik.crypto.models import CertificateKeyPair
from authentik.outposts.models import OutpostModel
@@ -17,7 +17,7 @@ class APIAccessMode(models.TextChoices):
CACHED = "cached"
-class LDAPProvider(OutpostModel, Provider):
+class LDAPProvider(OutpostModel, BackchannelProvider):
"""Allow applications to authenticate against authentik's users using LDAP."""
base_dn = models.TextField(
diff --git a/authentik/providers/scim/models.py b/authentik/providers/scim/models.py
index 179044c65..96c4b55d4 100644
--- a/authentik/providers/scim/models.py
+++ b/authentik/providers/scim/models.py
@@ -5,10 +5,16 @@ from django.utils.translation import gettext_lazy as _
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import Serializer
-from authentik.core.models import USER_ATTRIBUTE_SA, Group, PropertyMapping, Provider, User
+from authentik.core.models import (
+ USER_ATTRIBUTE_SA,
+ BackchannelProvider,
+ Group,
+ PropertyMapping,
+ User,
+)
-class SCIMProvider(Provider):
+class SCIMProvider(BackchannelProvider):
"""SCIM 2.0 provider to create users and groups in external applications"""
exclude_users_service_account = models.BooleanField(default=False)
diff --git a/authentik/providers/scim/tasks.py b/authentik/providers/scim/tasks.py
index 6c8f66d75..0480730fb 100644
--- a/authentik/providers/scim/tasks.py
+++ b/authentik/providers/scim/tasks.py
@@ -35,7 +35,7 @@ def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
@CELERY_APP.task()
def scim_sync_all():
"""Run sync for all providers"""
- for provider in SCIMProvider.objects.all():
+ for provider in SCIMProvider.objects.all(backchannel_application__isnull=False):
scim_sync.delay(provider.pk)
@@ -96,6 +96,14 @@ def scim_sync_users(page: int, provider_pk: int):
)
except StopSync as exc:
LOGGER.warning("Stopping sync", exc=exc)
+ messages.append(
+ _(
+ "Stopping sync due to error: %(error)s"
+ % {
+ "error": str(exc),
+ }
+ )
+ )
break
return messages
@@ -129,6 +137,14 @@ def scim_sync_group(page: int, provider_pk: int):
)
except StopSync as exc:
LOGGER.warning("Stopping sync", exc=exc)
+ messages.append(
+ _(
+ "Stopping sync due to error: %(error)s"
+ % {
+ "error": str(exc),
+ }
+ )
+ )
break
return messages
@@ -141,7 +157,7 @@ def scim_signal_direct(model: str, pk: Any, raw_op: str):
if not instance:
return
operation = PatchOp(raw_op)
- for provider in SCIMProvider.objects.all():
+ for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
client = client_for_model(provider, instance)
# Check if the object is allowed within the provider's restrictions
queryset: Optional[QuerySet] = None
@@ -172,7 +188,7 @@ def scim_signal_m2m(group_pk: str, action: str, pk_set: list[int]):
group = Group.objects.filter(pk=group_pk).first()
if not group:
return
- for provider in SCIMProvider.objects.all():
+ for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
# Check if the object is allowed within the provider's restrictions
queryset: QuerySet = provider.get_group_qs()
# The queryset we get from the provider must include the instance we've got given
diff --git a/authentik/providers/scim/tests/test_client.py b/authentik/providers/scim/tests/test_client.py
index c7d862824..d6a523192 100644
--- a/authentik/providers/scim/tests/test_client.py
+++ b/authentik/providers/scim/tests/test_client.py
@@ -3,6 +3,7 @@ from django.test import TestCase
from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
+from authentik.core.models import Application
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
@@ -18,6 +19,11 @@ class SCIMClientTests(TestCase):
url="https://localhost",
token=generate_id(),
)
+ self.app: Application = Application.objects.create(
+ name=generate_id(),
+ slug=generate_id(),
+ )
+ self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
diff --git a/authentik/providers/scim/tests/test_group.py b/authentik/providers/scim/tests/test_group.py
index 503117bcd..6004a453b 100644
--- a/authentik/providers/scim/tests/test_group.py
+++ b/authentik/providers/scim/tests/test_group.py
@@ -7,7 +7,7 @@ from jsonschema import validate
from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
-from authentik.core.models import Group, User
+from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
@@ -26,6 +26,11 @@ class SCIMGroupTests(TestCase):
url="https://localhost",
token=generate_id(),
)
+ self.app: Application = Application.objects.create(
+ name=generate_id(),
+ slug=generate_id(),
+ )
+ self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.set(
[SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")]
)
diff --git a/authentik/providers/scim/tests/test_membership.py b/authentik/providers/scim/tests/test_membership.py
index de0844450..184b91892 100644
--- a/authentik/providers/scim/tests/test_membership.py
+++ b/authentik/providers/scim/tests/test_membership.py
@@ -4,7 +4,7 @@ from guardian.shortcuts import get_anonymous_user
from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
-from authentik.core.models import Group, User
+from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
@@ -15,6 +15,7 @@ class SCIMMembershipTests(TestCase):
"""SCIM Membership tests"""
provider: SCIMProvider
+ app: Application
def setUp(self) -> None:
# Delete all users and groups as the mocked HTTP responses only return one ID
@@ -30,6 +31,11 @@ class SCIMMembershipTests(TestCase):
url="https://localhost",
token=generate_id(),
)
+ self.app: Application = Application.objects.create(
+ name=generate_id(),
+ slug=generate_id(),
+ )
+ self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.set(
[SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")]
)
diff --git a/authentik/providers/scim/tests/test_user.py b/authentik/providers/scim/tests/test_user.py
index 36ce9b282..dae05f268 100644
--- a/authentik/providers/scim/tests/test_user.py
+++ b/authentik/providers/scim/tests/test_user.py
@@ -7,7 +7,7 @@ from jsonschema import validate
from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
-from authentik.core.models import Group, User
+from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync
@@ -28,6 +28,11 @@ class SCIMUserTests(TestCase):
token=generate_id(),
exclude_users_service_account=True,
)
+ self.app: Application = Application.objects.create(
+ name=generate_id(),
+ slug=generate_id(),
+ )
+ self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
diff --git a/authentik/root/test_runner.py b/authentik/root/test_runner.py
index 938c9d614..4f3adbdc8 100644
--- a/authentik/root/test_runner.py
+++ b/authentik/root/test_runner.py
@@ -1,5 +1,6 @@
"""Integrate ./manage.py test with pytest"""
from argparse import ArgumentParser
+from unittest import TestCase
from django.conf import settings
@@ -7,6 +8,8 @@ from authentik.lib.config import CONFIG
from authentik.lib.sentry import sentry_init
from tests.e2e.utils import get_docker_tag
+TestCase.maxDiff = None
+
class PytestTestRunner: # pragma: no cover
"""Runs pytest to discover and run tests."""
diff --git a/schema.yml b/schema.yml
index 233014767..d154dd68e 100644
--- a/schema.yml
+++ b/schema.yml
@@ -14293,6 +14293,10 @@ paths:
name: application__isnull
schema:
type: boolean
+ - in: query
+ name: backchannel_only
+ schema:
+ type: boolean
- name: ordering
required: false
in: query
@@ -15831,6 +15835,11 @@ paths:
schema:
type: string
format: uuid
+ - in: query
+ name: backchannel_application
+ schema:
+ type: string
+ format: uuid
- in: query
name: digest_algorithm
schema:
@@ -15850,6 +15859,10 @@ paths:
* `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256
* `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384
* `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512
+ - in: query
+ name: is_backchannel
+ schema:
+ type: boolean
- in: query
name: issuer
schema:
@@ -26466,6 +26479,15 @@ components:
allOf:
- $ref: '#/components/schemas/Provider'
readOnly: true
+ backchannel_providers:
+ type: array
+ items:
+ type: integer
+ backchannel_providers_obj:
+ type: array
+ items:
+ $ref: '#/components/schemas/Provider'
+ readOnly: true
launch_url:
type: string
nullable: true
@@ -26493,6 +26515,7 @@ components:
group:
type: string
required:
+ - backchannel_providers_obj
- launch_url
- meta_icon
- name
@@ -26516,6 +26539,10 @@ components:
provider:
type: integer
nullable: true
+ backchannel_providers:
+ type: array
+ items:
+ type: integer
open_in_new_tab:
type: boolean
description: Open launch URL in a new browser tab or window.
@@ -30550,6 +30577,8 @@ components:
type: string
application_slug:
type: string
+ description: Prioritise backchannel slug over direct application slug
+ readOnly: true
search_group:
type: string
format: uuid
@@ -30697,6 +30726,14 @@ components:
type: string
description: Application's display Name.
readOnly: true
+ assigned_backchannel_application_slug:
+ type: string
+ description: Internal application name, used in URLs.
+ readOnly: true
+ assigned_backchannel_application_name:
+ type: string
+ description: Application's display Name.
+ readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
@@ -30751,6 +30788,8 @@ components:
required:
- assigned_application_name
- assigned_application_slug
+ - assigned_backchannel_application_name
+ - assigned_backchannel_application_slug
- authorization_flow
- component
- meta_model_name
@@ -31441,6 +31480,14 @@ components:
type: string
description: Application's display Name.
readOnly: true
+ assigned_backchannel_application_slug:
+ type: string
+ description: Internal application name, used in URLs.
+ readOnly: true
+ assigned_backchannel_application_name:
+ type: string
+ description: Application's display Name.
+ readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
@@ -31522,6 +31569,8 @@ components:
required:
- assigned_application_name
- assigned_application_slug
+ - assigned_backchannel_application_name
+ - assigned_backchannel_application_slug
- authorization_flow
- component
- meta_model_name
@@ -35366,6 +35415,10 @@ components:
provider:
type: integer
nullable: true
+ backchannel_providers:
+ type: array
+ items:
+ type: integer
open_in_new_tab:
type: boolean
description: Open launch URL in a new browser tab or window.
@@ -38362,6 +38415,14 @@ components:
type: string
description: Application's display Name.
readOnly: true
+ assigned_backchannel_application_slug:
+ type: string
+ description: Internal application name, used in URLs.
+ readOnly: true
+ assigned_backchannel_application_name:
+ type: string
+ description: Application's display Name.
+ readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
@@ -38377,6 +38438,8 @@ components:
required:
- assigned_application_name
- assigned_application_slug
+ - assigned_backchannel_application_name
+ - assigned_backchannel_application_slug
- authorization_flow
- component
- meta_model_name
@@ -38594,6 +38657,14 @@ components:
type: string
description: Application's display Name.
readOnly: true
+ assigned_backchannel_application_slug:
+ type: string
+ description: Internal application name, used in URLs.
+ readOnly: true
+ assigned_backchannel_application_name:
+ type: string
+ description: Application's display Name.
+ readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
@@ -38683,6 +38754,8 @@ components:
required:
- assigned_application_name
- assigned_application_slug
+ - assigned_backchannel_application_name
+ - assigned_backchannel_application_slug
- authorization_flow
- client_id
- component
@@ -38850,6 +38923,14 @@ components:
type: string
description: Application's display Name.
readOnly: true
+ assigned_backchannel_application_slug:
+ type: string
+ description: Internal application name, used in URLs.
+ readOnly: true
+ assigned_backchannel_application_name:
+ type: string
+ description: Application's display Name.
+ readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
@@ -38873,6 +38954,8 @@ components:
required:
- assigned_application_name
- assigned_application_slug
+ - assigned_backchannel_application_name
+ - assigned_backchannel_application_slug
- authorization_flow
- component
- meta_model_name
@@ -39177,6 +39260,14 @@ components:
type: string
description: Application's display Name.
readOnly: true
+ assigned_backchannel_application_slug:
+ type: string
+ description: Internal application name, used in URLs.
+ readOnly: true
+ assigned_backchannel_application_name:
+ type: string
+ description: Application's display Name.
+ readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
@@ -39274,6 +39365,8 @@ components:
- acs_url
- assigned_application_name
- assigned_application_slug
+ - assigned_backchannel_application_name
+ - assigned_backchannel_application_slug
- authorization_flow
- component
- meta_model_name
diff --git a/web/src/admin/applications/ApplicationForm.ts b/web/src/admin/applications/ApplicationForm.ts
index 233c70d12..d8f6e8cc1 100644
--- a/web/src/admin/applications/ApplicationForm.ts
+++ b/web/src/admin/applications/ApplicationForm.ts
@@ -1,3 +1,4 @@
+import "@goauthentik/admin/applications/ProviderSelectModal";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
@@ -12,7 +13,7 @@ import "@goauthentik/elements/forms/SearchSelect";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
@@ -32,12 +33,16 @@ export class ApplicationForm extends ModelForm
+ ${t`Select backchannel providers which augment the functionality of the main provider.`} +
+