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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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 <tanamarieberry@yahoo.com>
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] <support@github.com>
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] <support@github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Alexander Tereshkin
2025-10-01 18:03:38 +03:00
committed by GitHub
parent 1f2d411a7c
commit eeb5cb08cd
33 changed files with 3689 additions and 5 deletions
+1
View File
@@ -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",
@@ -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"]
+41
View File
@@ -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"]
@@ -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
+9
View File
@@ -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
@@ -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",),
),
]
+156
View File
@@ -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")
+48
View File
@@ -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
+185
View File
@@ -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}
),
)
+23
View File
@@ -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("<slug:source_slug>/start/", TelegramStartView.as_view(), name="start"),
path("<slug:source_slug>/", TelegramLoginView.as_view(), name="login"),
]
api_urlpatterns = [
("propertymappings/source/telegram", TelegramSourcePropertyMappingViewSet),
("sources/user_connections/telegram", UserTelegramSourceConnectionViewSet),
("sources/group_connections/telegram", GroupTelegramSourceConnectionViewSet),
("sources/telegram", TelegramSourceViewSet),
]
+98
View File
@@ -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,
)
+480
View File
@@ -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": {
+1
View File
@@ -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.*",
+1662
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<defs
id="defs121">
<linearGradient
x1="0.5"
y1="0"
x2="0.5"
y2="0.99258339"
id="linearGradient-1">
<stop
stop-color="#2AABEE"
offset="0%"
id="stop116" />
<stop
stop-color="#229ED9"
offset="100%"
id="stop118" />
</linearGradient>
</defs>
<g
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
transform="translate(-488,-488)">
<circle
id="Oval"
fill="url(#linearGradient-1)"
cx="500"
cy="500"
r="12"
style="fill:#000000;fill-opacity:1;stroke-width:0.024" />
<path
d="m 493.43188,499.87333 c 3.49825,-1.52413 5.83096,-2.52893 6.99813,-3.0144 3.33253,-1.38611 4.025,-1.62689 4.47634,-1.63485 0.0993,-0.002 0.32123,0.0229 0.465,0.13952 0.12141,0.0985 0.15481,0.23158 0.17079,0.32498 0.016,0.0934 0.0359,0.30615 0.0201,0.4724 -0.18059,1.89748 -0.96201,6.50217 -1.35955,8.62739 -0.16821,0.89925 -0.49943,1.20077 -0.82009,1.23028 -0.69686,0.0641 -1.22602,-0.46054 -1.90097,-0.90297 -1.05615,-0.69233 -1.65282,-1.1233 -2.678,-1.79888 -1.18477,-0.78075 -0.41673,-1.20986 0.25847,-1.91115 0.1767,-0.18354 3.24709,-2.97629 3.30652,-3.22964 0.007,-0.0317 0.0143,-0.14979 -0.0558,-0.21216 -0.0702,-0.0624 -0.17372,-0.041 -0.24845,-0.0241 -0.10593,0.024 -1.79315,1.13924 -5.06167,3.34558 -0.47891,0.32886 -0.9127,0.48909 -1.30135,0.48069 -0.42847,-0.009 -1.25265,-0.24226 -1.86535,-0.44142 -0.7515,-0.24429 -1.34878,-0.37344 -1.29677,-0.78831 0.0271,-0.21609 0.32466,-0.43708 0.89272,-0.66298 z"
id="Path-3"
fill="#ffffff"
style="stroke-width:0.024" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -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";
@@ -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<TelegramSourcePropertyMapping> {
protected override docLink = "/users-sources/sources/property-mappings/expressions";
loadInstance(pk: string): Promise<TelegramSourcePropertyMapping> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramRetrieve({
pmUuid: pk,
});
}
async send(data: TelegramSourcePropertyMapping): Promise<TelegramSourcePropertyMapping> {
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;
}
}
@@ -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";
+5
View File
@@ -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`<ak-source-scim-view
sourceSlug=${this.source.slug}
></ak-source-scim-view>`;
case "ak-source-telegram-form":
return html`<ak-source-telegram-view
sourceSlug=${this.source.slug}
></ak-source-telegram-view>`;
default:
return html`<p>Invalid source type ${this.source.component}</p>`;
}
+1
View File
@@ -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";
@@ -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<TelegramSource>) {
async loadInstance(pk: string): Promise<TelegramSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramRetrieve({
slug: pk,
});
return source;
}
async send(data: TelegramSource): Promise<TelegramSource> {
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`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-slug-input
name="slug"
value=${ifDefined(this.instance?.slug)}
label=${msg("Slug")}
required
input-hint="code"
></ak-slug-input>
<ak-form-element-horizontal name="enabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Enabled")}</span>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("User matching mode")}
required
name="userMatchingMode"
>
<select class="pf-c-form-control">
<option
value=${UserMatchingModeEnum.Identifier}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.Identifier}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.Identifier)}
</option>
<option
value=${UserMatchingModeEnum.EmailLink}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.EmailLink}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailLink)}
</option>
<option
value=${UserMatchingModeEnum.EmailDeny}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.EmailDeny}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailDeny)}
</option>
<option
value=${UserMatchingModeEnum.UsernameLink}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.UsernameLink}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameLink)}
</option>
<option
value=${UserMatchingModeEnum.UsernameDeny}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.UsernameDeny}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameDeny)}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Bot username")} required name="botUsername">
<input
type="text"
value="${ifDefined(this.instance?.botUsername)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-secret-text-input
label=${msg("Bot token")}
name="botToken"
input-hint="code"
?required=${this.instance === undefined}
?revealed=${this.instance === undefined}
></ak-secret-text-input>
<ak-form-element-horizontal required name="requestMessageAccess">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.requestMessageAccess ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Request access to send messages from your bot")}
</span>
</label>
</ak-form-element-horizontal>
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Pre-authentication flow")}
required
name="preAuthenticationFlow"
>
<ak-source-flow-search
flowType=${FlowsInstancesListDesignationEnum.StageConfiguration}
.currentFlow=${this.instance?.preAuthenticationFlow}
.instanceId=${this.instance?.pk}
fallback="default-source-pre-authentication"
></ak-source-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used before authentication.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
>
<ak-source-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
.instanceId=${this.instance?.pk}
fallback="default-source-authentication"
></ak-source-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow to use when authenticating existing users.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Enrollment flow")}
name="enrollmentFlow"
>
<ak-source-flow-search
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
.currentFlow=${this.instance?.enrollmentFlow}
.instanceId=${this.instance?.pk}
fallback="default-source-enrollment"
></ak-source-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow to use when enrolling new users.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group open label="${msg("Telegram Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${propertyMappingsProvider}
.selector=${propertyMappingsSelector(
this.instance?.userPropertyMappings,
)}
available-label="${msg("Available User Property Mappings")}"
selected-label="${msg("Selected User Property Mappings")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings for user creation.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="groupPropertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${propertyMappingsProvider}
.selector=${propertyMappingsSelector(
this.instance?.groupPropertyMappings,
)}
available-label="${msg("Available Group Property Mappings")}"
selected-label="${msg("Selected Group Property Mappings")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings for group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group label="${msg("Advanced settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Policy engine mode")}
required
name="policyEngineMode"
>
<ak-radio
.options=${policyEngineModes}
.value=${this.instance?.policyEngineMode}
>
</ak-radio>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-source-telegram-form": TelegramSourceForm;
}
}
@@ -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<TelegramSourcePropertyMapping>[]) =>
mappings.filter(
([_0, _1, _2, _3]: DualSelectPair<TelegramSourcePropertyMapping>) => 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);
};
}
@@ -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` <ak-tabs>
<section
slot="page-overview"
data-tab-title="${msg("Overview")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Name")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.name}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Telegram bot")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.botUsername}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Telegram Source")} </span>
<ak-source-telegram-form
slot="form"
.instancePk=${this.source.slug}
>
</ak-source-telegram-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Edit")}
</button>
</ak-forms-modal>
</div>
</div>
</div>
</section>
<section
slot="page-changelog"
data-tab-title="${msg("Changelog")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<ak-object-changelog
targetModelPk=${this.source.pk || ""}
targetModelApp="authentik_sources_telegram"
targetModelName="telegramsource"
>
</ak-object-changelog>
</div>
</div>
</div>
</section>
<div
slot="page-policy-binding"
data-tab-title="${msg("Policy Bindings")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${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.`,
)}
</div>
<div class="pf-c-card__body">
<ak-bound-policies-list
.target=${this.source.pk}
.typeNotices=${sourceBindingTypeNotices()}
.policyEngineMode=${this.source.policyEngineMode}
>
</ak-bound-policies-list>
</div>
</div>
</div>
</div>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.AuthentikSourcesTelegramTelegramsource}
objectPk=${this.source.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-source-telegram-view": TelegramSourceViewPage;
}
}
@@ -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}
>
</ak-user-settings-source-saml>`;
case "ak-user-settings-source-telegram":
return html`<ak-user-settings-source-telegram
objectId=${source.objectUid}
title=${source.title}
connectionPk=${connectionPk}
>
</ak-user-settings-source-telegram>`;
default:
return html`<p>
${msg(str`Error: unsupported source settings: ${source.component}`)}
@@ -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`<ak-spinner></ak-spinner>`;
}
if (this.connectionPk > 0) {
return html`<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new SourcesApi(DEFAULT_CONFIG)
.sourcesUserConnectionsTelegramDestroy({
id: this.connectionPk,
})
.then(() => {
showMessage({
level: MessageLevel.info,
message: msg("Successfully disconnected source"),
});
})
.catch(async (error: unknown) => {
const parsedError = await parseAPIResponseError(error);
showMessage({
level: MessageLevel.error,
message: msg(
str`Failed to disconnected source: ${pluckErrorDetail(parsedError)}`,
),
});
})
.finally(() => {
this.parentElement?.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
}}
>
${msg("Disconnect")}
</button>`;
}
return html`${msg("-")}`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-user-settings-source-telegram": SourceSettingsTelegram;
}
}
+6
View File
@@ -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}
></ak-flow-source-oauth-apple>`;
case "ak-source-telegram":
return html`<ak-flow-source-telegram
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-source-telegram>`;
// Providers
case "ak-provider-oauth2-device-code":
await import("#flow/providers/oauth2/DeviceCode");
@@ -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<string, (user: TelegramUserResponse) => 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` <ak-flow-card .challenge=${this.challenge}>
<span slot="title">${msg("Authenticating with Telegram...")}</span>
<form class="pf-c-form">
<hr class="pf-c-divider" />
<p>${msg("Click the button below to start.")}</p>
<div ${ref(this.btnRef)}></div>
</form>
</ak-flow-card>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-flow-source-telegram": TelegramLogin;
}
}
+5 -4
View File
@@ -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
+2 -1
View File
@@ -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",
],
},
@@ -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)