Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice, EndpointDeviceConnection
from authentik.events.logs import LogEvent, capture_logs
from authentik.events.models import SystemTask
from authentik.events.utils import cleanse_dict
Expand Down Expand Up @@ -119,6 +120,8 @@ def excluded_models() -> list[type[Model]]:
GoogleWorkspaceProviderGroup,
MicrosoftEntraProviderUser,
MicrosoftEntraProviderGroup,
EndpointDevice,
EndpointDeviceConnection,
)


Expand Down
1 change: 1 addition & 0 deletions authentik/enterprise/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.rac",
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
"authentik.enterprise.stages.source",
]

Expand Down
84 changes: 84 additions & 0 deletions authentik/enterprise/stages/authenticator_endpoint_gdtc/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""AuthenticatorEndpointGDTCStage API Views"""

from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger

from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage,
EndpointDevice,
)
from authentik.flows.api.stages import StageSerializer

LOGGER = get_logger()


class AuthenticatorEndpointGDTCStageSerializer(EnterpriseRequiredMixin, StageSerializer):
"""AuthenticatorEndpointGDTCStage Serializer"""

class Meta:
model = AuthenticatorEndpointGDTCStage
fields = StageSerializer.Meta.fields + [
"configure_flow",
"friendly_name",
"credentials",
]


class AuthenticatorEndpointGDTCStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorEndpointGDTCStage Viewset"""

queryset = AuthenticatorEndpointGDTCStage.objects.all()
serializer_class = AuthenticatorEndpointGDTCStageSerializer
filterset_fields = [
"name",
"configure_flow",
]
search_fields = ["name"]
ordering = ["name"]


class EndpointDeviceSerializer(ModelSerializer):
"""Serializer for Endpoint authenticator devices"""

class Meta:
model = EndpointDevice
fields = ["pk", "name"]
depth = 2


class EndpointDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Viewset for Endpoint authenticator devices"""

queryset = EndpointDevice.objects.all()
serializer_class = EndpointDeviceSerializer
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]


class EndpointAdminDeviceViewSet(ModelViewSet):
"""Viewset for Endpoint authenticator devices (for admins)"""

permission_classes = [IsAdminUser]
queryset = EndpointDevice.objects.all()
serializer_class = EndpointDeviceSerializer
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
13 changes: 13 additions & 0 deletions authentik/enterprise/stages/authenticator_endpoint_gdtc/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""authentik Endpoint app config"""

from authentik.enterprise.apps import EnterpriseConfig


class AuthentikStageAuthenticatorEndpointConfig(EnterpriseConfig):
"""authentik endpoint config"""

name = "authentik.enterprise.stages.authenticator_endpoint_gdtc"
label = "authentik_stages_authenticator_endpoint_gdtc"
verbose_name = "authentik Enterprise.Stages.Authenticator.Endpoint GDTC"
default = True
mountpoint = "endpoint/gdtc/"
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Generated by Django 5.0.9 on 2024-10-14 17:08

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("authentik_flows", "0027_auto_20231028_1424"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="AuthenticatorEndpointGDTCStage",
fields=[
(
"stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_flows.stage",
),
),
("friendly_name", models.TextField(null=True)),
("credentials", models.JSONField()),
(
"configure_flow",
models.ForeignKey(
blank=True,
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_flows.flow",
),
),
],
options={
"verbose_name": "Endpoint Authenticator Google Device Trust Connector Stage",
"verbose_name_plural": "Endpoint Authenticator Google Device Trust Connector Stages",
},
bases=("authentik_flows.stage", models.Model),
),
migrations.CreateModel(
name="EndpointDevice",
fields=[
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"name",
models.CharField(
help_text="The human-readable name of this device.", max_length=64
),
),
(
"confirmed",
models.BooleanField(default=True, help_text="Is this device ready for use?"),
),
("last_used", models.DateTimeField(null=True)),
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
(
"host_identifier",
models.TextField(
help_text="A unique identifier for the endpoint device, usually the device serial number",
unique=True,
),
),
("hostname", models.TextField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"verbose_name": "Endpoint Device",
"verbose_name_plural": "Endpoint Devices",
},
),
migrations.CreateModel(
name="EndpointDeviceConnection",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("attributes", models.JSONField()),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_stages_authenticator_endpoint_gdtc.endpointdevice",
),
),
(
"stage",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
),
),
],
),
]
102 changes: 102 additions & 0 deletions authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Endpoint stage"""

from uuid import uuid4

from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
from google.oauth2.service_account import Credentials
from rest_framework.serializers import BaseSerializer, Serializer

from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.flows.stage import StageView
from authentik.lib.models import SerializerModel
from authentik.stages.authenticator.models import Device


class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage):
"""Setup Google Chrome Device-trust connection"""

credentials = models.JSONField()

def google_credentials(self):
return {
"credentials": Credentials.from_service_account_info(
self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"]
),
}

@property
def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import (
AuthenticatorEndpointGDTCStageSerializer,
)

return AuthenticatorEndpointGDTCStageSerializer

@property
def view(self) -> type[StageView]:
from authentik.enterprise.stages.authenticator_endpoint_gdtc.stage import (
AuthenticatorEndpointStageView,
)

return AuthenticatorEndpointStageView

@property
def component(self) -> str:
return "ak-stage-authenticator-endpoint-gdtc-form"

def ui_user_settings(self) -> UserSettingSerializer | None:
return UserSettingSerializer(
data={
"title": self.friendly_name or str(self._meta.verbose_name),
"component": "ak-user-settings-authenticator-endpoint",
}
)

def __str__(self) -> str:
return f"Endpoint Authenticator Google Device Trust Connector Stage {self.name}"

class Meta:
verbose_name = _("Endpoint Authenticator Google Device Trust Connector Stage")
verbose_name_plural = _("Endpoint Authenticator Google Device Trust Connector Stages")


class EndpointDevice(SerializerModel, Device):
"""Endpoint Device for a single user"""

uuid = models.UUIDField(primary_key=True, default=uuid4)
host_identifier = models.TextField(
unique=True,
help_text="A unique identifier for the endpoint device, usually the device serial number",
)

hostname = models.TextField()

user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)

@property
def serializer(self) -> Serializer:
from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import (
EndpointDeviceSerializer,
)

return EndpointDeviceSerializer

def __str__(self):
return str(self.name) or str(self.user_id)

class Meta:
verbose_name = _("Endpoint Device")
verbose_name_plural = _("Endpoint Devices")


class EndpointDeviceConnection(models.Model):
device = models.ForeignKey(EndpointDevice, on_delete=models.CASCADE)
stage = models.ForeignKey(AuthenticatorEndpointGDTCStage, on_delete=models.CASCADE)

attributes = models.JSONField()

def __str__(self) -> str:
return f"Endpoint device connection {self.device_id} to {self.stage_id}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.http import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
FrameChallenge,
FrameChallengeResponse,
)
from authentik.flows.stage import ChallengeStageView


class AuthenticatorEndpointStageView(ChallengeStageView):
"""Endpoint stage"""

response_class = FrameChallengeResponse

def get_challenge(self, *args, **kwargs) -> Challenge:
return FrameChallenge(
data={
"component": "xak-flow-frame",
"url": self.request.build_absolute_uri(
reverse("authentik_stages_authenticator_endpoint_gdtc:chrome")
),
"loading_overlay": True,
"loading_text": _("Verifying your browser..."),
}
)

def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return self.executor.stage_ok()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<script>
window.parent.postMessage({
message: "submit",
source: "goauthentik.io",
context: "flow-executor"
});
</script>
</html>
Loading