Skip to content

Commit

Permalink
chrome
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Langhammer <[email protected]>
  • Loading branch information
BeryJu committed Jul 23, 2024
1 parent c2957a2 commit 78866da
Show file tree
Hide file tree
Showing 14 changed files with 1,340 additions and 124 deletions.
12 changes: 12 additions & 0 deletions authentik/flows/challenge.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ class AutoSubmitChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-autosubmit")


class FrameChallenge(Challenge):
"""Challenge type to render a frame"""

url = CharField()
component = CharField(default="xak-flow-frame")


class FrameChallengeResponse(ChallengeResponse):

component = CharField(default="xak-flow-frame")


class DataclassEncoder(DjangoJSONEncoder):
"""Convert any dataclass to json"""

Expand Down
1 change: 1 addition & 0 deletions authentik/root/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
SESSION_COOKIE_NAME = "authentik_session"
SESSION_COOKIE_DOMAIN = CONFIG.get("cookie_domain", None)
APPEND_SLASH = False
X_FRAME_OPTIONS = "SAMEORIGIN"

AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
Expand Down
35 changes: 1 addition & 34 deletions authentik/stages/authenticator_endpoint/api.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
"""AuthenticatorEndpointStage API Views"""
from django.http import Http404

from django_filters.rest_framework.backends import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, IntegerField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger
from rest_framework.permissions import AllowAny

from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_endpoint.models import (
Expand Down Expand Up @@ -79,30 +70,6 @@ class EndpointDeviceViewSet(
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]

@action(methods=["POST"], detail=False, permission_classes=[AllowAny])
def osquery_enroll(self, request: Request) -> Response:
# EndpointDevice.objects.create(host_identifier=request.data.get("host_identifier"))
print(request.data)
return Response({"node_key": "test-key", "node_invalid": False})

@action(methods=["POST"], detail=False, permission_classes=[AllowAny])
def osquery_config(self, request: Request) -> Response:
print(request.data)
return Response(
{
"schedule": {
# "macos_kextstat": {"query": "SELECT * FROM kernel_extensions;", "interval": 10},
"foobar": {"query": "select * from uptime;", "interval": 600},
},
"node_invalid": False,
}
)

@action(methods=["POST"], detail=False, permission_classes=[AllowAny])
def osquery_log(self, request: Request) -> Response:
print(request.data)
return Response({"schedule": {}, "node_invalid": False})


class EndpointAdminDeviceViewSet(ModelViewSet):
"""Viewset for Endpoint authenticator devices (for admins)"""
Expand Down
1 change: 1 addition & 0 deletions authentik/stages/authenticator_endpoint/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ class AuthentikStageAuthenticatorEndpointConfig(ManagedAppConfig):
label = "authentik_stages_authenticator_endpoint"
verbose_name = "authentik Stages.Authenticator.Endpoint"
default = True
mountpoint = "authenticators/device_trust/"
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Generated by Django 5.0.1 on 2024-01-27 18:55
# Generated by Django 5.0.6 on 2024-07-12 10:42

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


class Migration(migrations.Migration):

initial = True

dependencies = [
Expand Down
7 changes: 3 additions & 4 deletions authentik/stages/authenticator_endpoint/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
"""Endpoint stage"""
from typing import Optional

from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
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

Expand All @@ -23,7 +22,7 @@ def serializer(self) -> type[BaseSerializer]:
return AuthenticatorEndpointStageSerializer

Check warning on line 22 in authentik/stages/authenticator_endpoint/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/models.py#L22

Added line #L22 was not covered by tests

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

return AuthenticatorEndpointStageView
Expand All @@ -32,7 +31,7 @@ def type(self) -> type[View]:
def component(self) -> str:
return "ak-stage-authenticator-endpoint-form"

Check warning on line 32 in authentik/stages/authenticator_endpoint/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/models.py#L32

Added line #L32 was not covered by tests

def ui_user_settings(self) -> Optional[UserSettingSerializer]:
def ui_user_settings(self) -> UserSettingSerializer | None:
return UserSettingSerializer(

Check warning on line 35 in authentik/stages/authenticator_endpoint/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/models.py#L35

Added line #L35 was not covered by tests
data={
"title": self.friendly_name or str(self._meta.verbose_name),
Expand Down
24 changes: 24 additions & 0 deletions authentik/stages/authenticator_endpoint/stage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
from django.http import HttpResponse
from django.urls import reverse

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(

Check warning on line 19 in authentik/stages/authenticator_endpoint/stage.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/stage.py#L19

Added line #L19 was not covered by tests
data={
"component": "xak-flow-frame",
"url": self.request.build_absolute_uri(
reverse("authentik_stages_authenticator_endpoint:chrome")
),
}
)

def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return self.executor.stage_ok()

Check warning on line 29 in authentik/stages/authenticator_endpoint/stage.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/stage.py#L29

Added line #L29 was not covered by tests
10 changes: 10 additions & 0 deletions authentik/stages/authenticator_endpoint/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"""API URLs"""

from django.urls import path

from authentik.stages.authenticator_endpoint.api import (
AuthenticatorEndpointStageViewSet,
EndpointAdminDeviceViewSet,
EndpointDeviceViewSet,
)
from authentik.stages.authenticator_endpoint.views.google_chrome.dtc import (
GoogleChromeDeviceTrustConnector,
)

urlpatterns = [
path("chrome/", GoogleChromeDeviceTrustConnector.as_view(), name="chrome"),
]

api_urlpatterns = [
("authenticators/endpoint", EndpointDeviceViewSet),
Expand Down
Empty file.
Empty file.
66 changes: 66 additions & 0 deletions authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from json import dumps, loads
from typing import Any

from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS

# Header we get from chrome that initiates verified access
HEADER_DEVICE_TRUST = "X-Device-Trust"
# Header we send to the client with the challenge
HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge"
# Header we get back from the client that we verify with google
HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
# Header value for x-device-trust that initiates the flow
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"


class GoogleChromeDeviceTrustConnector(View):

def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
self.google_client = build(

Check warning on line 27 in authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py#L27

Added line #L27 was not covered by tests
"verifiedaccess",
"v2",
cache_discovery=False,
credentials=Credentials.from_service_account_file("sa.json"),
)

def get(self, request: HttpRequest) -> HttpResponse:
x_device_trust = request.headers.get(HEADER_DEVICE_TRUST)
x_access_challenge_response = request.headers.get(HEADER_ACCESS_CHALLENGE_RESPONSE)
if x_device_trust == "VerifiedAccess" and x_access_challenge_response is None:
challenge = self.google_client.challenge().generate().execute()
res = HttpResponseRedirect(

Check warning on line 39 in authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py#L35-L39

Added lines #L35 - L39 were not covered by tests
self.request.build_absolute_uri(
reverse("authentik_stages_authenticator_endpoint:chrome")
)
)
res[HEADER_ACCESS_CHALLENGE] = dumps(challenge)
return res
if x_access_challenge_response:
response = (

Check warning on line 47 in authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py#L44-L47

Added lines #L44 - L47 were not covered by tests
self.google_client.challenge().verify(body=loads(x_access_challenge_response)).execute()
)
# Remove deprecated string representation of deviceSignals
response.pop("deviceSignal", None)
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
flow_plan.context.setdefault(PLAN_CONTEXT_METHOD, "trusted_endpoint")
flow_plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS].setdefault("endpoints", [])
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS]["endpoints"].append(response)
request.session[SESSION_KEY_PLAN] = flow_plan
return HttpResponse(

Check warning on line 58 in authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py

View check run for this annotation

Codecov / codecov/patch

authentik/stages/authenticator_endpoint/views/google_chrome/dtc.py#L51-L58

Added lines #L51 - L58 were not covered by tests
"""<html><script>
window.parent.postMessage({
message: "submit",
source: "goauthentik.io",
context: "flow-executor"
});
</script></html>""",
)
Loading

0 comments on commit 78866da

Please sign in to comment.