authentik.enterprise.endpoints.connectors.agent.views.auth_interactive

  1from hashlib import sha256
  2from hmac import compare_digest
  3
  4from django.http import Http404, HttpRequest, HttpResponse, HttpResponseBadRequest, QueryDict
  5
  6from authentik.common.oauth.constants import QS_LOGIN_HINT
  7from authentik.endpoints.connectors.agent.auth import (
  8    agent_auth_issue_token,
  9    check_device_policies,
 10)
 11from authentik.endpoints.connectors.agent.models import AgentConnector, DeviceAuthenticationToken
 12from authentik.endpoints.connectors.agent.stage import PLAN_CONTEXT_DEVICE_AUTH_TOKEN
 13from authentik.endpoints.models import Device
 14from authentik.enterprise.policy import EnterprisePolicyAccessView
 15from authentik.flows.exceptions import FlowNonApplicableException
 16from authentik.flows.models import in_memory_stage
 17from authentik.flows.planner import PLAN_CONTEXT_DEVICE, FlowPlanner
 18from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
 19from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
 20
 21QS_AGENT_IA_TOKEN = "ak-auth-ia-token"  # nosec
 22
 23
 24class AgentInteractiveAuth(EnterprisePolicyAccessView):
 25    """Agent device authentication"""
 26
 27    auth_token: DeviceAuthenticationToken
 28    device: Device
 29    connector: AgentConnector
 30
 31    def resolve_provider_application(self):
 32        auth_token = (
 33            DeviceAuthenticationToken.objects.filter(identifier=self.kwargs["token_uuid"])
 34            .prefetch_related()
 35            .first()
 36        )
 37        if not auth_token:
 38            raise Http404
 39        self.auth_token = auth_token
 40        self.device = auth_token.device
 41        self.connector = auth_token.connector.agentconnector
 42
 43    def user_has_access(self, user=None, pbm=None):
 44        enterprise_result = self.check_license()
 45        if not enterprise_result.passing:
 46            return enterprise_result
 47        return check_device_policies(self.device, user or self.request.user, self.request)
 48
 49    def modify_flow_context(self, flow, context):
 50        return {
 51            PLAN_CONTEXT_DEVICE: self.device,
 52            **context,
 53        }
 54
 55    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 56        device_token_hash = request.headers.get("X-Authentik-Platform-Auth-DTH")
 57        if not device_token_hash:
 58            return HttpResponseBadRequest("Invalid device token")
 59        if not compare_digest(
 60            device_token_hash, sha256(self.auth_token.device_token.key.encode()).hexdigest()
 61        ):
 62            return HttpResponseBadRequest("Invalid device token")
 63        if not self.connector.authorization_flow:
 64            return HttpResponseBadRequest("No authorization flow configured")
 65
 66        planner = FlowPlanner(self.connector.authorization_flow)
 67        planner.allow_empty_flows = True
 68        context = {
 69            PLAN_CONTEXT_DEVICE: self.device,
 70            PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
 71        }
 72        if QS_LOGIN_HINT in request.GET:
 73            context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = request.GET[QS_LOGIN_HINT]
 74        try:
 75            plan = planner.plan(self.request, context)
 76        except FlowNonApplicableException:
 77            return self.handle_no_permission_authenticated()
 78        plan.append_stage(in_memory_stage(AgentAuthFulfillmentStage))
 79
 80        return plan.to_redirect(
 81            self.request,
 82            self.connector.authorization_flow,
 83            allowed_silent_types=[AgentAuthFulfillmentStage],
 84        )
 85
 86
 87class AgentAuthFulfillmentStage(StageView):
 88    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 89        device: Device = self.executor.plan.context.pop(PLAN_CONTEXT_DEVICE)
 90        auth_token: DeviceAuthenticationToken = self.executor.plan.context.pop(
 91            PLAN_CONTEXT_DEVICE_AUTH_TOKEN
 92        )
 93
 94        token, exp = agent_auth_issue_token(
 95            device,
 96            auth_token.connector.agentconnector,
 97            request.user,
 98            jti=str(auth_token.identifier),
 99        )
100        if not token or not exp:
101            return self.executor.stage_invalid("Failed to generate token")
102        auth_token.user = request.user
103        auth_token.token = token
104        auth_token.expires = exp
105        auth_token.expiring = True
106        auth_token.save()
107        qd = QueryDict(mutable=True)
108        qd[QS_AGENT_IA_TOKEN] = token
109        return HttpResponseRedirectScheme(
110            "goauthentik.io://platform/finished?" + qd.urlencode(),
111            allowed_schemes=["goauthentik.io"],
112        )
QS_AGENT_IA_TOKEN = 'ak-auth-ia-token'
class AgentInteractiveAuth(authentik.enterprise.policy.EnterprisePolicyAccessView):
25class AgentInteractiveAuth(EnterprisePolicyAccessView):
26    """Agent device authentication"""
27
28    auth_token: DeviceAuthenticationToken
29    device: Device
30    connector: AgentConnector
31
32    def resolve_provider_application(self):
33        auth_token = (
34            DeviceAuthenticationToken.objects.filter(identifier=self.kwargs["token_uuid"])
35            .prefetch_related()
36            .first()
37        )
38        if not auth_token:
39            raise Http404
40        self.auth_token = auth_token
41        self.device = auth_token.device
42        self.connector = auth_token.connector.agentconnector
43
44    def user_has_access(self, user=None, pbm=None):
45        enterprise_result = self.check_license()
46        if not enterprise_result.passing:
47            return enterprise_result
48        return check_device_policies(self.device, user or self.request.user, self.request)
49
50    def modify_flow_context(self, flow, context):
51        return {
52            PLAN_CONTEXT_DEVICE: self.device,
53            **context,
54        }
55
56    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
57        device_token_hash = request.headers.get("X-Authentik-Platform-Auth-DTH")
58        if not device_token_hash:
59            return HttpResponseBadRequest("Invalid device token")
60        if not compare_digest(
61            device_token_hash, sha256(self.auth_token.device_token.key.encode()).hexdigest()
62        ):
63            return HttpResponseBadRequest("Invalid device token")
64        if not self.connector.authorization_flow:
65            return HttpResponseBadRequest("No authorization flow configured")
66
67        planner = FlowPlanner(self.connector.authorization_flow)
68        planner.allow_empty_flows = True
69        context = {
70            PLAN_CONTEXT_DEVICE: self.device,
71            PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
72        }
73        if QS_LOGIN_HINT in request.GET:
74            context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = request.GET[QS_LOGIN_HINT]
75        try:
76            plan = planner.plan(self.request, context)
77        except FlowNonApplicableException:
78            return self.handle_no_permission_authenticated()
79        plan.append_stage(in_memory_stage(AgentAuthFulfillmentStage))
80
81        return plan.to_redirect(
82            self.request,
83            self.connector.authorization_flow,
84            allowed_silent_types=[AgentAuthFulfillmentStage],
85        )

Agent device authentication

def resolve_provider_application(self):
32    def resolve_provider_application(self):
33        auth_token = (
34            DeviceAuthenticationToken.objects.filter(identifier=self.kwargs["token_uuid"])
35            .prefetch_related()
36            .first()
37        )
38        if not auth_token:
39            raise Http404
40        self.auth_token = auth_token
41        self.device = auth_token.device
42        self.connector = auth_token.connector.agentconnector

Resolve self.provider and self.application. *.DoesNotExist Exceptions cause a normal AccessDenied view to be shown. An Http404 exception is not caught, and will return directly

def user_has_access(self, user=None, pbm=None):
44    def user_has_access(self, user=None, pbm=None):
45        enterprise_result = self.check_license()
46        if not enterprise_result.passing:
47            return enterprise_result
48        return check_device_policies(self.device, user or self.request.user, self.request)

Check if user has access to application.

def modify_flow_context(self, flow, context):
50    def modify_flow_context(self, flow, context):
51        return {
52            PLAN_CONTEXT_DEVICE: self.device,
53            **context,
54        }

optionally modify the flow context which is used for the authentication flow

def get( self, request: django.http.request.HttpRequest, *args, **kwargs) -> django.http.response.HttpResponse:
56    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
57        device_token_hash = request.headers.get("X-Authentik-Platform-Auth-DTH")
58        if not device_token_hash:
59            return HttpResponseBadRequest("Invalid device token")
60        if not compare_digest(
61            device_token_hash, sha256(self.auth_token.device_token.key.encode()).hexdigest()
62        ):
63            return HttpResponseBadRequest("Invalid device token")
64        if not self.connector.authorization_flow:
65            return HttpResponseBadRequest("No authorization flow configured")
66
67        planner = FlowPlanner(self.connector.authorization_flow)
68        planner.allow_empty_flows = True
69        context = {
70            PLAN_CONTEXT_DEVICE: self.device,
71            PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
72        }
73        if QS_LOGIN_HINT in request.GET:
74            context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = request.GET[QS_LOGIN_HINT]
75        try:
76            plan = planner.plan(self.request, context)
77        except FlowNonApplicableException:
78            return self.handle_no_permission_authenticated()
79        plan.append_stage(in_memory_stage(AgentAuthFulfillmentStage))
80
81        return plan.to_redirect(
82            self.request,
83            self.connector.authorization_flow,
84            allowed_silent_types=[AgentAuthFulfillmentStage],
85        )
class AgentAuthFulfillmentStage(authentik.flows.stage.StageView):
 88class AgentAuthFulfillmentStage(StageView):
 89    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 90        device: Device = self.executor.plan.context.pop(PLAN_CONTEXT_DEVICE)
 91        auth_token: DeviceAuthenticationToken = self.executor.plan.context.pop(
 92            PLAN_CONTEXT_DEVICE_AUTH_TOKEN
 93        )
 94
 95        token, exp = agent_auth_issue_token(
 96            device,
 97            auth_token.connector.agentconnector,
 98            request.user,
 99            jti=str(auth_token.identifier),
100        )
101        if not token or not exp:
102            return self.executor.stage_invalid("Failed to generate token")
103        auth_token.user = request.user
104        auth_token.token = token
105        auth_token.expires = exp
106        auth_token.expiring = True
107        auth_token.save()
108        qd = QueryDict(mutable=True)
109        qd[QS_AGENT_IA_TOKEN] = token
110        return HttpResponseRedirectScheme(
111            "goauthentik.io://platform/finished?" + qd.urlencode(),
112            allowed_schemes=["goauthentik.io"],
113        )

Abstract Stage

def get( self, request: django.http.request.HttpRequest, *args, **kwargs) -> django.http.response.HttpResponse:
 89    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 90        device: Device = self.executor.plan.context.pop(PLAN_CONTEXT_DEVICE)
 91        auth_token: DeviceAuthenticationToken = self.executor.plan.context.pop(
 92            PLAN_CONTEXT_DEVICE_AUTH_TOKEN
 93        )
 94
 95        token, exp = agent_auth_issue_token(
 96            device,
 97            auth_token.connector.agentconnector,
 98            request.user,
 99            jti=str(auth_token.identifier),
100        )
101        if not token or not exp:
102            return self.executor.stage_invalid("Failed to generate token")
103        auth_token.user = request.user
104        auth_token.token = token
105        auth_token.expires = exp
106        auth_token.expiring = True
107        auth_token.save()
108        qd = QueryDict(mutable=True)
109        qd[QS_AGENT_IA_TOKEN] = token
110        return HttpResponseRedirectScheme(
111            "goauthentik.io://platform/finished?" + qd.urlencode(),
112            allowed_schemes=["goauthentik.io"],
113        )