mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
endpoints/stage: v2, better error handling, more settings (#18545)
* add options, idle fallback Signed-off-by: Jens Langhammer <jens@goauthentik.io> * delete other device tokens during enroll Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better error handling Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@@ -50,6 +50,8 @@ class AgentConnectorSerializer(ConnectorSerializer):
|
||||
"nss_uid_offset",
|
||||
"nss_gid_offset",
|
||||
"challenge_key",
|
||||
"challenge_idle_timeout",
|
||||
"challenge_trigger_check_in",
|
||||
"jwt_federation_providers",
|
||||
]
|
||||
|
||||
@@ -133,6 +135,7 @@ class AgentConnectorViewSet(
|
||||
device=device,
|
||||
connector=token.connector,
|
||||
)
|
||||
DeviceToken.objects.filter(device=connection).delete()
|
||||
token = DeviceToken.objects.create(device=connection, expiring=False)
|
||||
return Response(
|
||||
{
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-02 20:31
|
||||
|
||||
import authentik.lib.utils.time
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_endpoints_connectors_agent",
|
||||
"0003_agentconnector_auth_session_duration_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="agentconnector",
|
||||
name="challenge_idle_timeout",
|
||||
field=models.TextField(
|
||||
default="seconds=5",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="agentconnector",
|
||||
name="challenge_trigger_check_in",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -46,6 +46,10 @@ class AgentConnector(Connector):
|
||||
nss_gid_offset = models.PositiveIntegerField(default=1000)
|
||||
|
||||
challenge_key = models.ForeignKey(CertificateKeyPair, on_delete=models.CASCADE, null=True)
|
||||
challenge_idle_timeout = models.TextField(
|
||||
validators=[timedelta_string_validator], default="seconds=5"
|
||||
)
|
||||
challenge_trigger_check_in = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.http import HttpResponse
|
||||
from django.utils.timezone import now
|
||||
from jwt import PyJWTError, decode, encode
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.endpoints.connectors.agent.models import DeviceToken
|
||||
@@ -17,6 +17,7 @@ from authentik.flows.challenge import (
|
||||
from authentik.flows.planner import PLAN_CONTEXT_DEVICE
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.models import JWTAlgorithms
|
||||
|
||||
PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE = "goauthentik.io/endpoints/connectors/agent/challenge"
|
||||
@@ -29,6 +30,7 @@ class EndpointAgentChallenge(Challenge):
|
||||
|
||||
component = CharField(default="ak-stage-endpoint-agent")
|
||||
challenge = CharField()
|
||||
challenge_idle_timeout = IntegerField()
|
||||
|
||||
|
||||
class EndpointAgentChallengeResponse(ChallengeResponse):
|
||||
@@ -40,20 +42,24 @@ class EndpointAgentChallengeResponse(ChallengeResponse):
|
||||
def validate_response(self, response: str | None) -> Device | None:
|
||||
if not response:
|
||||
return None
|
||||
raw = decode(
|
||||
response,
|
||||
options={"verify_signature": False},
|
||||
audience="goauthentik.io/platform/endpoint",
|
||||
)
|
||||
try:
|
||||
raw = decode(
|
||||
response,
|
||||
options={"verify_signature": False},
|
||||
audience="goauthentik.io/platform/endpoint",
|
||||
)
|
||||
except PyJWTError as exc:
|
||||
self.stage.logger.warning("Could not parse response", exc=exc)
|
||||
raise ValidationError("Invalid challenge response") from None
|
||||
device = Device.filter_not_expired(identifier=raw["iss"]).first()
|
||||
if not device:
|
||||
self.stage.logger.warning("Could not find device for challenge")
|
||||
raise ValidationError("Invalid challenge response")
|
||||
try:
|
||||
for token in DeviceToken.filter_not_expired(
|
||||
device__device=device,
|
||||
device__connector=self.stage.executor.current_stage.connector,
|
||||
).values_list("key", flat=True):
|
||||
for token in DeviceToken.filter_not_expired(
|
||||
device__device=device,
|
||||
device__connector=self.stage.executor.current_stage.connector,
|
||||
).values_list("key", flat=True):
|
||||
try:
|
||||
decoded = decode(
|
||||
response,
|
||||
key=token,
|
||||
@@ -68,9 +74,9 @@ class EndpointAgentChallengeResponse(ChallengeResponse):
|
||||
self.stage.logger.warning("mismatched challenge")
|
||||
raise ValidationError("Invalid challenge response")
|
||||
return device
|
||||
except PyJWTError as exc:
|
||||
self.stage.logger.warning("failed to validate device challenge response", exc=exc)
|
||||
raise ValidationError("Invalid challenge response") from None
|
||||
except PyJWTError as exc:
|
||||
self.stage.logger.warning("failed to validate device challenge response", exc=exc)
|
||||
raise ValidationError("Invalid challenge response")
|
||||
|
||||
|
||||
class AuthenticatorEndpointStageView(ChallengeStageView):
|
||||
@@ -96,6 +102,7 @@ class AuthenticatorEndpointStageView(ChallengeStageView):
|
||||
"iss": str(stage.pk),
|
||||
"iat": int(iat.timestamp()),
|
||||
"exp": int((iat + timedelta(minutes=5)).timestamp()),
|
||||
"goauthentik.io/device/check_in": stage.connector.challenge_trigger_check_in,
|
||||
},
|
||||
headers={"kid": keypair.kid},
|
||||
key=keypair.private_key,
|
||||
@@ -106,13 +113,23 @@ class AuthenticatorEndpointStageView(ChallengeStageView):
|
||||
data={
|
||||
"component": "ak-stage-endpoint-agent",
|
||||
"challenge": challenge,
|
||||
"challenge_idle_timeout": int(
|
||||
timedelta_from_string(stage.connector.challenge_idle_timeout).total_seconds()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def challenge_invalid(self, response: EndpointAgentChallengeResponse) -> HttpResponse:
|
||||
if self.executor.current_stage.mode == StageMode.OPTIONAL:
|
||||
return self.executor.stage_ok()
|
||||
return super().challenge_invalid(response)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
self.executor.plan.context.pop(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, None)
|
||||
if device := response.validated_data.get("response"):
|
||||
self.executor.plan.context[PLAN_CONTEXT_DEVICE] = device
|
||||
elif self.executor.current_stage.mode == StageMode.REQUIRED:
|
||||
return self.executor.stage_invalid("Invalid challenge response")
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def cleanup(self):
|
||||
self.executor.plan.context.pop(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, None)
|
||||
|
||||
@@ -58,6 +58,16 @@ class TestAgentAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_enroll_token_delete(self):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-enroll"),
|
||||
data={"device_serial": self.device.identifier, "device_name": "bar"},
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(DeviceToken.objects.filter(pk=self.device_token.pk).exists())
|
||||
self.assertEqual(DeviceToken.objects.filter(device=self.connection).count(), 1)
|
||||
|
||||
def test_enroll_group(self):
|
||||
device_group = DeviceAccessGroup.objects.create(name=generate_id())
|
||||
self.token.device_group = device_group
|
||||
|
||||
@@ -49,6 +49,11 @@ class TestEndpointStage(FlowTestCase):
|
||||
|
||||
challenge = loads(res.content.decode())["challenge"]
|
||||
|
||||
DeviceToken.objects.create(
|
||||
device=self.connection,
|
||||
key=generate_id(),
|
||||
)
|
||||
|
||||
response = encode(
|
||||
{
|
||||
"iss": self.device.identifier,
|
||||
@@ -123,6 +128,27 @@ class TestEndpointStage(FlowTestCase):
|
||||
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
|
||||
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)
|
||||
|
||||
def test_endpoint_stage_optional_invalid(self):
|
||||
flow = create_test_flow()
|
||||
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.OPTIONAL)
|
||||
FlowStageBinding.objects.create(stage=stage, target=flow, order=0)
|
||||
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageResponse(res, flow=flow, component="ak-stage-endpoint-agent")
|
||||
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
data={"component": "ak-stage-endpoint-agent", "response": generate_id()},
|
||||
)
|
||||
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
|
||||
plan = plan()
|
||||
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
|
||||
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)
|
||||
|
||||
def test_endpoint_stage_required_none(self):
|
||||
flow = create_test_flow()
|
||||
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.REQUIRED)
|
||||
@@ -144,3 +170,27 @@ class TestEndpointStage(FlowTestCase):
|
||||
component="ak-stage-access-denied",
|
||||
error_message="Invalid challenge response",
|
||||
)
|
||||
|
||||
def test_endpoint_stage_required_invalid(self):
|
||||
flow = create_test_flow()
|
||||
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.REQUIRED)
|
||||
FlowStageBinding.objects.create(stage=stage, target=flow, order=0)
|
||||
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageResponse(res, flow=flow, component="ak-stage-endpoint-agent")
|
||||
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
data={"component": "ak-stage-endpoint-agent", "response": generate_id()},
|
||||
)
|
||||
self.assertStageResponse(
|
||||
res,
|
||||
flow=flow,
|
||||
component="ak-stage-endpoint-agent",
|
||||
response_errors={
|
||||
"response": [{"string": "Invalid challenge response", "code": "invalid"}]
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,10 +6,15 @@ PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
|
||||
|
||||
class EndpointStageView(StageView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
def _get_inner(self):
|
||||
stage: EndpointStage = self.executor.current_stage
|
||||
inner_stage: type[StageView] | None = stage.connector.stage
|
||||
if not inner_stage:
|
||||
return self.executor.stage_ok()
|
||||
view = inner_stage(self.executor, request=self.request)
|
||||
return view.dispatch(request, *args, **kwargs)
|
||||
return inner_stage(self.executor, request=self.request)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return self._get_inner().dispatch(request, *args, **kwargs)
|
||||
|
||||
def cleanup(self):
|
||||
return self._get_inner().cleanup()
|
||||
|
||||
@@ -6023,6 +6023,15 @@
|
||||
"format": "uuid",
|
||||
"title": "Challenge key"
|
||||
},
|
||||
"challenge_idle_timeout": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Challenge idle timeout"
|
||||
},
|
||||
"challenge_trigger_check_in": {
|
||||
"type": "boolean",
|
||||
"title": "Challenge trigger check in"
|
||||
},
|
||||
"jwt_federation_providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
+17
@@ -32804,6 +32804,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
challenge_idle_timeout:
|
||||
type: string
|
||||
challenge_trigger_check_in:
|
||||
type: boolean
|
||||
jwt_federation_providers:
|
||||
type: array
|
||||
items:
|
||||
@@ -32852,6 +32856,11 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
challenge_idle_timeout:
|
||||
type: string
|
||||
minLength: 1
|
||||
challenge_trigger_check_in:
|
||||
type: boolean
|
||||
jwt_federation_providers:
|
||||
type: array
|
||||
items:
|
||||
@@ -36779,8 +36788,11 @@ components:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
challenge:
|
||||
type: string
|
||||
challenge_idle_timeout:
|
||||
type: integer
|
||||
required:
|
||||
- challenge
|
||||
- challenge_idle_timeout
|
||||
EndpointAgentChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response to signed challenge
|
||||
@@ -45471,6 +45483,11 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
challenge_idle_timeout:
|
||||
type: string
|
||||
minLength: 1
|
||||
challenge_trigger_check_in:
|
||||
type: boolean
|
||||
jwt_federation_providers:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -37,8 +37,8 @@ export class AgentConnectorForm extends WithBrandConfig(ModelForm<AgentConnector
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated connector.")
|
||||
: msg("Successfully created connector.");
|
||||
? msg("Successfully updated agent connector.")
|
||||
: msg("Successfully created agent connector.");
|
||||
}
|
||||
|
||||
async send(data: AgentConnector): Promise<AgentConnector> {
|
||||
@@ -61,6 +61,18 @@ export class AgentConnectorForm extends WithBrandConfig(ModelForm<AgentConnector
|
||||
value=${ifDefined(this.instance?.name)}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="refreshInterval"
|
||||
label=${msg("Refresh interval")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.refreshInterval ?? "minutes=30")}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Interval how frequently the agent tries to update its config.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal name="enabled">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
@@ -76,73 +88,103 @@ export class AgentConnectorForm extends WithBrandConfig(ModelForm<AgentConnector
|
||||
<span class="pf-c-switch__label">${msg("Enabled")}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
label=${msg("Authorization flow")}
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authorize.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="challengeKey">
|
||||
<ak-crypto-certificate-search
|
||||
label=${msg("Certificate")}
|
||||
placeholder=${msg("Select a certificate...")}
|
||||
certificate=${ifPresent(this.instance?.challengeKey)}
|
||||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Certificate used for signing device compliance challenges.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="authSessionDuration"
|
||||
label=${msg("Session duration")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.authSessionDuration ?? "hours=8")}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long an authenticated session is valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal name="authTerminateSessionOnExpiry">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.authTerminateSessionOnExpiry ?? true}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Terminate authenticated sessions on token expiry")}</span
|
||||
<ak-form-group label="${msg("Authentication settings")}">
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="refreshInterval"
|
||||
label=${msg("Refresh interval")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.refreshInterval ?? "minutes=30")}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Interval how frequently the agent tries to update its config.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-group open label="${msg("Unix settings")}">
|
||||
<ak-flow-search
|
||||
label=${msg("Authorization flow")}
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authorize.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="authSessionDuration"
|
||||
label=${msg("Session duration")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.authSessionDuration ?? "hours=8")}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long an authenticated session is valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal name="authTerminateSessionOnExpiry">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.authTerminateSessionOnExpiry ?? true}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Terminate authenticated sessions on token expiry")}</span
|
||||
>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label="${msg("Device compliance settings")}">
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Challenge certificate")}
|
||||
name="challengeKey"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
label=${msg("Certificate")}
|
||||
placeholder=${msg("Select a certificate...")}
|
||||
certificate=${ifPresent(this.instance?.challengeKey)}
|
||||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Certificate used for signing device compliance challenges.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="challengeIdleTimeout"
|
||||
label=${msg("Challenge idle timeout")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.challengeIdleTimeout ?? "seconds=3")}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Duration the flow executor will wait before continuing without a response.",
|
||||
)}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal name="challengeTriggerCheckIn">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.challengeTriggerCheckIn ?? true}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Trigger check-in on device")}</span
|
||||
>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label="${msg("Unix settings")}">
|
||||
<div class="pf-c-form">
|
||||
<ak-number-input
|
||||
label=${msg("NSS User ID offset")}
|
||||
|
||||
@@ -375,7 +375,7 @@ export class FlowExecutor
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-user-login>`;
|
||||
case "ak-stage-endpoint-agent":
|
||||
await import("#flow/stages/endpoint_agent/EndpointAgentStage");
|
||||
await import("#flow/stages/endpoint/agent/EndpointAgentStage");
|
||||
return html`<ak-stage-endpoint-agent
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
|
||||
+11
@@ -36,6 +36,17 @@ export class EndpointAgentStage extends BaseStage<
|
||||
);
|
||||
}
|
||||
});
|
||||
// Fallback in case we don't get a response
|
||||
setTimeout(() => {
|
||||
this.host?.submit(
|
||||
{
|
||||
response: null,
|
||||
} as EndpointAgentChallengeResponseRequest,
|
||||
{
|
||||
invisible: true,
|
||||
},
|
||||
);
|
||||
}, this.challenge.challengeIdleTimeout * 1000);
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
Reference in New Issue
Block a user