From eeb5cb08cd3fdb635a952de11375b2d7dd21996a Mon Sep 17 00:00:00 2001 From: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:03:38 +0300 Subject: [PATCH] sources: add Telegram source (#15749) * sources: add Telegram source (#2232) * sources/telegram: put telegram user info into policy context (#2232) * sources/telegram: replace regular input for bot token with a "secret" one (#2232) * sources/telegram: fix typo on Telegram source form * sources/telegram: added UserSourceConnection/GroupSourceConnection and SourceFlowManager subclasses for Telegram source * sources/telegram: improved code layout * sources/telegram: collapsed migrations * sources/telegram: fix lint errors * sources/telegram: fixed lint errors in docs * sources/telegram: fix app config * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * sources/telegram: add user source settings UI so that the users can disconnect Telegram source from their account * sources/telegram: clean up code per @risson's suggestions * sources/telegram: improve docs based on @tanberry's suggestions * sources/telegram: fix minor docs formatting issue * sources/teleram: add tests for views * sources/telegram: update serielizer field types references to be in line with convention * sources/telegram: add missing type annotations * sources/telegram: add check for source.enabled in the redirect view * sources/telegram: add pre-authentication flow to telegram source * sources: add Telegram source (#2232) * sources/telegram: added UserSourceConnection/GroupSourceConnection and SourceFlowManager subclasses for Telegram source * sources/telegram: collapsed migrations * sources/telegram: fix lint errors * sources/telegram: clean up code per @risson's suggestions * sources/teregram: fix merge errors * sources/telegram: improve docs wording * Standardized documentation * sources/telegram: added telegram source package to the list of ignored modules for mypy * sources/telegram: fix TS lint errors * sources/telegram: improve test coverage * web: bump @types/node from 22.15.19 to 24.5.2 in /web (#16989) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.19 to 24.5.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 24.5.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> Signed-off-by: dependabot[bot] Co-authored-by: Tana M Berry Co-authored-by: dewi-tik Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- authentik/root/settings.py | 1 + authentik/sources/telegram/__init__.py | 0 authentik/sources/telegram/api/__init__.py | 0 .../sources/telegram/api/property_mappings.py | 31 + authentik/sources/telegram/api/source.py | 41 + .../sources/telegram/api/source_connection.py | 33 + authentik/sources/telegram/apps.py | 9 + .../telegram/migrations/0001_initial.py | 118 ++ .../sources/telegram/migrations/__init__.py | 0 authentik/sources/telegram/models.py | 156 ++ authentik/sources/telegram/stage.py | 48 + authentik/sources/telegram/tests.py | 185 ++ authentik/sources/telegram/urls.py | 23 + authentik/sources/telegram/views.py | 98 + blueprints/schema.json | 480 +++++ pyproject.toml | 1 + schema.yml | 1662 +++++++++++++++++ web/authentik/sources/telegram.svg | 45 + .../PropertyMappingListPage.ts | 1 + .../PropertyMappingSourceTelegramForm.ts | 39 + .../PropertyMappingWizard.ts | 1 + web/src/admin/sources/SourceViewPage.ts | 5 + web/src/admin/sources/SourceWizard.ts | 1 + .../sources/telegram/TelegramSourceForm.ts | 263 +++ .../telegram/TelegramSourceFormHelpers.ts | 45 + .../telegram/TelegramSourceViewPage.ts | 160 ++ .../elements/user/sources/SourceSettings.ts | 8 + .../user/sources/SourceSettingsTelegram.ts | 73 + web/src/flow/FlowExecutor.ts | 6 + .../flow/sources/telegram/TelegramLogin.ts | 89 + website/docs/developer-docs/contributing.md | 9 +- website/docs/sidebar.mjs | 3 +- .../sources/social-logins/telegram/index.md | 60 + 33 files changed, 3689 insertions(+), 5 deletions(-) create mode 100644 authentik/sources/telegram/__init__.py create mode 100644 authentik/sources/telegram/api/__init__.py create mode 100644 authentik/sources/telegram/api/property_mappings.py create mode 100644 authentik/sources/telegram/api/source.py create mode 100644 authentik/sources/telegram/api/source_connection.py create mode 100644 authentik/sources/telegram/apps.py create mode 100644 authentik/sources/telegram/migrations/0001_initial.py create mode 100644 authentik/sources/telegram/migrations/__init__.py create mode 100644 authentik/sources/telegram/models.py create mode 100644 authentik/sources/telegram/stage.py create mode 100644 authentik/sources/telegram/tests.py create mode 100644 authentik/sources/telegram/urls.py create mode 100644 authentik/sources/telegram/views.py create mode 100644 web/authentik/sources/telegram.svg create mode 100644 web/src/admin/property-mappings/PropertyMappingSourceTelegramForm.ts create mode 100644 web/src/admin/sources/telegram/TelegramSourceForm.ts create mode 100644 web/src/admin/sources/telegram/TelegramSourceFormHelpers.ts create mode 100644 web/src/admin/sources/telegram/TelegramSourceViewPage.ts create mode 100644 web/src/elements/user/sources/SourceSettingsTelegram.ts create mode 100644 web/src/flow/sources/telegram/TelegramLogin.ts create mode 100644 website/docs/users-sources/sources/social-logins/telegram/index.md diff --git a/authentik/root/settings.py b/authentik/root/settings.py index f7a3e23231..8e7c79e5a5 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -103,6 +103,7 @@ TENANT_APPS = [ "authentik.sources.plex", "authentik.sources.saml", "authentik.sources.scim", + "authentik.sources.telegram", "authentik.stages.authenticator", "authentik.stages.authenticator_duo", "authentik.stages.authenticator_email", diff --git a/authentik/sources/telegram/__init__.py b/authentik/sources/telegram/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/sources/telegram/api/__init__.py b/authentik/sources/telegram/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/sources/telegram/api/property_mappings.py b/authentik/sources/telegram/api/property_mappings.py new file mode 100644 index 0000000000..3ab0d5876e --- /dev/null +++ b/authentik/sources/telegram/api/property_mappings.py @@ -0,0 +1,31 @@ +"""Telegram source property mappings API""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.sources.telegram.models import TelegramSourcePropertyMapping + + +class TelegramSourcePropertyMappingSerializer(PropertyMappingSerializer): + """TelegramSourcePropertyMapping Serializer""" + + class Meta(PropertyMappingSerializer.Meta): + model = TelegramSourcePropertyMapping + + +class TelegramSourcePropertyMappingFilter(PropertyMappingFilterSet): + """Filter for TelegramSourcePropertyMapping""" + + class Meta(PropertyMappingFilterSet.Meta): + model = TelegramSourcePropertyMapping + + +class TelegramSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet): + """TelegramSourcePropertyMapping Viewset""" + + queryset = TelegramSourcePropertyMapping.objects.all() + serializer_class = TelegramSourcePropertyMappingSerializer + filterset_class = TelegramSourcePropertyMappingFilter + search_fields = ["name"] + ordering = ["name"] diff --git a/authentik/sources/telegram/api/source.py b/authentik/sources/telegram/api/source.py new file mode 100644 index 0000000000..50b64e8993 --- /dev/null +++ b/authentik/sources/telegram/api/source.py @@ -0,0 +1,41 @@ +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.sources import SourceSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.sources.telegram.models import TelegramSource + + +class TelegramSourceSerializer(SourceSerializer): + class Meta: + model = TelegramSource + fields = SourceSerializer.Meta.fields + [ + "bot_username", + "bot_token", + "request_message_access", + "pre_authentication_flow", + ] + extra_kwargs = { + "bot_token": {"write_only": True}, + } + + +class TelegramSourceViewSet(UsedByMixin, ModelViewSet): + queryset = TelegramSource.objects.all() + serializer_class = TelegramSourceSerializer + lookup_field = "slug" + + filterset_fields = [ + "pbm_uuid", + "name", + "slug", + "enabled", + "authentication_flow", + "enrollment_flow", + "policy_engine_mode", + "user_matching_mode", + "group_matching_mode", + "bot_username", + "request_message_access", + ] + search_fields = ["name", "slug"] + ordering = ["name"] diff --git a/authentik/sources/telegram/api/source_connection.py b/authentik/sources/telegram/api/source_connection.py new file mode 100644 index 0000000000..3b95401b81 --- /dev/null +++ b/authentik/sources/telegram/api/source_connection.py @@ -0,0 +1,33 @@ +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.sources import ( + GroupSourceConnectionSerializer, + GroupSourceConnectionViewSet, + UserSourceConnectionSerializer, + UserSourceConnectionViewSet, +) +from authentik.sources.telegram.models import ( + GroupTelegramSourceConnection, + UserTelegramSourceConnection, +) + + +class UserTelegramSourceConnectionSerializer(UserSourceConnectionSerializer): + class Meta(UserSourceConnectionSerializer.Meta): + model = UserTelegramSourceConnection + fields = UserSourceConnectionSerializer.Meta.fields + + +class UserTelegramSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet): + queryset = UserTelegramSourceConnection.objects.all() + serializer_class = UserTelegramSourceConnectionSerializer + + +class GroupTelegramSourceConnectionSerializer(GroupSourceConnectionSerializer): + class Meta(GroupSourceConnectionSerializer.Meta): + model = GroupTelegramSourceConnection + + +class GroupTelegramSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet): + queryset = GroupTelegramSourceConnection.objects.all() + serializer_class = GroupTelegramSourceConnectionSerializer diff --git a/authentik/sources/telegram/apps.py b/authentik/sources/telegram/apps.py new file mode 100644 index 0000000000..234a8c53d1 --- /dev/null +++ b/authentik/sources/telegram/apps.py @@ -0,0 +1,9 @@ +from authentik.blueprints.apps import ManagedAppConfig + + +class TelegramConfig(ManagedAppConfig): + name = "authentik.sources.telegram" + label = "authentik_sources_telegram" + verbose_name = "authentik Sources.Telegram" + mountpoint = "source/telegram/" + default = True diff --git a/authentik/sources/telegram/migrations/0001_initial.py b/authentik/sources/telegram/migrations/0001_initial.py new file mode 100644 index 0000000000..3ddd96e199 --- /dev/null +++ b/authentik/sources/telegram/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# Generated by Django 5.1.12 on 2025-09-24 07:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("authentik_core", "0050_user_last_updated_and_more"), + ("authentik_flows", "0028_flowtoken_revoke_on_execution"), + ] + + operations = [ + migrations.CreateModel( + name="GroupTelegramSourceConnection", + fields=[ + ( + "groupsourceconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.groupsourceconnection", + ), + ), + ], + options={ + "verbose_name": "Group Telegram Source Connection", + "verbose_name_plural": "Group Telegram Source Connections", + }, + bases=("authentik_core.groupsourceconnection",), + ), + migrations.CreateModel( + name="TelegramSourcePropertyMapping", + fields=[ + ( + "propertymapping_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.propertymapping", + ), + ), + ], + options={ + "verbose_name": "Telegram Source Property Mapping", + "verbose_name_plural": "Telegram Source Property Mappings", + }, + bases=("authentik_core.propertymapping",), + ), + migrations.CreateModel( + name="UserTelegramSourceConnection", + fields=[ + ( + "usersourceconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.usersourceconnection", + ), + ), + ], + options={ + "verbose_name": "User Telegram Source Connection", + "verbose_name_plural": "User Telegram Source Connections", + }, + bases=("authentik_core.usersourceconnection",), + ), + migrations.CreateModel( + name="TelegramSource", + fields=[ + ( + "source_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.source", + ), + ), + ("bot_username", models.TextField(help_text="Telegram bot username")), + ("bot_token", models.TextField(help_text="Telegram bot token")), + ( + "request_message_access", + models.BooleanField( + default=False, help_text="Request access to send messages from your bot." + ), + ), + ( + "pre_authentication_flow", + models.ForeignKey( + help_text="Flow used before authentication.", + on_delete=django.db.models.deletion.CASCADE, + related_name="telegram_source_pre_authentication", + to="authentik_flows.flow", + ), + ), + ], + options={ + "verbose_name": "Telegram Source", + "verbose_name_plural": "Telegram Sources", + }, + bases=("authentik_core.source",), + ), + ] diff --git a/authentik/sources/telegram/migrations/__init__.py b/authentik/sources/telegram/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/sources/telegram/models.py b/authentik/sources/telegram/models.py new file mode 100644 index 0000000000..36063d185b --- /dev/null +++ b/authentik/sources/telegram/models.py @@ -0,0 +1,156 @@ +"""Telegram source""" + +from typing import Any + +from django.db import models +from django.http import HttpRequest +from django.templatetags.static import static +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import BaseSerializer, Serializer + +from authentik.core.models import ( + GroupSourceConnection, + PropertyMapping, + Source, + UserSourceConnection, +) +from authentik.core.types import UILoginButton, UserSettingSerializer +from authentik.flows.challenge import RedirectChallenge +from authentik.flows.models import Flow + + +class TelegramSource(Source): + """Log in with Telegram.""" + + bot_username = models.TextField(help_text=_("Telegram bot username")) + bot_token = models.TextField(help_text=_("Telegram bot token")) + + request_message_access = models.BooleanField( + default=False, help_text=_("Request access to send messages from your bot.") + ) + + pre_authentication_flow = models.ForeignKey( + Flow, + on_delete=models.CASCADE, + help_text=_("Flow used before authentication."), + related_name="telegram_source_pre_authentication", + ) + + @property + def component(self) -> str: + return "ak-source-telegram-form" + + @property + def icon_url(self) -> str | None: + icon = super().icon_url + if not icon: + icon = static("authentik/sources/telegram.svg") + return icon + + @property + def serializer(self) -> type[BaseSerializer]: + from authentik.sources.telegram.api.source import TelegramSourceSerializer + + return TelegramSourceSerializer + + def ui_login_button(self, request: HttpRequest) -> UILoginButton: + return UILoginButton( + challenge=RedirectChallenge( + data={ + "to": reverse( + "authentik_sources_telegram:start", + kwargs={"source_slug": self.slug}, + ), + } + ), + name=self.name, + icon_url=self.icon_url, + ) + + def ui_user_settings(self) -> UserSettingSerializer | None: + return UserSettingSerializer( + data={ + "title": self.name, + "component": "ak-user-settings-source-telegram", + "icon_url": self.icon_url, + } + ) + + @property + def property_mapping_type(self) -> "type[PropertyMapping]": + return TelegramSourcePropertyMapping + + def get_base_user_properties( + self, info: dict[str, Any] | None = None, **kwargs + ) -> dict[str, Any | dict[str, Any]]: + info = info or {} + name = info.get("first_name", "") + if "last_name" in info: + name += " " + info["last_name"] + return { + "username": info.get("username", None), + "email": None, + "name": name if name else None, + } + + def get_base_group_properties(self, group_id: str, **kwargs): + return { + "name": group_id, + } + + class Meta: + verbose_name = _("Telegram Source") + verbose_name_plural = _("Telegram Sources") + + +class TelegramSourcePropertyMapping(PropertyMapping): + """Map Telegram properties to User or Group object attributes""" + + @property + def component(self) -> str: + return "ak-property-mapping-source-telegram-form" + + @property + def serializer(self) -> type[Serializer]: + from authentik.sources.telegram.api.property_mappings import ( + TelegramSourcePropertyMappingSerializer, + ) + + return TelegramSourcePropertyMappingSerializer + + class Meta: + verbose_name = _("Telegram Source Property Mapping") + verbose_name_plural = _("Telegram Source Property Mappings") + + +class UserTelegramSourceConnection(UserSourceConnection): + """Connect user and Telegram source""" + + @property + def serializer(self) -> type[Serializer]: + from authentik.sources.telegram.api.source_connection import ( + UserTelegramSourceConnectionSerializer, + ) + + return UserTelegramSourceConnectionSerializer + + class Meta: + verbose_name = _("User Telegram Source Connection") + verbose_name_plural = _("User Telegram Source Connections") + + +class GroupTelegramSourceConnection(GroupSourceConnection): + """Group-source connection for Telegram""" + + @property + def serializer(self) -> type[Serializer]: + from authentik.sources.telegram.api.source_connection import ( + GroupTelegramSourceConnectionSerializer, + ) + + return GroupTelegramSourceConnectionSerializer + + class Meta: + verbose_name = _("Group Telegram Source Connection") + verbose_name_plural = _("Group Telegram Source Connections") diff --git a/authentik/sources/telegram/stage.py b/authentik/sources/telegram/stage.py new file mode 100644 index 0000000000..1a092e5a9f --- /dev/null +++ b/authentik/sources/telegram/stage.py @@ -0,0 +1,48 @@ +import hashlib +import hmac +from datetime import datetime, timedelta + +from django.utils.translation import gettext_lazy as _ +from rest_framework.fields import BooleanField, CharField, IntegerField, URLField +from rest_framework.serializers import ValidationError + +from authentik.flows.challenge import Challenge, ChallengeResponse +from authentik.stages.identification.stage import LoginChallengeMixin + + +class TelegramLoginChallenge(LoginChallengeMixin, Challenge): + component = CharField(default="ak-source-telegram") + bot_username = CharField(help_text=_("Telegram bot username")) + request_message_access = BooleanField() + + +class TelegramChallengeResponse(ChallengeResponse): + component = CharField(default="ak-source-telegram") + + id = IntegerField() + first_name = CharField(max_length=255, required=False) + last_name = CharField(max_length=255, required=False) + username = CharField(max_length=255, required=False) + photo_url = URLField(required=False) + auth_date = IntegerField(required=True) + hash = CharField(max_length=64, required=True) + + def validate_auth_date(self, auth_date: int) -> int: + if datetime.fromtimestamp(auth_date) < datetime.now() - timedelta(minutes=5): + raise ValidationError(_("Authentication date is too old")) + return auth_date + + def validate(self, attrs: dict) -> dict: + # Check the response as defined in https://core.telegram.org/widgets/login + attrs_to_check = attrs.copy() + attrs_to_check.pop("component") + attrs_to_check.pop("hash") + check_str = "\n".join([f"{key}={value}" for key, value in sorted(attrs_to_check.items())]) + digest = hmac.new( + hashlib.sha256(self.stage.source.bot_token.encode("utf-8")).digest(), + check_str.encode("utf-8"), + "sha256", + ).hexdigest() + if not hmac.compare_digest(digest, attrs["hash"]): + raise ValidationError(_("Invalid hash")) + return attrs diff --git a/authentik/sources/telegram/tests.py b/authentik/sources/telegram/tests.py new file mode 100644 index 0000000000..14b72071ec --- /dev/null +++ b/authentik/sources/telegram/tests.py @@ -0,0 +1,185 @@ +"""Telegram source tests""" + +import hashlib +import hmac +from datetime import datetime, timedelta +from unittest.mock import Mock + +from django.test import TestCase +from django.urls import reverse +from rest_framework.exceptions import ValidationError + +from authentik.core.tests.utils import create_test_flow +from authentik.flows.models import FlowDesignation, FlowStageBinding +from authentik.flows.tests import FlowTestCase +from authentik.sources.telegram.stage import TelegramChallengeResponse +from authentik.stages.identification.models import IdentificationStage, UserFields + + +class MockTelegramResponseMixin: + def _add_hash(self, response): + to_hash = "\n".join([f"{key}={value}" for key, value in sorted(response.items())]) + response["hash"] = hmac.new( + hashlib.sha256(self.source.bot_token.encode("utf-8")).digest(), + to_hash.encode("utf-8"), + "sha256", + ).hexdigest() + + def _make_valid_response(self): + resp = { + "id": "123456789", + "first_name": "Test", + "last_name": "User", + "username": "testuser", + "auth_date": str(int(datetime.now().timestamp())), + } + self._add_hash(resp) + return resp + + def _make_outdated_response(self): + resp = self._make_valid_response() + resp["auth_date"] = str(int((datetime.now() - timedelta(days=1)).timestamp())) + self._add_hash(resp) + return resp + + +class TestTelegramSource(MockTelegramResponseMixin, TestCase): + """Telegram Source tests""" + + def setUp(self): + from authentik.sources.telegram.models import TelegramSource + + self.source = TelegramSource.objects.create( + name="test", + slug="test", + bot_username="test_bot", + bot_token="modern_token", # nosec + request_message_access=True, + pre_authentication_flow=create_test_flow(), + ) + self.mock_stage = Mock() + self.mock_stage.source = self.source + + def test_ui_login_button(self): + """Test UI login button""" + ui_login_button = self.source.ui_login_button(None) + self.assertIsNotNone(ui_login_button) + self.assertEqual(ui_login_button.name, "test") + self.assertTrue(ui_login_button.challenge.is_valid(raise_exception=True)) + + def test_challenge_response(self): + """Test correct Telegram response validation""" + cr = TelegramChallengeResponse(data=self._make_valid_response()) + cr.stage = self.mock_stage + self.assertTrue(cr.is_valid(raise_exception=True)) + + def test_outdated_challenge_response(self): + """Test outdated Telegram response validation""" + cr = TelegramChallengeResponse(data=self._make_outdated_response()) + cr.stage = self.mock_stage + with self.assertRaises(ValidationError): + cr.is_valid(raise_exception=True) + + def test_invalid_hash_challenge_response(self): + """Test invalid hash in Telegram response validation""" + resp = self._make_valid_response() + resp["hash"] = "invalid_hash" + cr = TelegramChallengeResponse(data=resp) + cr.stage = self.mock_stage + with self.assertRaises(ValidationError): + cr.is_valid(raise_exception=True) + + def test_user_base_properties(self): + """Test user base properties""" + cr = TelegramChallengeResponse(data=self._make_valid_response()) + cr.stage = self.mock_stage + cr.is_valid(raise_exception=True) + properties = self.source.get_base_user_properties(info=cr.validated_data) + self.assertEqual( + properties, + { + "username": "testuser", + "name": "Test User", + "email": None, + }, + ) + + def test_group_base_properties(self): + """Test group base properties""" + for group_id in ["group 1", "group 2"]: + properties = self.source.get_base_group_properties(group_id=group_id) + self.assertEqual(properties, {"name": group_id}) + + +class TestTelegramViews(MockTelegramResponseMixin, FlowTestCase): + """Test Telegram source views""" + + def setUp(self): + super().setUp() + from authentik.sources.telegram.models import TelegramSource + + self.pre_auth_flow = create_test_flow() + + self.source = TelegramSource.objects.create( + name="test", + slug="test", + bot_username="test_bot", + bot_token="modern_token", # nosec + request_message_access=True, + enrollment_flow=create_test_flow(), + pre_authentication_flow=self.pre_auth_flow, + ) + + self.flow = create_test_flow(FlowDesignation.AUTHENTICATION) + self.stage = IdentificationStage.objects.create( + name="identification", + user_fields=[UserFields.E_MAIL], + pretend_user_exists=False, + ) + self.stage.sources.set([self.source]) + self.stage.save() + FlowStageBinding.objects.create( + target=self.flow, + stage=self.stage, + order=0, + ) + + def _make_initial_request(self): + return self.client.get( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + ) + + def _make_start_request(self): + return self.client.get( + reverse("authentik_sources_telegram:start", kwargs={"source_slug": self.source.slug}), + follow=True, + ) + + def test_start_view(self): + """Test TelegramStartView""" + self.assertEqual(self._make_initial_request().status_code, 200) + + response = self._make_start_request() + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.redirect_chain[0][0], + reverse("authentik_core:if-flow", kwargs={"flow_slug": self.pre_auth_flow.slug}), + ) + + def test_challenge_view(self): + """Test TelegramLoginView""" + self._make_initial_request() + self._make_start_request() + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.pre_auth_flow.slug}) + get_response = self.client.get(url) + self.assertEqual(get_response.status_code, 200) + form_data = self._make_valid_response() + form_data["component"] = "ak-source-telegram" + response = self.client.post(url, form_data) + self.assertEqual(response.status_code, 200) + self.assertStageRedirects( + response, + reverse( + "authentik_core:if-flow", kwargs={"flow_slug": self.source.enrollment_flow.slug} + ), + ) diff --git a/authentik/sources/telegram/urls.py b/authentik/sources/telegram/urls.py new file mode 100644 index 0000000000..4880093102 --- /dev/null +++ b/authentik/sources/telegram/urls.py @@ -0,0 +1,23 @@ +"""Telegram source API views""" + +from django.urls import path + +from authentik.sources.telegram.api.property_mappings import TelegramSourcePropertyMappingViewSet +from authentik.sources.telegram.api.source import TelegramSourceViewSet +from authentik.sources.telegram.api.source_connection import ( + GroupTelegramSourceConnectionViewSet, + UserTelegramSourceConnectionViewSet, +) +from authentik.sources.telegram.views import TelegramLoginView, TelegramStartView + +urlpatterns = [ + path("/start/", TelegramStartView.as_view(), name="start"), + path("/", TelegramLoginView.as_view(), name="login"), +] + +api_urlpatterns = [ + ("propertymappings/source/telegram", TelegramSourcePropertyMappingViewSet), + ("sources/user_connections/telegram", UserTelegramSourceConnectionViewSet), + ("sources/group_connections/telegram", GroupTelegramSourceConnectionViewSet), + ("sources/telegram", TelegramSourceViewSet), +] diff --git a/authentik/sources/telegram/views.py b/authentik/sources/telegram/views.py new file mode 100644 index 0000000000..34acec9c71 --- /dev/null +++ b/authentik/sources/telegram/views.py @@ -0,0 +1,98 @@ +from django.http import Http404, HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404 +from django.views import View + +from authentik.core.sources.flow_manager import SourceFlowManager +from authentik.flows.challenge import Challenge +from authentik.flows.exceptions import FlowNonApplicableException +from authentik.flows.models import in_memory_stage +from authentik.flows.planner import ( + PLAN_CONTEXT_REDIRECT, + PLAN_CONTEXT_SOURCE, + PLAN_CONTEXT_SSO, + FlowPlanner, +) +from authentik.flows.stage import ChallengeStageView +from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET +from authentik.sources.telegram.models import ( + GroupTelegramSourceConnection, + TelegramSource, + UserTelegramSourceConnection, +) +from authentik.sources.telegram.stage import TelegramChallengeResponse, TelegramLoginChallenge + + +class TelegramStartView(View): + def handle_login_flow( + self, source: TelegramSource, *stages_to_append, **kwargs + ) -> HttpResponse: + """Prepare Authentication Plan, redirect user FlowExecutor""" + # Ensure redirect is carried through when user was trying to + # authorize application + final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( + NEXT_ARG_NAME, "authentik_core:if-user" + ) + kwargs.update( + { + PLAN_CONTEXT_SSO: True, + PLAN_CONTEXT_SOURCE: source, + PLAN_CONTEXT_REDIRECT: final_redirect, + } + ) + # We run the Flow planner here so we can pass the Pending user in the context + planner = FlowPlanner(source.pre_authentication_flow) + planner.allow_empty_flows = True + try: + plan = planner.plan(self.request, kwargs) + except FlowNonApplicableException: + raise Http404 from None + for stage in stages_to_append: + plan.append_stage(stage) + return plan.to_redirect(self.request, source.pre_authentication_flow) + + def get(self, request: HttpRequest, source_slug: str) -> HttpResponse: + source = get_object_or_404(TelegramSource, slug=source_slug, enabled=True) + telegram_login_stage = in_memory_stage(TelegramLoginView) + + return self.handle_login_flow(source, telegram_login_stage) + + +class TelegramSourceFlowManager(SourceFlowManager): + """Flow manager for Telegram source""" + + user_connection_type = UserTelegramSourceConnection + group_connection_type = GroupTelegramSourceConnection + + +class TelegramLoginView(ChallengeStageView): + + response_class = TelegramChallengeResponse + + def dispatch(self, request, *args, **kwargs): + self.source = self.executor.plan.context[PLAN_CONTEXT_SOURCE] + return super().dispatch(request, *args, **kwargs) + + def get_challenge(self, *args, **kwargs) -> Challenge: + return TelegramLoginChallenge( + data={ + "bot_username": self.source.bot_username, + "request_message_access": self.source.request_message_access, + }, + ) + + def challenge_valid(self, response: TelegramChallengeResponse) -> HttpResponse: + raw_info = response.validated_data.copy() + raw_info.pop("component") + raw_info.pop("hash") + raw_info.pop("auth_date") + source = self.source + sfm = TelegramSourceFlowManager( + source=source, + request=self.request, + identifier=raw_info["id"], + user_info={"info": raw_info}, + policy_context={"telegram": raw_info}, + ) + return sfm.get_flow( + raw_info=raw_info, + ) diff --git a/blueprints/schema.json b/blueprints/schema.json index 3098c41f41..54d15d9305 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -3016,6 +3016,166 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_sources_telegram.grouptelegramsourceconnection" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "created", + "must_created", + "present" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/$defs/model_authentik_sources_telegram.grouptelegramsourceconnection_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_sources_telegram.grouptelegramsourceconnection" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_sources_telegram.grouptelegramsourceconnection" + } + } + }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_sources_telegram.telegramsource" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "created", + "must_created", + "present" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/$defs/model_authentik_sources_telegram.telegramsource_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_sources_telegram.telegramsource" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_sources_telegram.telegramsource" + } + } + }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_sources_telegram.telegramsourcepropertymapping" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "created", + "must_created", + "present" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/$defs/model_authentik_sources_telegram.telegramsourcepropertymapping_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_sources_telegram.telegramsourcepropertymapping" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_sources_telegram.telegramsourcepropertymapping" + } + } + }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_sources_telegram.usertelegramsourceconnection" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "created", + "must_created", + "present" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/$defs/model_authentik_sources_telegram.usertelegramsourceconnection_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_sources_telegram.usertelegramsourceconnection" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_sources_telegram.usertelegramsourceconnection" + } + } + }, { "type": "object", "required": [ @@ -5271,6 +5431,22 @@ "authentik_sources_scim.view_scimsourcegroup", "authentik_sources_scim.view_scimsourcepropertymapping", "authentik_sources_scim.view_scimsourceuser", + "authentik_sources_telegram.add_grouptelegramsourceconnection", + "authentik_sources_telegram.add_telegramsource", + "authentik_sources_telegram.add_telegramsourcepropertymapping", + "authentik_sources_telegram.add_usertelegramsourceconnection", + "authentik_sources_telegram.change_grouptelegramsourceconnection", + "authentik_sources_telegram.change_telegramsource", + "authentik_sources_telegram.change_telegramsourcepropertymapping", + "authentik_sources_telegram.change_usertelegramsourceconnection", + "authentik_sources_telegram.delete_grouptelegramsourceconnection", + "authentik_sources_telegram.delete_telegramsource", + "authentik_sources_telegram.delete_telegramsourcepropertymapping", + "authentik_sources_telegram.delete_usertelegramsourceconnection", + "authentik_sources_telegram.view_grouptelegramsourceconnection", + "authentik_sources_telegram.view_telegramsource", + "authentik_sources_telegram.view_telegramsourcepropertymapping", + "authentik_sources_telegram.view_usertelegramsourceconnection", "authentik_stages_authenticator_duo.add_authenticatorduostage", "authentik_stages_authenticator_duo.add_duodevice", "authentik_stages_authenticator_duo.change_authenticatorduostage", @@ -7336,6 +7512,7 @@ "authentik.sources.plex", "authentik.sources.saml", "authentik.sources.scim", + "authentik.sources.telegram", "authentik.stages.authenticator", "authentik.stages.authenticator_duo", "authentik.stages.authenticator_email", @@ -7446,6 +7623,10 @@ "authentik_sources_saml.groupsamlsourceconnection", "authentik_sources_scim.scimsource", "authentik_sources_scim.scimsourcepropertymapping", + "authentik_sources_telegram.telegramsource", + "authentik_sources_telegram.telegramsourcepropertymapping", + "authentik_sources_telegram.usertelegramsourceconnection", + "authentik_sources_telegram.grouptelegramsourceconnection", "authentik_stages_authenticator_duo.authenticatorduostage", "authentik_stages_authenticator_duo.duodevice", "authentik_stages_authenticator_email.authenticatoremailstage", @@ -9976,6 +10157,22 @@ "authentik_sources_scim.view_scimsourcegroup", "authentik_sources_scim.view_scimsourcepropertymapping", "authentik_sources_scim.view_scimsourceuser", + "authentik_sources_telegram.add_grouptelegramsourceconnection", + "authentik_sources_telegram.add_telegramsource", + "authentik_sources_telegram.add_telegramsourcepropertymapping", + "authentik_sources_telegram.add_usertelegramsourceconnection", + "authentik_sources_telegram.change_grouptelegramsourceconnection", + "authentik_sources_telegram.change_telegramsource", + "authentik_sources_telegram.change_telegramsourcepropertymapping", + "authentik_sources_telegram.change_usertelegramsourceconnection", + "authentik_sources_telegram.delete_grouptelegramsourceconnection", + "authentik_sources_telegram.delete_telegramsource", + "authentik_sources_telegram.delete_telegramsourcepropertymapping", + "authentik_sources_telegram.delete_usertelegramsourceconnection", + "authentik_sources_telegram.view_grouptelegramsourceconnection", + "authentik_sources_telegram.view_telegramsource", + "authentik_sources_telegram.view_telegramsourcepropertymapping", + "authentik_sources_telegram.view_usertelegramsourceconnection", "authentik_stages_authenticator_duo.add_authenticatorduostage", "authentik_stages_authenticator_duo.add_duodevice", "authentik_stages_authenticator_duo.change_authenticatorduostage", @@ -12068,6 +12265,289 @@ } } }, + "model_authentik_sources_telegram.grouptelegramsourceconnection": { + "type": "object", + "properties": { + "group": { + "type": "string", + "format": "uuid", + "title": "Group" + }, + "source": { + "type": "integer", + "title": "Source" + }, + "identifier": { + "type": "string", + "minLength": 1, + "title": "Identifier" + }, + "icon": { + "type": "string", + "minLength": 1, + "title": "Icon" + } + }, + "required": [] + }, + "model_authentik_sources_telegram.grouptelegramsourceconnection_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_grouptelegramsourceconnection", + "change_grouptelegramsourceconnection", + "delete_grouptelegramsourceconnection", + "view_grouptelegramsourceconnection" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, + "model_authentik_sources_telegram.telegramsource": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name", + "description": "Source's display Name." + }, + "slug": { + "type": "string", + "maxLength": 50, + "minLength": 1, + "pattern": "^[-a-zA-Z0-9_]+$", + "title": "Slug", + "description": "Internal source name, used in URLs." + }, + "enabled": { + "type": "boolean", + "title": "Enabled" + }, + "authentication_flow": { + "type": "string", + "format": "uuid", + "title": "Authentication flow", + "description": "Flow to use when authenticating existing users." + }, + "enrollment_flow": { + "type": "string", + "format": "uuid", + "title": "Enrollment flow", + "description": "Flow to use when enrolling new users." + }, + "user_property_mappings": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "title": "User property mappings" + }, + "group_property_mappings": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "title": "Group property mappings" + }, + "policy_engine_mode": { + "type": "string", + "enum": [ + "all", + "any" + ], + "title": "Policy engine mode" + }, + "user_matching_mode": { + "type": "string", + "enum": [ + "identifier", + "email_link", + "email_deny", + "username_link", + "username_deny" + ], + "title": "User matching mode", + "description": "How the source determines if an existing user should be authenticated or a new user enrolled." + }, + "user_path_template": { + "type": "string", + "minLength": 1, + "title": "User path template" + }, + "icon": { + "type": "string", + "minLength": 1, + "title": "Icon" + }, + "bot_username": { + "type": "string", + "minLength": 1, + "title": "Bot username", + "description": "Telegram bot username" + }, + "bot_token": { + "type": "string", + "minLength": 1, + "title": "Bot token", + "description": "Telegram bot token" + }, + "request_message_access": { + "type": "boolean", + "title": "Request message access", + "description": "Request access to send messages from your bot." + }, + "pre_authentication_flow": { + "type": "string", + "format": "uuid", + "title": "Pre authentication flow", + "description": "Flow used before authentication." + } + }, + "required": [] + }, + "model_authentik_sources_telegram.telegramsource_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_telegramsource", + "change_telegramsource", + "delete_telegramsource", + "view_telegramsource" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, + "model_authentik_sources_telegram.telegramsourcepropertymapping": { + "type": "object", + "properties": { + "managed": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "title": "Managed by authentik", + "description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update." + }, + "name": { + "type": "string", + "minLength": 1, + "title": "Name" + }, + "expression": { + "type": "string", + "minLength": 1, + "title": "Expression" + } + }, + "required": [] + }, + "model_authentik_sources_telegram.telegramsourcepropertymapping_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_telegramsourcepropertymapping", + "change_telegramsourcepropertymapping", + "delete_telegramsourcepropertymapping", + "view_telegramsourcepropertymapping" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, + "model_authentik_sources_telegram.usertelegramsourceconnection": { + "type": "object", + "properties": { + "user": { + "type": "integer", + "title": "User" + }, + "source": { + "type": "integer", + "title": "Source" + }, + "identifier": { + "type": "string", + "minLength": 1, + "title": "Identifier" + }, + "icon": { + "type": "string", + "minLength": 1, + "title": "Icon" + } + }, + "required": [] + }, + "model_authentik_sources_telegram.usertelegramsourceconnection_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_usertelegramsourceconnection", + "change_usertelegramsourceconnection", + "delete_usertelegramsourceconnection", + "view_usertelegramsourceconnection" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, "model_authentik_stages_authenticator_duo.authenticatorduostage": { "type": "object", "properties": { diff --git a/pyproject.toml b/pyproject.toml index 680819b92b..5a7ef6f17e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -236,6 +236,7 @@ module = [ "authentik.sources.plex.*", "authentik.sources.saml.*", "authentik.sources.scim.*", + "authentik.sources.telegram.*", "authentik.stages.authenticator_duo.*", "authentik.stages.authenticator_email.*", "authentik.stages.authenticator_sms.*", diff --git a/schema.yml b/schema.yml index 1aeb2f07f6..ea17c0a418 100644 --- a/schema.yml +++ b/schema.yml @@ -17646,6 +17646,267 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /propertymappings/source/telegram/: + get: + operationId: propertymappings_source_telegram_list + description: TelegramSourcePropertyMapping Viewset + parameters: + - in: query + name: managed + schema: + type: array + items: + type: string + explode: true + style: form + - in: query + name: managed__isnull + schema: + type: boolean + - in: query + name: name + schema: + type: string + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - $ref: '#/components/parameters/QuerySearch' + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedTelegramSourcePropertyMappingList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: propertymappings_source_telegram_create + description: TelegramSourcePropertyMapping Viewset + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourcePropertyMappingRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourcePropertyMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /propertymappings/source/telegram/{pm_uuid}/: + get: + operationId: propertymappings_source_telegram_retrieve + description: TelegramSourcePropertyMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Telegram Source Property Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourcePropertyMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: propertymappings_source_telegram_update + description: TelegramSourcePropertyMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Telegram Source Property Mapping. + required: true + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourcePropertyMappingRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourcePropertyMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: propertymappings_source_telegram_partial_update + description: TelegramSourcePropertyMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Telegram Source Property Mapping. + required: true + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedTelegramSourcePropertyMappingRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourcePropertyMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: propertymappings_source_telegram_destroy + description: TelegramSourcePropertyMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Telegram Source Property Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /propertymappings/source/telegram/{pm_uuid}/used_by/: + get: + operationId: propertymappings_source_telegram_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Telegram Source Property Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /providers/all/: get: operationId: providers_all_list @@ -22992,6 +23253,10 @@ paths: - authentik_sources_saml.usersamlsourceconnection - authentik_sources_scim.scimsource - authentik_sources_scim.scimsourcepropertymapping + - authentik_sources_telegram.grouptelegramsourceconnection + - authentik_sources_telegram.telegramsource + - authentik_sources_telegram.telegramsourcepropertymapping + - authentik_sources_telegram.usertelegramsourceconnection - authentik_stages_authenticator_duo.authenticatorduostage - authentik_stages_authenticator_duo.duodevice - authentik_stages_authenticator_email.authenticatoremailstage @@ -23221,6 +23486,10 @@ paths: - authentik_sources_saml.usersamlsourceconnection - authentik_sources_scim.scimsource - authentik_sources_scim.scimsourcepropertymapping + - authentik_sources_telegram.grouptelegramsourceconnection + - authentik_sources_telegram.telegramsource + - authentik_sources_telegram.telegramsourcepropertymapping + - authentik_sources_telegram.usertelegramsourceconnection - authentik_stages_authenticator_duo.authenticatorduostage - authentik_stages_authenticator_duo.duodevice - authentik_stages_authenticator_email.authenticatoremailstage @@ -25889,6 +26158,260 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /sources/group_connections/telegram/: + get: + operationId: sources_group_connections_telegram_list + description: Group-source connection Viewset + parameters: + - in: query + name: group + schema: + type: string + format: uuid + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - $ref: '#/components/parameters/QuerySearch' + - in: query + name: source__slug + schema: + type: string + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedGroupTelegramSourceConnectionList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: sources_group_connections_telegram_create + description: Group-source connection Viewset + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupTelegramSourceConnectionRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/GroupTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /sources/group_connections/telegram/{id}/: + get: + operationId: sources_group_connections_telegram_retrieve + description: Group-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Group Telegram Source + Connection. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/GroupTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: sources_group_connections_telegram_update + description: Group-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Group Telegram Source + Connection. + required: true + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupTelegramSourceConnectionRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/GroupTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: sources_group_connections_telegram_partial_update + description: Group-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Group Telegram Source + Connection. + required: true + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedGroupTelegramSourceConnectionRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/GroupTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: sources_group_connections_telegram_destroy + description: Group-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Group Telegram Source + Connection. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /sources/group_connections/telegram/{id}/used_by/: + get: + operationId: sources_group_connections_telegram_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Group Telegram Source + Connection. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /sources/kerberos/: get: operationId: sources_kerberos_list @@ -28548,6 +29071,318 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /sources/telegram/: + get: + operationId: sources_telegram_list + description: Mixin to add a used_by endpoint to return a list of all objects + using this object + parameters: + - in: query + name: authentication_flow + schema: + type: string + format: uuid + - in: query + name: bot_username + schema: + type: string + - in: query + name: enabled + schema: + type: boolean + - in: query + name: enrollment_flow + schema: + type: string + format: uuid + - in: query + name: group_matching_mode + schema: + type: string + enum: + - identifier + - name_deny + - name_link + description: |+ + How the source determines if an existing group should be used or a new group created. + + - in: query + name: name + schema: + type: string + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - in: query + name: pbm_uuid + schema: + type: string + format: uuid + - in: query + name: policy_engine_mode + schema: + type: string + enum: + - all + - any + - in: query + name: request_message_access + schema: + type: boolean + - $ref: '#/components/parameters/QuerySearch' + - in: query + name: slug + schema: + type: string + - in: query + name: user_matching_mode + schema: + type: string + enum: + - email_deny + - email_link + - identifier + - username_deny + - username_link + description: |+ + How the source determines if an existing user should be authenticated or a new user enrolled. + + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedTelegramSourceList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: sources_telegram_create + description: Mixin to add a used_by endpoint to return a list of all objects + using this object + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourceRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSource' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /sources/telegram/{slug}/: + get: + operationId: sources_telegram_retrieve + description: Mixin to add a used_by endpoint to return a list of all objects + using this object + parameters: + - in: path + name: slug + schema: + type: string + description: Internal source name, used in URLs. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSource' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: sources_telegram_update + description: Mixin to add a used_by endpoint to return a list of all objects + using this object + parameters: + - in: path + name: slug + schema: + type: string + description: Internal source name, used in URLs. + required: true + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSourceRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSource' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: sources_telegram_partial_update + description: Mixin to add a used_by endpoint to return a list of all objects + using this object + parameters: + - in: path + name: slug + schema: + type: string + description: Internal source name, used in URLs. + required: true + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedTelegramSourceRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSource' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: sources_telegram_destroy + description: Mixin to add a used_by endpoint to return a list of all objects + using this object + parameters: + - in: path + name: slug + schema: + type: string + description: Internal source name, used in URLs. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /sources/telegram/{slug}/used_by/: + get: + operationId: sources_telegram_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: slug + schema: + type: string + description: Internal source name, used in URLs. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /sources/user_connections/all/: get: operationId: sources_user_connections_all_list @@ -30009,6 +30844,259 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /sources/user_connections/telegram/: + get: + operationId: sources_user_connections_telegram_list + description: User-source connection Viewset + parameters: + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - $ref: '#/components/parameters/QuerySearch' + - in: query + name: source__slug + schema: + type: string + - in: query + name: user + schema: + type: integer + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedUserTelegramSourceConnectionList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: sources_user_connections_telegram_create + description: User-source connection Viewset + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserTelegramSourceConnectionRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/UserTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /sources/user_connections/telegram/{id}/: + get: + operationId: sources_user_connections_telegram_retrieve + description: User-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this User Telegram Source + Connection. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: sources_user_connections_telegram_update + description: User-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this User Telegram Source + Connection. + required: true + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserTelegramSourceConnectionRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: sources_user_connections_telegram_partial_update + description: User-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this User Telegram Source + Connection. + required: true + tags: + - sources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedUserTelegramSourceConnectionRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserTelegramSourceConnection' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: sources_user_connections_telegram_destroy + description: User-source connection Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this User Telegram Source + Connection. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /sources/user_connections/telegram/{id}/used_by/: + get: + operationId: sources_user_connections_telegram_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this User Telegram Source + Connection. + required: true + tags: + - sources + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /ssf/streams/: get: operationId: ssf_streams_list @@ -38552,6 +39640,7 @@ components: - authentik.sources.plex - authentik.sources.saml - authentik.sources.scim + - authentik.sources.telegram - authentik.stages.authenticator - authentik.stages.authenticator_duo - authentik.stages.authenticator_email @@ -40652,6 +41741,7 @@ components: - $ref: '#/components/schemas/RedirectChallenge' - $ref: '#/components/schemas/SessionEndChallenge' - $ref: '#/components/schemas/ShellChallenge' + - $ref: '#/components/schemas/TelegramLoginChallenge' - $ref: '#/components/schemas/UserLoginChallenge' discriminator: propertyName: component @@ -40681,6 +41771,7 @@ components: xak-flow-redirect: '#/components/schemas/RedirectChallenge' ak-stage-session-end: '#/components/schemas/SessionEndChallenge' xak-flow-shell: '#/components/schemas/ShellChallenge' + ak-source-telegram: '#/components/schemas/TelegramLoginChallenge' ak-stage-user-login: '#/components/schemas/UserLoginChallenge' ClientTypeEnum: enum: @@ -42695,6 +43786,7 @@ components: - $ref: '#/components/schemas/PlexAuthenticationChallengeResponseRequest' - $ref: '#/components/schemas/PromptChallengeResponseRequest' - $ref: '#/components/schemas/RedirectChallengeResponseRequest' + - $ref: '#/components/schemas/TelegramChallengeResponseRequest' - $ref: '#/components/schemas/UserLoginChallengeResponseRequest' discriminator: propertyName: component @@ -42720,6 +43812,7 @@ components: ak-source-plex: '#/components/schemas/PlexAuthenticationChallengeResponseRequest' ak-stage-prompt: '#/components/schemas/PromptChallengeResponseRequest' xak-flow-redirect: '#/components/schemas/RedirectChallengeResponseRequest' + ak-source-telegram: '#/components/schemas/TelegramChallengeResponseRequest' ak-stage-user-login: '#/components/schemas/UserLoginChallengeResponseRequest' FlowDesignationEnum: enum: @@ -43987,6 +45080,59 @@ components: - group - identifier - source + GroupTelegramSourceConnection: + type: object + description: Group Source Connection + properties: + pk: + type: integer + readOnly: true + title: ID + group: + type: string + format: uuid + source: + type: string + format: uuid + source_obj: + allOf: + - $ref: '#/components/schemas/Source' + readOnly: true + identifier: + type: string + created: + type: string + format: date-time + readOnly: true + last_updated: + type: string + format: date-time + readOnly: true + required: + - created + - group + - identifier + - last_updated + - pk + - source + - source_obj + GroupTelegramSourceConnectionRequest: + type: object + description: Group Source Connection + properties: + group: + type: string + format: uuid + source: + type: string + format: uuid + identifier: + type: string + minLength: 1 + required: + - group + - identifier + - source IdentificationChallenge: type: object description: Identification challenges with all UI elements @@ -45633,12 +46779,14 @@ components: - $ref: '#/components/schemas/RedirectChallenge' - $ref: '#/components/schemas/AppleLoginChallenge' - $ref: '#/components/schemas/PlexAuthenticationChallenge' + - $ref: '#/components/schemas/TelegramLoginChallenge' discriminator: propertyName: component mapping: xak-flow-redirect: '#/components/schemas/RedirectChallenge' ak-source-oauth-apple: '#/components/schemas/AppleLoginChallenge' ak-source-plex: '#/components/schemas/PlexAuthenticationChallenge' + ak-source-telegram: '#/components/schemas/TelegramLoginChallenge' LoginSource: type: object description: Serializer for Login buttons of sources @@ -46015,6 +47163,10 @@ components: - authentik_sources_saml.groupsamlsourceconnection - authentik_sources_scim.scimsource - authentik_sources_scim.scimsourcepropertymapping + - authentik_sources_telegram.telegramsource + - authentik_sources_telegram.telegramsourcepropertymapping + - authentik_sources_telegram.usertelegramsourceconnection + - authentik_sources_telegram.grouptelegramsourceconnection - authentik_stages_authenticator_duo.authenticatorduostage - authentik_stages_authenticator_duo.duodevice - authentik_stages_authenticator_email.authenticatoremailstage @@ -47969,6 +49121,21 @@ components: - pagination - results - autocomplete + PaginatedGroupTelegramSourceConnectionList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/GroupTelegramSourceConnection' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - pagination + - results + - autocomplete PaginatedIdentificationStageList: type: object properties: @@ -49064,6 +50231,36 @@ components: - pagination - results - autocomplete + PaginatedTelegramSourceList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/TelegramSource' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - pagination + - results + - autocomplete + PaginatedTelegramSourcePropertyMappingList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/TelegramSourcePropertyMapping' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - pagination + - results + - autocomplete PaginatedTenantList: type: object properties: @@ -49304,6 +50501,21 @@ components: - pagination - results - autocomplete + PaginatedUserTelegramSourceConnectionList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/UserTelegramSourceConnection' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - pagination + - results + - autocomplete PaginatedUserWriteStageList: type: object properties: @@ -50892,6 +52104,19 @@ components: identifier: type: string minLength: 1 + PatchedGroupTelegramSourceConnectionRequest: + type: object + description: Group Source Connection + properties: + group: + type: string + format: uuid + source: + type: string + format: uuid + identifier: + type: string + minLength: 1 PatchedIdentificationStageRequest: type: object description: IdentificationStage Serializer @@ -52948,6 +54173,87 @@ components: minLength: 1 description: The human-readable name of this device. maxLength: 64 + PatchedTelegramSourcePropertyMappingRequest: + type: object + description: TelegramSourcePropertyMapping Serializer + properties: + managed: + type: string + nullable: true + minLength: 1 + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This flag only indicates that an object can + be overwritten by migrations. You can still modify the objects via the + API, but expect changes to be overwritten in a later update. + name: + type: string + minLength: 1 + expression: + type: string + minLength: 1 + PatchedTelegramSourceRequest: + type: object + description: Source Serializer + properties: + name: + type: string + minLength: 1 + description: Source's display Name. + slug: + type: string + minLength: 1 + description: Internal source name, used in URLs. + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + enabled: + type: boolean + authentication_flow: + type: string + format: uuid + nullable: true + description: Flow to use when authenticating existing users. + enrollment_flow: + type: string + format: uuid + nullable: true + description: Flow to use when enrolling new users. + user_property_mappings: + type: array + items: + type: string + format: uuid + group_property_mappings: + type: array + items: + type: string + format: uuid + policy_engine_mode: + $ref: '#/components/schemas/PolicyEngineMode' + user_matching_mode: + allOf: + - $ref: '#/components/schemas/UserMatchingModeEnum' + description: How the source determines if an existing user should be authenticated + or a new user enrolled. + user_path_template: + type: string + minLength: 1 + bot_username: + type: string + minLength: 1 + description: Telegram bot username + bot_token: + type: string + writeOnly: true + minLength: 1 + description: Telegram bot token + request_message_access: + type: boolean + description: Request access to send messages from your bot. + pre_authentication_flow: + type: string + format: uuid + description: Flow used before authentication. PatchedTenantRequest: type: object description: Tenant Serializer @@ -53195,6 +54501,18 @@ components: identifier: type: string minLength: 1 + PatchedUserTelegramSourceConnectionRequest: + type: object + description: User source connection + properties: + user: + type: integer + source: + type: string + format: uuid + identifier: + type: string + minLength: 1 PatchedUserWriteStageRequest: type: object description: UserWriteStage Serializer @@ -57907,6 +59225,299 @@ components: - warning - error type: string + TelegramChallengeResponseRequest: + type: object + description: Base class for all challenge responses + properties: + component: + type: string + minLength: 1 + default: ak-source-telegram + id: + type: integer + first_name: + type: string + minLength: 1 + maxLength: 255 + last_name: + type: string + minLength: 1 + maxLength: 255 + username: + type: string + minLength: 1 + maxLength: 255 + photo_url: + type: string + format: uri + minLength: 1 + auth_date: + type: integer + hash: + type: string + minLength: 1 + maxLength: 64 + required: + - auth_date + - hash + - id + TelegramLoginChallenge: + type: object + description: Base login challenge for Identification stage + properties: + flow_info: + $ref: '#/components/schemas/ContextualFlowInfo' + component: + type: string + default: ak-source-telegram + response_errors: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/ErrorDetail' + bot_username: + type: string + description: Telegram bot username + request_message_access: + type: boolean + required: + - bot_username + - request_message_access + TelegramSource: + type: object + description: Source Serializer + properties: + pk: + type: string + format: uuid + readOnly: true + title: Pbm uuid + name: + type: string + description: Source's display Name. + slug: + type: string + description: Internal source name, used in URLs. + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + enabled: + type: boolean + authentication_flow: + type: string + format: uuid + nullable: true + description: Flow to use when authenticating existing users. + enrollment_flow: + type: string + format: uuid + nullable: true + description: Flow to use when enrolling new users. + user_property_mappings: + type: array + items: + type: string + format: uuid + group_property_mappings: + type: array + items: + type: string + format: uuid + component: + type: string + description: Get object component so that we know how to edit the object + readOnly: true + verbose_name: + type: string + description: Return object's verbose_name + readOnly: true + verbose_name_plural: + type: string + description: Return object's plural verbose_name + readOnly: true + meta_model_name: + type: string + description: Return internal model name + readOnly: true + policy_engine_mode: + $ref: '#/components/schemas/PolicyEngineMode' + user_matching_mode: + allOf: + - $ref: '#/components/schemas/UserMatchingModeEnum' + description: How the source determines if an existing user should be authenticated + or a new user enrolled. + managed: + type: string + nullable: true + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This flag only indicates that an object can + be overwritten by migrations. You can still modify the objects via the + API, but expect changes to be overwritten in a later update. + readOnly: true + user_path_template: + type: string + icon: + type: string + nullable: true + readOnly: true + bot_username: + type: string + description: Telegram bot username + request_message_access: + type: boolean + description: Request access to send messages from your bot. + pre_authentication_flow: + type: string + format: uuid + description: Flow used before authentication. + required: + - bot_username + - component + - icon + - managed + - meta_model_name + - name + - pk + - pre_authentication_flow + - slug + - verbose_name + - verbose_name_plural + TelegramSourcePropertyMapping: + type: object + description: TelegramSourcePropertyMapping Serializer + properties: + pk: + type: string + format: uuid + readOnly: true + title: Pm uuid + managed: + type: string + nullable: true + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This flag only indicates that an object can + be overwritten by migrations. You can still modify the objects via the + API, but expect changes to be overwritten in a later update. + name: + type: string + expression: + type: string + component: + type: string + description: Get object's component so that we know how to edit the object + readOnly: true + verbose_name: + type: string + description: Return object's verbose_name + readOnly: true + verbose_name_plural: + type: string + description: Return object's plural verbose_name + readOnly: true + meta_model_name: + type: string + description: Return internal model name + readOnly: true + required: + - component + - expression + - meta_model_name + - name + - pk + - verbose_name + - verbose_name_plural + TelegramSourcePropertyMappingRequest: + type: object + description: TelegramSourcePropertyMapping Serializer + properties: + managed: + type: string + nullable: true + minLength: 1 + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This flag only indicates that an object can + be overwritten by migrations. You can still modify the objects via the + API, but expect changes to be overwritten in a later update. + name: + type: string + minLength: 1 + expression: + type: string + minLength: 1 + required: + - expression + - name + TelegramSourceRequest: + type: object + description: Source Serializer + properties: + name: + type: string + minLength: 1 + description: Source's display Name. + slug: + type: string + minLength: 1 + description: Internal source name, used in URLs. + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + enabled: + type: boolean + authentication_flow: + type: string + format: uuid + nullable: true + description: Flow to use when authenticating existing users. + enrollment_flow: + type: string + format: uuid + nullable: true + description: Flow to use when enrolling new users. + user_property_mappings: + type: array + items: + type: string + format: uuid + group_property_mappings: + type: array + items: + type: string + format: uuid + policy_engine_mode: + $ref: '#/components/schemas/PolicyEngineMode' + user_matching_mode: + allOf: + - $ref: '#/components/schemas/UserMatchingModeEnum' + description: How the source determines if an existing user should be authenticated + or a new user enrolled. + user_path_template: + type: string + minLength: 1 + bot_username: + type: string + minLength: 1 + description: Telegram bot username + bot_token: + type: string + writeOnly: true + minLength: 1 + description: Telegram bot token + request_message_access: + type: boolean + description: Request access to send messages from your bot. + pre_authentication_flow: + type: string + format: uuid + description: Flow used before authentication. + required: + - bot_token + - bot_username + - name + - pre_authentication_flow + - slug Tenant: type: object description: Tenant Serializer @@ -59276,6 +60887,57 @@ components: - identifier - source - user + UserTelegramSourceConnection: + type: object + description: User source connection + properties: + pk: + type: integer + readOnly: true + title: ID + user: + type: integer + source: + type: string + format: uuid + source_obj: + allOf: + - $ref: '#/components/schemas/Source' + readOnly: true + identifier: + type: string + created: + type: string + format: date-time + readOnly: true + last_updated: + type: string + format: date-time + readOnly: true + required: + - created + - identifier + - last_updated + - pk + - source + - source_obj + - user + UserTelegramSourceConnectionRequest: + type: object + description: User source connection + properties: + user: + type: integer + source: + type: string + format: uuid + identifier: + type: string + minLength: 1 + required: + - identifier + - source + - user UserTypeEnum: enum: - internal diff --git a/web/authentik/sources/telegram.svg b/web/authentik/sources/telegram.svg new file mode 100644 index 0000000000..c10ae480a9 --- /dev/null +++ b/web/authentik/sources/telegram.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/web/src/admin/property-mappings/PropertyMappingListPage.ts b/web/src/admin/property-mappings/PropertyMappingListPage.ts index 1dc083ac41..dbc83696bf 100644 --- a/web/src/admin/property-mappings/PropertyMappingListPage.ts +++ b/web/src/admin/property-mappings/PropertyMappingListPage.ts @@ -12,6 +12,7 @@ import "#admin/property-mappings/PropertyMappingSourceOAuthForm"; import "#admin/property-mappings/PropertyMappingSourcePlexForm"; import "#admin/property-mappings/PropertyMappingSourceSAMLForm"; import "#admin/property-mappings/PropertyMappingSourceSCIMForm"; +import "#admin/property-mappings/PropertyMappingSourceTelegramForm"; import "#admin/property-mappings/PropertyMappingTestForm"; import "#admin/property-mappings/PropertyMappingWizard"; import "#admin/rbac/ObjectPermissionModal"; diff --git a/web/src/admin/property-mappings/PropertyMappingSourceTelegramForm.ts b/web/src/admin/property-mappings/PropertyMappingSourceTelegramForm.ts new file mode 100644 index 0000000000..580792d625 --- /dev/null +++ b/web/src/admin/property-mappings/PropertyMappingSourceTelegramForm.ts @@ -0,0 +1,39 @@ +import "#elements/CodeMirror"; +import "#elements/forms/HorizontalFormElement"; + +import { DEFAULT_CONFIG } from "#common/api/config"; + +import { BasePropertyMappingForm } from "#admin/property-mappings/BasePropertyMappingForm"; + +import { PropertymappingsApi, TelegramSourcePropertyMapping } from "@goauthentik/api"; + +import { customElement } from "lit/decorators.js"; + +@customElement("ak-property-mapping-source-telegram-form") +export class PropertyMappingSourceTelegramForm extends BasePropertyMappingForm { + protected override docLink = "/users-sources/sources/property-mappings/expressions"; + + loadInstance(pk: string): Promise { + return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramRetrieve({ + pmUuid: pk, + }); + } + + async send(data: TelegramSourcePropertyMapping): Promise { + if (this.instance) { + return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramUpdate({ + pmUuid: this.instance.pk, + telegramSourcePropertyMappingRequest: data, + }); + } + return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramCreate({ + telegramSourcePropertyMappingRequest: data, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-property-mapping-source-telegram-form": PropertyMappingSourceTelegramForm; + } +} diff --git a/web/src/admin/property-mappings/PropertyMappingWizard.ts b/web/src/admin/property-mappings/PropertyMappingWizard.ts index 04f3d8dafa..a6599d2a53 100644 --- a/web/src/admin/property-mappings/PropertyMappingWizard.ts +++ b/web/src/admin/property-mappings/PropertyMappingWizard.ts @@ -12,6 +12,7 @@ import "#admin/property-mappings/PropertyMappingSourceOAuthForm"; import "#admin/property-mappings/PropertyMappingSourcePlexForm"; import "#admin/property-mappings/PropertyMappingSourceSAMLForm"; import "#admin/property-mappings/PropertyMappingSourceSCIMForm"; +import "#admin/property-mappings/PropertyMappingSourceTelegramForm"; import "#admin/property-mappings/PropertyMappingTestForm"; import "#elements/forms/ProxyForm"; import "#elements/wizard/FormWizardPage"; diff --git a/web/src/admin/sources/SourceViewPage.ts b/web/src/admin/sources/SourceViewPage.ts index a81dcb1cd6..2c20a0b93c 100644 --- a/web/src/admin/sources/SourceViewPage.ts +++ b/web/src/admin/sources/SourceViewPage.ts @@ -4,6 +4,7 @@ import "#admin/sources/oauth/OAuthSourceViewPage"; import "#admin/sources/plex/PlexSourceViewPage"; import "#admin/sources/saml/SAMLSourceViewPage"; import "#admin/sources/scim/SCIMSourceViewPage"; +import "#admin/sources/telegram/TelegramSourceViewPage"; import "#elements/EmptyState"; import "#elements/buttons/SpinnerButton/ak-spinner-button"; @@ -63,6 +64,10 @@ export class SourceViewPage extends AKElement { return html``; + case "ak-source-telegram-form": + return html``; default: return html`

Invalid source type ${this.source.component}

`; } diff --git a/web/src/admin/sources/SourceWizard.ts b/web/src/admin/sources/SourceWizard.ts index f7c206d92e..197881beb3 100644 --- a/web/src/admin/sources/SourceWizard.ts +++ b/web/src/admin/sources/SourceWizard.ts @@ -4,6 +4,7 @@ import "#admin/sources/oauth/OAuthSourceForm"; import "#admin/sources/plex/PlexSourceForm"; import "#admin/sources/saml/SAMLSourceForm"; import "#admin/sources/scim/SCIMSourceForm"; +import "#admin/sources/telegram/TelegramSourceForm"; import "#elements/forms/ProxyForm"; import "#elements/wizard/FormWizardPage"; import "#elements/wizard/Wizard"; diff --git a/web/src/admin/sources/telegram/TelegramSourceForm.ts b/web/src/admin/sources/telegram/TelegramSourceForm.ts new file mode 100644 index 0000000000..3db1f0dffa --- /dev/null +++ b/web/src/admin/sources/telegram/TelegramSourceForm.ts @@ -0,0 +1,263 @@ +import { propertyMappingsProvider, propertyMappingsSelector } from "./TelegramSourceFormHelpers.js"; + +import { DEFAULT_CONFIG } from "#common/api/config"; + +import { WithCapabilitiesConfig } from "#elements/mixins/capabilities"; + +import { policyEngineModes } from "#admin/policies/PolicyEngineModes"; +import { BaseSourceForm } from "#admin/sources/BaseSourceForm"; +import { UserMatchingModeToLabel } from "#admin/sources/oauth/utils"; + +import { + FlowsInstancesListDesignationEnum, + SourcesApi, + TelegramSource, + TelegramSourceRequest, + UserMatchingModeEnum, +} from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html, TemplateResult } from "lit"; +import { customElement } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +@customElement("ak-source-telegram-form") +export class TelegramSourceForm extends WithCapabilitiesConfig(BaseSourceForm) { + async loadInstance(pk: string): Promise { + const source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramRetrieve({ + slug: pk, + }); + return source; + } + + async send(data: TelegramSource): Promise { + let source: TelegramSource; + if (this.instance?.pk) { + source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramPartialUpdate({ + slug: this.instance.slug, + patchedTelegramSourceRequest: data, + }); + } else { + source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramCreate({ + telegramSourceRequest: data as unknown as TelegramSourceRequest, + }); + } + return source; + } + + renderForm(): TemplateResult { + return html` + + + + + + + + + + + + + + + + + + + + +
+ + +

+ ${msg("Flow used before authentication.")} +

+
+ + +

+ ${msg("Flow to use when authenticating existing users.")} +

+
+ + +

+ ${msg("Flow to use when enrolling new users.")} +

+
+
+
+ +
+ + +

+ ${msg("Property mappings for user creation.")} +

+
+ + +

+ ${msg("Property mappings for group creation.")} +

+
+
+
+ +
+ + + + +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-source-telegram-form": TelegramSourceForm; + } +} diff --git a/web/src/admin/sources/telegram/TelegramSourceFormHelpers.ts b/web/src/admin/sources/telegram/TelegramSourceFormHelpers.ts new file mode 100644 index 0000000000..b2ec6d6365 --- /dev/null +++ b/web/src/admin/sources/telegram/TelegramSourceFormHelpers.ts @@ -0,0 +1,45 @@ +import { DEFAULT_CONFIG } from "#common/api/config"; + +import { DualSelectPair } from "#elements/ak-dual-select/types"; + +import { PropertymappingsApi, TelegramSourcePropertyMapping } from "@goauthentik/api"; + +const mappingToSelect = (m: TelegramSourcePropertyMapping) => [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceTelegramList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, _3]: DualSelectPair) => false, + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceTelegramRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/sources/telegram/TelegramSourceViewPage.ts b/web/src/admin/sources/telegram/TelegramSourceViewPage.ts new file mode 100644 index 0000000000..43876ed7e3 --- /dev/null +++ b/web/src/admin/sources/telegram/TelegramSourceViewPage.ts @@ -0,0 +1,160 @@ +import "#admin/sources/telegram/TelegramSourceForm"; + +import { DEFAULT_CONFIG } from "#common/api/config"; + +import { AKElement } from "#elements/Base"; + +import { sourceBindingTypeNotices } from "#admin/sources/utils"; + +import { + RbacPermissionsAssignedByUsersListModelEnum, + SourcesApi, + TelegramSource, +} from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { CSSResult, html, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFContent from "@patternfly/patternfly/components/Content/content.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +@customElement("ak-source-telegram-view") +export class TelegramSourceViewPage extends AKElement { + @property({ type: String }) + set sourceSlug(value: string) { + new SourcesApi(DEFAULT_CONFIG) + .sourcesTelegramRetrieve({ + slug: value, + }) + .then((source) => { + this.source = source; + }); + } + + @property({ attribute: false }) + source?: TelegramSource; + + static get styles(): CSSResult[] { + return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList]; + } + + render(): TemplateResult { + if (!this.source) { + return html``; + } + return html` +
+
+
+
+
+
+
+ ${msg("Name")} +
+
+
+ ${this.source.name} +
+
+
+
+
+ ${msg("Telegram bot")} +
+
+
+ ${this.source.botUsername} +
+
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ ${msg( + `These bindings control which users can access this source. + You can only use policies here as access is checked before the user is authenticated.`, + )} +
+
+ + +
+
+
+
+ +
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-source-telegram-view": TelegramSourceViewPage; + } +} diff --git a/web/src/elements/user/sources/SourceSettings.ts b/web/src/elements/user/sources/SourceSettings.ts index f8fafeeb0c..314b945b2e 100644 --- a/web/src/elements/user/sources/SourceSettings.ts +++ b/web/src/elements/user/sources/SourceSettings.ts @@ -2,6 +2,7 @@ import "#elements/EmptyState"; import "#elements/user/sources/SourceSettingsOAuth"; import "#elements/user/sources/SourceSettingsPlex"; import "#elements/user/sources/SourceSettingsSAML"; +import "#elements/user/sources/SourceSettingsTelegram"; import { DEFAULT_CONFIG } from "#common/api/config"; import { EVENT_REFRESH } from "#common/constants"; @@ -104,6 +105,13 @@ export class UserSourceSettingsPage extends AKElement { .configureUrl=${this.canConnect ? source.configureUrl : undefined} > `; + case "ak-user-settings-source-telegram": + return html` + `; default: return html`

${msg(str`Error: unsupported source settings: ${source.component}`)} diff --git a/web/src/elements/user/sources/SourceSettingsTelegram.ts b/web/src/elements/user/sources/SourceSettingsTelegram.ts new file mode 100644 index 0000000000..0548ce69b7 --- /dev/null +++ b/web/src/elements/user/sources/SourceSettingsTelegram.ts @@ -0,0 +1,73 @@ +import "#elements/Spinner"; + +import { DEFAULT_CONFIG } from "#common/api/config"; +import { EVENT_REFRESH } from "#common/constants"; +import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network"; +import { MessageLevel } from "#common/messages"; + +import { showMessage } from "#elements/messages/MessageContainer"; +import { BaseUserSettings } from "#elements/user/sources/BaseUserSettings"; + +import { SourcesApi } from "@goauthentik/api"; + +import { msg, str } from "@lit/localize"; +import { html, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-user-settings-source-telegram") +export class SourceSettingsTelegram extends BaseUserSettings { + @property() + title!: string; + + @property({ type: Number }) + connectionPk = 0; + + render(): TemplateResult { + if (this.connectionPk === -1) { + return html``; + } + if (this.connectionPk > 0) { + return html``; + } + return html`${msg("-")}`; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-user-settings-source-telegram": SourceSettingsTelegram; + } +} diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index f4e93b3964..d923567022 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -4,6 +4,7 @@ import "#flow/components/ak-brand-footer"; import "#flow/components/ak-flow-card"; import "#flow/sources/apple/AppleLoginInit"; import "#flow/sources/plex/PlexLoginInit"; +import "#flow/sources/telegram/TelegramLogin"; import "#flow/stages/FlowErrorStage"; import "#flow/stages/FlowFrameStage"; import "#flow/stages/RedirectStage"; @@ -490,6 +491,11 @@ export class FlowExecutor .host=${this as StageHost} .challenge=${this.challenge} >`; + case "ak-source-telegram": + return html``; // Providers case "ak-provider-oauth2-device-code": await import("#flow/providers/oauth2/DeviceCode"); diff --git a/web/src/flow/sources/telegram/TelegramLogin.ts b/web/src/flow/sources/telegram/TelegramLogin.ts new file mode 100644 index 0000000000..14b643af03 --- /dev/null +++ b/web/src/flow/sources/telegram/TelegramLogin.ts @@ -0,0 +1,89 @@ +import { BaseStage } from "#flow/stages/base"; + +import { TelegramChallengeResponseRequest, TelegramLoginChallenge } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { CSSResult, html, TemplateResult } from "lit"; +import { customElement } from "lit/decorators.js"; +import { createRef, ref } from "lit/directives/ref.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFDivider from "@patternfly/patternfly/components/Divider/divider.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFLogin from "@patternfly/patternfly/components/Login/login.css"; +import PFTitle from "@patternfly/patternfly/components/Title/title.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +type TelegramUserResponse = { + id: number; + first_name?: string; + last_name?: string; + username?: string; + photo_url?: string; + auth_date: number; + hash: string; +}; + +@customElement("ak-flow-source-telegram") +export class TelegramLogin extends BaseStage< + TelegramLoginChallenge, + TelegramChallengeResponseRequest +> { + btnRef = createRef(); + + static get styles(): CSSResult[] { + return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, PFDivider]; + } + + firstUpdated(): void { + const widgetScript = document.createElement("script"); + widgetScript.src = "https://telegram.org/js/telegram-widget.js?22"; + widgetScript.type = "text/javascript"; + widgetScript.setAttribute("data-radius", "0"); + widgetScript.setAttribute("data-telegram-login", this.challenge.botUsername); + if (this.challenge.requestMessageAccess) { + widgetScript.setAttribute("data-request-access", "write"); + } + const callbackName = + "__ak_telegram_login_callback_" + (Math.random() + 1).toString(36).substring(7); + (window as unknown as Record void>)[callbackName] = + (user: TelegramUserResponse) => { + this.host.submit({ + id: user.id, + authDate: user.auth_date, + hash: user.hash, + firstName: user.first_name, + lastName: user.last_name, + username: user.username, + photoUrl: user.photo_url, + }); + }; + widgetScript.setAttribute("data-onauth", callbackName + "(user)"); + this.btnRef.value?.appendChild(widgetScript); + widgetScript.onload = () => { + if (widgetScript.previousSibling) { + this.btnRef.value?.appendChild(widgetScript.previousSibling); + } + }; + document.body.append(widgetScript); + } + + render(): TemplateResult { + return html` + ${msg("Authenticating with Telegram...")} +

+
+

${msg("Click the button below to start.")}

+ +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-flow-source-telegram": TelegramLogin; + } +} diff --git a/website/docs/developer-docs/contributing.md b/website/docs/developer-docs/contributing.md index 521b680bb5..71c954e190 100644 --- a/website/docs/developer-docs/contributing.md +++ b/website/docs/developer-docs/contributing.md @@ -60,16 +60,17 @@ authentik │   ├── oauth2 - OIDC-compliant OAuth2 provider │   ├── proxy - Provides an identity-aware proxy using an outpost │   ├── radius - Provides a RADIUS server that authenticates using flows -│   ├── saml - SAML2 Provider -│   └── scim - SCIM Provider +│   ├── saml - SAML2 provider +│   └── scim - SCIM provider ├── recovery - Generate keys to use in case you lock yourself out ├── root - Root Django application, contains global settings and routes ├── sources │   ├── kerberos - Sync Kerberos users into authentik │   ├── ldap - Sync LDAP users from OpenLDAP or Active Directory into authentik -│   ├── oauth - OAuth1 and OAuth2 Source +│   ├── oauth - OAuth1 and OAuth2 source │   ├── plex - Plex source -│   └── saml - SAML2 Source +│   ├── saml - SAML2 source +│   └── telegram - Telegram source ├── stages │   ├── authenticator_duo - Configure a DUO authenticator │   ├── authenticator_static - Configure TOTP backup keys diff --git a/website/docs/sidebar.mjs b/website/docs/sidebar.mjs index 3d55c251d7..f0d934b8bf 100644 --- a/website/docs/sidebar.mjs +++ b/website/docs/sidebar.mjs @@ -570,8 +570,9 @@ const items = [ ], }, "users-sources/sources/social-logins/mailcow/index", - "users-sources/sources/social-logins/twitch/index", "users-sources/sources/social-logins/plex/index", + "users-sources/sources/social-logins/telegram/index", + "users-sources/sources/social-logins/twitch/index", "users-sources/sources/social-logins/twitter/index", ], }, diff --git a/website/docs/users-sources/sources/social-logins/telegram/index.md b/website/docs/users-sources/sources/social-logins/telegram/index.md new file mode 100644 index 0000000000..1d6fb73eeb --- /dev/null +++ b/website/docs/users-sources/sources/social-logins/telegram/index.md @@ -0,0 +1,60 @@ +--- +title: Telegram +support_level: community +--- + +Configuring Telegram as a source allows users to authenticate within authentik using their Telegram account credentials. + +## Preparation + +Using Telegram as a source requires that your authentik instance is served from a domain. + +## Telegram configuration + +To use Telegram as a source, you first need to register a Telegram bot: + +1. Start a chat with `@BotFather` on Telegram. +2. Use the `/newbot` command to create a new bot. Define a name and username for your new bot (e.g., `authentik_bot`). +3. BotFather will provide you with a token for the new bot. Take note of the username and token because they will be required when setting up the source in authentik. +4. Link the bot to your authentik domain name using the `/setdomain` command. + +:::note +The domain name set in Telegram must **exactly** match the FQDN of the authentik installation. +::: + +Now that the bot is configured you can proceed to creating a source in authentik. + +## authentik configuration + +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 **Telegram** as the source type. + - **Create Telegram Source**: provide a name, a slug, and the following required configurations: + - **Bot username**: The username of your Telegram bot (e.g., `authentik_bot`). + - **Bot token**: The token of your Telegram bot. + - **Request access to send messages from your bot**: enable this to allow your bot to send messages to authentik users utilizing the Telegram source for authentication. + +3. Click **Save**. + +:::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). +::: + +## Telegram source property mappings + +[Property mappings](../../property-mappings/index.md) can be used to map Telegram user properties to authentik user properties. + +### Expression data + +Telegram user data is accessible to Telegram source property mappings as a dictionary named `info`. +The dictionary contains the following fields: + +- `id` - Telegram user ID +- `username` - Username of the user. Might not be present. +- `first_name` - First name of the user. Might not be present. +- `last_name` - Last name of the user. Might not be present. +- `photo_url` - URL of the user's profile photo. Might not be present. + +## Resources + +- [Telegram Documentation - BotFather](https://core.telegram.org/bots/features#botfather)