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

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):
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        ]
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):
65class MDMConfigSerializer(PassiveSerializer):
66    platform = ChoiceField(choices=OSFamily.choices)
67    enrollment_token = PrimaryKeyRelatedField(
68        queryset=EnrollmentToken.objects.including_expired().all()
69    )
70
71    def validate_platform(self, platform: OSFamily) -> OSFamily:
72        if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
73            raise ValidationError(_("Selected platform not supported"))
74        return platform
75
76    def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken:
77        if token.is_expired:
78            raise ValidationError(_("Token is expired"))
79        if token.connector != self.context["connector"]:
80            raise ValidationError(_("Invalid token for connector"))
81        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:
71    def validate_platform(self, platform: OSFamily) -> OSFamily:
72        if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
73            raise ValidationError(_("Selected platform not supported"))
74        return platform
76    def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken:
77        if token.is_expired:
78            raise ValidationError(_("Token is expired"))
79        if token.connector != self.context["connector"]:
80            raise ValidationError(_("Invalid token for connector"))
81        return token
 84class AgentConnectorViewSet(
 85    ConditionalInheritance(
 86        "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin"
 87    ),
 88    UsedByMixin,
 89    ModelViewSet,
 90):
 91    queryset = AgentConnector.objects.all()
 92    serializer_class = AgentConnectorSerializer
 93    search_fields = ["name"]
 94    ordering = ["name"]
 95    filterset_fields = ["name", "enabled"]
 96
 97    @extend_schema(
 98        request=MDMConfigSerializer(),
 99        responses=MDMConfigResponseSerializer(),
100    )
101    @action(methods=["POST"], detail=True)
102    def mdm_config(self, request: Request, pk) -> Response:
103        """Generate configuration for MDM systems to deploy authentik Agent"""
104        connector = cast(AgentConnector, self.get_object())
105        data = MDMConfigSerializer(data=request.data, context={"connector": connector})
106        data.is_valid(raise_exception=True)
107        token = data.validated_data["enrollment_token"]
108        if not request.user.has_perm("view_enrollment_token_key", token):
109            raise PermissionDenied()
110        ctrl = connector.controller(connector)
111        payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
112        return Response(payload.validated_data)
113
114    @extend_schema(
115        request=EnrollSerializer(),
116        responses={200: AgentTokenResponseSerializer},
117    )
118    @action(
119        methods=["POST"],
120        detail=False,
121        authentication_classes=[AgentEnrollmentAuth],
122        # Permissions are handled via AgentEnrollmentAuth
123        permission_classes=[AllowAny],
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(
155        methods=["GET"],
156        detail=False,
157        authentication_classes=[AgentAuth],
158        # Permissions are handled via AgentAuth
159        permission_classes=[AllowAny],
160    )
161    def agent_config(self, request: Request):
162        token: DeviceToken = request.auth
163        connector: AgentConnector = token.device.connector.agentconnector
164        return Response(
165            AgentConfigSerializer(
166                connector, context={"request": request, "device": token.device.device}
167            ).data
168        )
169
170    @extend_schema(
171        request=DeviceFacts(),
172        responses={204: OpenApiResponse(description="Successfully checked in")},
173    )
174    @action(
175        methods=["POST"],
176        detail=False,
177        authentication_classes=[AgentAuth],
178        # Permissions are handled via AgentAuth
179        permission_classes=[AllowAny],
180    )
181    def check_in(self, request: Request):
182        token: DeviceToken = request.auth
183        data = DeviceFacts(data=request.data)
184        data.is_valid(raise_exception=True)
185        connection: AgentDeviceConnection = token.device
186        connection.create_snapshot(data.validated_data)
187        return Response(status=204)
188
189    @extend_schema(
190        request=OpenApiTypes.NONE,
191        parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)],
192        responses={
193            200: AgentTokenResponseSerializer(),
194            404: OpenApiResponse(description="Device not found"),
195        },
196    )
197    @action(
198        methods=["POST"],
199        detail=False,
200        pagination_class=None,
201        filter_backends=[],
202        permission_classes=[IsAuthenticated],
203        authentication_classes=[DeviceAuthFedAuthentication],
204    )
205    def auth_fed(self, request: Request) -> Response:
206        federated_token, device, connector = request.auth
207
208        policy_result = check_device_policies(device, federated_token.user, request._request)
209        if not policy_result.passing:
210            raise ValidationError(
211                {"policy_result": "Policy denied access", "policy_messages": policy_result.messages}
212            )
213
214        token, exp = agent_auth_issue_token(device, connector, federated_token.user)
215        rel_exp = int((exp - now()).total_seconds())
216        Event.new(
217            EventAction.LOGIN,
218            **{
219                PLAN_CONTEXT_METHOD: "jwt",
220                PLAN_CONTEXT_METHOD_ARGS: {
221                    "jwt": federated_token,
222                    "provider": federated_token.provider,
223                },
224                PLAN_CONTEXT_DEVICE: device,
225            },
226        ).from_http(request, user=federated_token.user)
227        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:
 97    @extend_schema(
 98        request=MDMConfigSerializer(),
 99        responses=MDMConfigResponseSerializer(),
100    )
101    @action(methods=["POST"], detail=True)
102    def mdm_config(self, request: Request, pk) -> Response:
103        """Generate configuration for MDM systems to deploy authentik Agent"""
104        connector = cast(AgentConnector, self.get_object())
105        data = MDMConfigSerializer(data=request.data, context={"connector": connector})
106        data.is_valid(raise_exception=True)
107        token = data.validated_data["enrollment_token"]
108        if not request.user.has_perm("view_enrollment_token_key", token):
109            raise PermissionDenied()
110        ctrl = connector.controller(connector)
111        payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
112        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], permission_classes=[AllowAny])
def enroll(self, request: rest_framework.request.Request):
114    @extend_schema(
115        request=EnrollSerializer(),
116        responses={200: AgentTokenResponseSerializer},
117    )
118    @action(
119        methods=["POST"],
120        detail=False,
121        authentication_classes=[AgentEnrollmentAuth],
122        # Permissions are handled via AgentEnrollmentAuth
123        permission_classes=[AllowAny],
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        )
@extend_schema(request=OpenApiTypes.NONE, responses=AgentConfigSerializer())
@action(methods=['GET'], detail=False, authentication_classes=[AgentAuth], permission_classes=[AllowAny])
def agent_config(self, request: rest_framework.request.Request):
150    @extend_schema(
151        request=OpenApiTypes.NONE,
152        responses=AgentConfigSerializer(),
153    )
154    @action(
155        methods=["GET"],
156        detail=False,
157        authentication_classes=[AgentAuth],
158        # Permissions are handled via AgentAuth
159        permission_classes=[AllowAny],
160    )
161    def agent_config(self, request: Request):
162        token: DeviceToken = request.auth
163        connector: AgentConnector = token.device.connector.agentconnector
164        return Response(
165            AgentConfigSerializer(
166                connector, context={"request": request, "device": token.device.device}
167            ).data
168        )
@extend_schema(request=DeviceFacts(), responses={204: OpenApiResponse(description='Successfully checked in')})
@action(methods=['POST'], detail=False, authentication_classes=[AgentAuth], permission_classes=[AllowAny])
def check_in(self, request: rest_framework.request.Request):
170    @extend_schema(
171        request=DeviceFacts(),
172        responses={204: OpenApiResponse(description="Successfully checked in")},
173    )
174    @action(
175        methods=["POST"],
176        detail=False,
177        authentication_classes=[AgentAuth],
178        # Permissions are handled via AgentAuth
179        permission_classes=[AllowAny],
180    )
181    def check_in(self, request: Request):
182        token: DeviceToken = request.auth
183        data = DeviceFacts(data=request.data)
184        data.is_valid(raise_exception=True)
185        connection: AgentDeviceConnection = token.device
186        connection.create_snapshot(data.validated_data)
187        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:
189    @extend_schema(
190        request=OpenApiTypes.NONE,
191        parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)],
192        responses={
193            200: AgentTokenResponseSerializer(),
194            404: OpenApiResponse(description="Device not found"),
195        },
196    )
197    @action(
198        methods=["POST"],
199        detail=False,
200        pagination_class=None,
201        filter_backends=[],
202        permission_classes=[IsAuthenticated],
203        authentication_classes=[DeviceAuthFedAuthentication],
204    )
205    def auth_fed(self, request: Request) -> Response:
206        federated_token, device, connector = request.auth
207
208        policy_result = check_device_policies(device, federated_token.user, request._request)
209        if not policy_result.passing:
210            raise ValidationError(
211                {"policy_result": "Policy denied access", "policy_messages": policy_result.messages}
212            )
213
214        token, exp = agent_auth_issue_token(device, connector, federated_token.user)
215        rel_exp = int((exp - now()).total_seconds())
216        Event.new(
217            EventAction.LOGIN,
218            **{
219                PLAN_CONTEXT_METHOD: "jwt",
220                PLAN_CONTEXT_METHOD_ARGS: {
221                    "jwt": federated_token,
222                    "provider": federated_token.provider,
223                },
224                PLAN_CONTEXT_DEVICE: device,
225            },
226        ).from_http(request, user=federated_token.user)
227        return Response({"token": token, "expires_in": rel_exp})
name = None
description = None
suffix = None
detail = None
basename = None