authentik.endpoints.connectors.agent.api.connectors

  1from typing import cast
  2
  3from django.utils.timezone import now
  4from django.utils.translation import gettext_lazy as _
  5from drf_spectacular.types import OpenApiTypes
  6from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
  7from rest_framework.decorators import action
  8from rest_framework.exceptions import PermissionDenied, ValidationError
  9from rest_framework.fields import ChoiceField
 10from rest_framework.permissions import IsAuthenticated
 11from rest_framework.relations import PrimaryKeyRelatedField
 12from rest_framework.request import Request
 13from rest_framework.response import Response
 14from rest_framework.viewsets import ModelViewSet
 15
 16from authentik.core.api.used_by import UsedByMixin
 17from authentik.core.api.utils import PassiveSerializer
 18from authentik.endpoints.api.connectors import ConnectorSerializer
 19from authentik.endpoints.connectors.agent.api.agent import (
 20    AgentConfigSerializer,
 21    AgentTokenResponseSerializer,
 22    EnrollSerializer,
 23)
 24from authentik.endpoints.connectors.agent.auth import (
 25    AgentAuth,
 26    AgentEnrollmentAuth,
 27    DeviceAuthFedAuthentication,
 28    agent_auth_issue_token,
 29    check_device_policies,
 30)
 31from authentik.endpoints.connectors.agent.controller import MDMConfigResponseSerializer
 32from authentik.endpoints.connectors.agent.models import (
 33    AgentConnector,
 34    AgentDeviceConnection,
 35    DeviceToken,
 36    EnrollmentToken,
 37)
 38from authentik.endpoints.facts import DeviceFacts, OSFamily
 39from authentik.endpoints.models import Device
 40from authentik.events.models import Event, EventAction
 41from authentik.flows.planner import PLAN_CONTEXT_DEVICE
 42from authentik.lib.utils.reflection import ConditionalInheritance
 43from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 44
 45
 46class AgentConnectorSerializer(ConnectorSerializer):
 47
 48    class Meta(ConnectorSerializer.Meta):
 49        model = AgentConnector
 50        fields = ConnectorSerializer.Meta.fields + [
 51            "snapshot_expiry",
 52            "auth_session_duration",
 53            "auth_terminate_session_on_expiry",
 54            "refresh_interval",
 55            "authorization_flow",
 56            "nss_uid_offset",
 57            "nss_gid_offset",
 58            "challenge_key",
 59            "challenge_idle_timeout",
 60            "challenge_trigger_check_in",
 61            "jwt_federation_providers",
 62        ]
 63
 64
 65class MDMConfigSerializer(PassiveSerializer):
 66
 67    platform = ChoiceField(choices=OSFamily.choices)
 68    enrollment_token = PrimaryKeyRelatedField(
 69        queryset=EnrollmentToken.objects.including_expired().all()
 70    )
 71
 72    def validate_platform(self, platform: OSFamily) -> OSFamily:
 73        if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
 74            raise ValidationError(_("Selected platform not supported"))
 75        return platform
 76
 77    def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken:
 78        if token.is_expired:
 79            raise ValidationError(_("Token is expired"))
 80        if token.connector != self.context["connector"]:
 81            raise ValidationError(_("Invalid token for connector"))
 82        return token
 83
 84
 85class AgentConnectorViewSet(
 86    ConditionalInheritance(
 87        "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin"
 88    ),
 89    UsedByMixin,
 90    ModelViewSet,
 91):
 92
 93    queryset = AgentConnector.objects.all()
 94    serializer_class = AgentConnectorSerializer
 95    search_fields = ["name"]
 96    ordering = ["name"]
 97    filterset_fields = ["name", "enabled"]
 98
 99    @extend_schema(
100        request=MDMConfigSerializer(),
101        responses=MDMConfigResponseSerializer(),
102    )
103    @action(methods=["POST"], detail=True)
104    def mdm_config(self, request: Request, pk) -> Response:
105        """Generate configuration for MDM systems to deploy authentik Agent"""
106        connector = cast(AgentConnector, self.get_object())
107        data = MDMConfigSerializer(data=request.data, context={"connector": connector})
108        data.is_valid(raise_exception=True)
109        token = data.validated_data["enrollment_token"]
110        if not request.user.has_perm("view_enrollment_token_key", token):
111            raise PermissionDenied()
112        ctrl = connector.controller(connector)
113        payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
114        return Response(payload.validated_data)
115
116    @extend_schema(
117        request=EnrollSerializer(),
118        responses={200: AgentTokenResponseSerializer},
119    )
120    @action(
121        methods=["POST"],
122        detail=False,
123        authentication_classes=[AgentEnrollmentAuth],
124    )
125    def enroll(self, request: Request):
126        token: EnrollmentToken = request.auth
127        data = EnrollSerializer(data=request.data)
128        data.is_valid(raise_exception=True)
129        device, _ = Device.objects.get_or_create(
130            identifier=data.validated_data["device_serial"],
131            defaults={
132                "name": data.validated_data["device_name"],
133                "expiring": False,
134                "access_group": token.device_group,
135            },
136        )
137        connection, _ = AgentDeviceConnection.objects.update_or_create(
138            device=device,
139            connector=token.connector,
140        )
141        DeviceToken.objects.including_expired().filter(device=connection).delete()
142        token = DeviceToken.objects.create(device=connection, expiring=False)
143        return Response(
144            {
145                "token": token.key,
146                "expires_in": 0,
147            }
148        )
149
150    @extend_schema(
151        request=OpenApiTypes.NONE,
152        responses=AgentConfigSerializer(),
153    )
154    @action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
155    def agent_config(self, request: Request):
156        token: DeviceToken = request.auth
157        connector: AgentConnector = token.device.connector.agentconnector
158        return Response(
159            AgentConfigSerializer(
160                connector, context={"request": request, "device": token.device.device}
161            ).data
162        )
163
164    @extend_schema(
165        request=DeviceFacts(),
166        responses={204: OpenApiResponse(description="Successfully checked in")},
167    )
168    @action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
169    def check_in(self, request: Request):
170        token: DeviceToken = request.auth
171        data = DeviceFacts(data=request.data)
172        data.is_valid(raise_exception=True)
173        connection: AgentDeviceConnection = token.device
174        connection.create_snapshot(data.validated_data)
175        return Response(status=204)
176
177    @extend_schema(
178        request=OpenApiTypes.NONE,
179        parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)],
180        responses={
181            200: AgentTokenResponseSerializer(),
182            404: OpenApiResponse(description="Device not found"),
183        },
184    )
185    @action(
186        methods=["POST"],
187        detail=False,
188        pagination_class=None,
189        filter_backends=[],
190        permission_classes=[IsAuthenticated],
191        authentication_classes=[DeviceAuthFedAuthentication],
192    )
193    def auth_fed(self, request: Request) -> Response:
194        federated_token, device, connector = request.auth
195
196        policy_result = check_device_policies(device, federated_token.user, request._request)
197        if not policy_result.passing:
198            raise ValidationError(
199                {"policy_result": "Policy denied access", "policy_messages": policy_result.messages}
200            )
201
202        token, exp = agent_auth_issue_token(device, connector, federated_token.user)
203        rel_exp = int((exp - now()).total_seconds())
204        Event.new(
205            EventAction.LOGIN,
206            **{
207                PLAN_CONTEXT_METHOD: "jwt",
208                PLAN_CONTEXT_METHOD_ARGS: {
209                    "jwt": federated_token,
210                    "provider": federated_token.provider,
211                },
212                PLAN_CONTEXT_DEVICE: device,
213            },
214        ).from_http(request, user=federated_token.user)
215        return Response({"token": token, "expires_in": rel_exp})
class AgentConnectorSerializer(authentik.endpoints.api.connectors.ConnectorSerializer):
47class AgentConnectorSerializer(ConnectorSerializer):
48
49    class Meta(ConnectorSerializer.Meta):
50        model = AgentConnector
51        fields = ConnectorSerializer.Meta.fields + [
52            "snapshot_expiry",
53            "auth_session_duration",
54            "auth_terminate_session_on_expiry",
55            "refresh_interval",
56            "authorization_flow",
57            "nss_uid_offset",
58            "nss_gid_offset",
59            "challenge_key",
60            "challenge_idle_timeout",
61            "challenge_trigger_check_in",
62            "jwt_federation_providers",
63        ]

A ModelSerializer is just a regular Serializer, except that:

  • A set of default fields are automatically populated.
  • A set of default validators are automatically populated.
  • Default .create() and .update() implementations are provided.

The process of automatically determining a set of serializer fields based on the model fields is reasonably complex, but you almost certainly don't need to dig into the implementation.

If the ModelSerializer class doesn't generate the set of fields that you need you should either declare the extra/differing fields explicitly on the serializer class, or simply use a Serializer class.

class AgentConnectorSerializer.Meta(authentik.endpoints.api.connectors.ConnectorSerializer.Meta):
49    class Meta(ConnectorSerializer.Meta):
50        model = AgentConnector
51        fields = ConnectorSerializer.Meta.fields + [
52            "snapshot_expiry",
53            "auth_session_duration",
54            "auth_terminate_session_on_expiry",
55            "refresh_interval",
56            "authorization_flow",
57            "nss_uid_offset",
58            "nss_gid_offset",
59            "challenge_key",
60            "challenge_idle_timeout",
61            "challenge_trigger_check_in",
62            "jwt_federation_providers",
63        ]
fields = ['connector_uuid', 'name', 'enabled', 'component', 'verbose_name', 'verbose_name_plural', 'meta_model_name', 'snapshot_expiry', 'auth_session_duration', 'auth_terminate_session_on_expiry', 'refresh_interval', 'authorization_flow', 'nss_uid_offset', 'nss_gid_offset', 'challenge_key', 'challenge_idle_timeout', 'challenge_trigger_check_in', 'jwt_federation_providers']
class MDMConfigSerializer(authentik.core.api.utils.PassiveSerializer):
66class MDMConfigSerializer(PassiveSerializer):
67
68    platform = ChoiceField(choices=OSFamily.choices)
69    enrollment_token = PrimaryKeyRelatedField(
70        queryset=EnrollmentToken.objects.including_expired().all()
71    )
72
73    def validate_platform(self, platform: OSFamily) -> OSFamily:
74        if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
75            raise ValidationError(_("Selected platform not supported"))
76        return platform
77
78    def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken:
79        if token.is_expired:
80            raise ValidationError(_("Token is expired"))
81        if token.connector != self.context["connector"]:
82            raise ValidationError(_("Invalid token for connector"))
83        return token

Base serializer class which doesn't implement create/update methods

platform
enrollment_token
def validate_platform( self, platform: authentik.endpoints.facts.OSFamily) -> authentik.endpoints.facts.OSFamily:
73    def validate_platform(self, platform: OSFamily) -> OSFamily:
74        if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
75            raise ValidationError(_("Selected platform not supported"))
76        return platform
78    def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken:
79        if token.is_expired:
80            raise ValidationError(_("Token is expired"))
81        if token.connector != self.context["connector"]:
82            raise ValidationError(_("Invalid token for connector"))
83        return token
 86class AgentConnectorViewSet(
 87    ConditionalInheritance(
 88        "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin"
 89    ),
 90    UsedByMixin,
 91    ModelViewSet,
 92):
 93
 94    queryset = AgentConnector.objects.all()
 95    serializer_class = AgentConnectorSerializer
 96    search_fields = ["name"]
 97    ordering = ["name"]
 98    filterset_fields = ["name", "enabled"]
 99
100    @extend_schema(
101        request=MDMConfigSerializer(),
102        responses=MDMConfigResponseSerializer(),
103    )
104    @action(methods=["POST"], detail=True)
105    def mdm_config(self, request: Request, pk) -> Response:
106        """Generate configuration for MDM systems to deploy authentik Agent"""
107        connector = cast(AgentConnector, self.get_object())
108        data = MDMConfigSerializer(data=request.data, context={"connector": connector})
109        data.is_valid(raise_exception=True)
110        token = data.validated_data["enrollment_token"]
111        if not request.user.has_perm("view_enrollment_token_key", token):
112            raise PermissionDenied()
113        ctrl = connector.controller(connector)
114        payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
115        return Response(payload.validated_data)
116
117    @extend_schema(
118        request=EnrollSerializer(),
119        responses={200: AgentTokenResponseSerializer},
120    )
121    @action(
122        methods=["POST"],
123        detail=False,
124        authentication_classes=[AgentEnrollmentAuth],
125    )
126    def enroll(self, request: Request):
127        token: EnrollmentToken = request.auth
128        data = EnrollSerializer(data=request.data)
129        data.is_valid(raise_exception=True)
130        device, _ = Device.objects.get_or_create(
131            identifier=data.validated_data["device_serial"],
132            defaults={
133                "name": data.validated_data["device_name"],
134                "expiring": False,
135                "access_group": token.device_group,
136            },
137        )
138        connection, _ = AgentDeviceConnection.objects.update_or_create(
139            device=device,
140            connector=token.connector,
141        )
142        DeviceToken.objects.including_expired().filter(device=connection).delete()
143        token = DeviceToken.objects.create(device=connection, expiring=False)
144        return Response(
145            {
146                "token": token.key,
147                "expires_in": 0,
148            }
149        )
150
151    @extend_schema(
152        request=OpenApiTypes.NONE,
153        responses=AgentConfigSerializer(),
154    )
155    @action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
156    def agent_config(self, request: Request):
157        token: DeviceToken = request.auth
158        connector: AgentConnector = token.device.connector.agentconnector
159        return Response(
160            AgentConfigSerializer(
161                connector, context={"request": request, "device": token.device.device}
162            ).data
163        )
164
165    @extend_schema(
166        request=DeviceFacts(),
167        responses={204: OpenApiResponse(description="Successfully checked in")},
168    )
169    @action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
170    def check_in(self, request: Request):
171        token: DeviceToken = request.auth
172        data = DeviceFacts(data=request.data)
173        data.is_valid(raise_exception=True)
174        connection: AgentDeviceConnection = token.device
175        connection.create_snapshot(data.validated_data)
176        return Response(status=204)
177
178    @extend_schema(
179        request=OpenApiTypes.NONE,
180        parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)],
181        responses={
182            200: AgentTokenResponseSerializer(),
183            404: OpenApiResponse(description="Device not found"),
184        },
185    )
186    @action(
187        methods=["POST"],
188        detail=False,
189        pagination_class=None,
190        filter_backends=[],
191        permission_classes=[IsAuthenticated],
192        authentication_classes=[DeviceAuthFedAuthentication],
193    )
194    def auth_fed(self, request: Request) -> Response:
195        federated_token, device, connector = request.auth
196
197        policy_result = check_device_policies(device, federated_token.user, request._request)
198        if not policy_result.passing:
199            raise ValidationError(
200                {"policy_result": "Policy denied access", "policy_messages": policy_result.messages}
201            )
202
203        token, exp = agent_auth_issue_token(device, connector, federated_token.user)
204        rel_exp = int((exp - now()).total_seconds())
205        Event.new(
206            EventAction.LOGIN,
207            **{
208                PLAN_CONTEXT_METHOD: "jwt",
209                PLAN_CONTEXT_METHOD_ARGS: {
210                    "jwt": federated_token,
211                    "provider": federated_token.provider,
212                },
213                PLAN_CONTEXT_DEVICE: device,
214            },
215        ).from_http(request, user=federated_token.user)
216        return Response({"token": token, "expires_in": rel_exp})

Mixin to add a used_by endpoint to return a list of all objects using this object

queryset = <InheritanceQuerySet []>
serializer_class = <class 'AgentConnectorSerializer'>
search_fields = ['name']
ordering = ['name']
filterset_fields = ['name', 'enabled']
@extend_schema(request=MDMConfigSerializer(), responses=MDMConfigResponseSerializer())
@action(methods=['POST'], detail=True)
def mdm_config( self, request: rest_framework.request.Request, pk) -> rest_framework.response.Response:
100    @extend_schema(
101        request=MDMConfigSerializer(),
102        responses=MDMConfigResponseSerializer(),
103    )
104    @action(methods=["POST"], detail=True)
105    def mdm_config(self, request: Request, pk) -> Response:
106        """Generate configuration for MDM systems to deploy authentik Agent"""
107        connector = cast(AgentConnector, self.get_object())
108        data = MDMConfigSerializer(data=request.data, context={"connector": connector})
109        data.is_valid(raise_exception=True)
110        token = data.validated_data["enrollment_token"]
111        if not request.user.has_perm("view_enrollment_token_key", token):
112            raise PermissionDenied()
113        ctrl = connector.controller(connector)
114        payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
115        return Response(payload.validated_data)

Generate configuration for MDM systems to deploy authentik Agent

@extend_schema(request=EnrollSerializer(), responses={200: AgentTokenResponseSerializer})
@action(methods=['POST'], detail=False, authentication_classes=[AgentEnrollmentAuth])
def enroll(self, request: rest_framework.request.Request):
117    @extend_schema(
118        request=EnrollSerializer(),
119        responses={200: AgentTokenResponseSerializer},
120    )
121    @action(
122        methods=["POST"],
123        detail=False,
124        authentication_classes=[AgentEnrollmentAuth],
125    )
126    def enroll(self, request: Request):
127        token: EnrollmentToken = request.auth
128        data = EnrollSerializer(data=request.data)
129        data.is_valid(raise_exception=True)
130        device, _ = Device.objects.get_or_create(
131            identifier=data.validated_data["device_serial"],
132            defaults={
133                "name": data.validated_data["device_name"],
134                "expiring": False,
135                "access_group": token.device_group,
136            },
137        )
138        connection, _ = AgentDeviceConnection.objects.update_or_create(
139            device=device,
140            connector=token.connector,
141        )
142        DeviceToken.objects.including_expired().filter(device=connection).delete()
143        token = DeviceToken.objects.create(device=connection, expiring=False)
144        return Response(
145            {
146                "token": token.key,
147                "expires_in": 0,
148            }
149        )
@extend_schema(request=OpenApiTypes.NONE, responses=AgentConfigSerializer())
@action(methods=['GET'], detail=False, authentication_classes=[AgentAuth])
def agent_config(self, request: rest_framework.request.Request):
151    @extend_schema(
152        request=OpenApiTypes.NONE,
153        responses=AgentConfigSerializer(),
154    )
155    @action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
156    def agent_config(self, request: Request):
157        token: DeviceToken = request.auth
158        connector: AgentConnector = token.device.connector.agentconnector
159        return Response(
160            AgentConfigSerializer(
161                connector, context={"request": request, "device": token.device.device}
162            ).data
163        )
@extend_schema(request=DeviceFacts(), responses={204: OpenApiResponse(description='Successfully checked in')})
@action(methods=['POST'], detail=False, authentication_classes=[AgentAuth])
def check_in(self, request: rest_framework.request.Request):
165    @extend_schema(
166        request=DeviceFacts(),
167        responses={204: OpenApiResponse(description="Successfully checked in")},
168    )
169    @action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
170    def check_in(self, request: Request):
171        token: DeviceToken = request.auth
172        data = DeviceFacts(data=request.data)
173        data.is_valid(raise_exception=True)
174        connection: AgentDeviceConnection = token.device
175        connection.create_snapshot(data.validated_data)
176        return Response(status=204)
@extend_schema(request=OpenApiTypes.NONE, parameters=[OpenApiParameter('device', OpenApiTypes.STR, location='query', required=True)], responses={200: AgentTokenResponseSerializer(), 404: OpenApiResponse(description='Device not found')})
@action(methods=['POST'], detail=False, pagination_class=None, filter_backends=[], permission_classes=[IsAuthenticated], authentication_classes=[DeviceAuthFedAuthentication])
def auth_fed( self, request: rest_framework.request.Request) -> rest_framework.response.Response:
178    @extend_schema(
179        request=OpenApiTypes.NONE,
180        parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)],
181        responses={
182            200: AgentTokenResponseSerializer(),
183            404: OpenApiResponse(description="Device not found"),
184        },
185    )
186    @action(
187        methods=["POST"],
188        detail=False,
189        pagination_class=None,
190        filter_backends=[],
191        permission_classes=[IsAuthenticated],
192        authentication_classes=[DeviceAuthFedAuthentication],
193    )
194    def auth_fed(self, request: Request) -> Response:
195        federated_token, device, connector = request.auth
196
197        policy_result = check_device_policies(device, federated_token.user, request._request)
198        if not policy_result.passing:
199            raise ValidationError(
200                {"policy_result": "Policy denied access", "policy_messages": policy_result.messages}
201            )
202
203        token, exp = agent_auth_issue_token(device, connector, federated_token.user)
204        rel_exp = int((exp - now()).total_seconds())
205        Event.new(
206            EventAction.LOGIN,
207            **{
208                PLAN_CONTEXT_METHOD: "jwt",
209                PLAN_CONTEXT_METHOD_ARGS: {
210                    "jwt": federated_token,
211                    "provider": federated_token.provider,
212                },
213                PLAN_CONTEXT_DEVICE: device,
214            },
215        ).from_http(request, user=federated_token.user)
216        return Response({"token": token, "expires_in": rel_exp})
name = None
description = None
suffix = None
detail = None
basename = None