more fixes, start implementing validate
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
154b91cc92
commit
bb8a70448f
|
@ -1,7 +1,6 @@
|
||||||
"""AuthenticatorMobileStage API Views"""
|
"""AuthenticatorMobileStage API Views"""
|
||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiResponse
|
||||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, ChoiceField
|
from rest_framework.fields import CharField, ChoiceField
|
||||||
|
@ -101,7 +100,7 @@ class MobileDeviceViewSet(
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=OpenApiTypes.NONE,
|
request=None,
|
||||||
responses={
|
responses={
|
||||||
200: inline_serializer(
|
200: inline_serializer(
|
||||||
"MobileDeviceEnrollmentStatusSerializer",
|
"MobileDeviceEnrollmentStatusSerializer",
|
||||||
|
@ -128,7 +127,7 @@ class MobileDeviceViewSet(
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
204: OpenApiTypes.STR,
|
204: OpenApiResponse(description="Key successfully set"),
|
||||||
},
|
},
|
||||||
request=MobileDeviceSetPushKeySerializer,
|
request=MobileDeviceSetPushKeySerializer,
|
||||||
)
|
)
|
||||||
|
@ -138,10 +137,10 @@ class MobileDeviceViewSet(
|
||||||
permission_classes=[],
|
permission_classes=[],
|
||||||
authentication_classes=[MobileDeviceTokenAuthentication],
|
authentication_classes=[MobileDeviceTokenAuthentication],
|
||||||
)
|
)
|
||||||
def set_notification_key(self, request: Request) -> Response:
|
def set_notification_key(self, request: Request, pk: str) -> Response:
|
||||||
"""Called by the phone whenever the firebase key changes and we need to update it"""
|
"""Called by the phone whenever the firebase key changes and we need to update it"""
|
||||||
device: MobileDevice = self.get_object()
|
device: MobileDevice = self.get_object()
|
||||||
data = MobileDeviceSetPushKeySerializer(data=request)
|
data = MobileDeviceSetPushKeySerializer(data=request.data)
|
||||||
data.is_valid(raise_exception=True)
|
data.is_valid(raise_exception=True)
|
||||||
device.firebase_token = data.validated_data["firebase_key"]
|
device.firebase_token = data.validated_data["firebase_key"]
|
||||||
device.save()
|
device.save()
|
||||||
|
@ -153,7 +152,7 @@ class MobileDeviceViewSet(
|
||||||
permission_classes=[],
|
permission_classes=[],
|
||||||
authentication_classes=[MobileDeviceTokenAuthentication],
|
authentication_classes=[MobileDeviceTokenAuthentication],
|
||||||
)
|
)
|
||||||
def receive_response(self, request: Request) -> Response:
|
def receive_response(self, request: Request, pk: str) -> Response:
|
||||||
"""Get response from notification on phone"""
|
"""Get response from notification on phone"""
|
||||||
print(request.data)
|
print(request.data)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
"""Mobile authenticator stage"""
|
"""Mobile authenticator stage"""
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from firebase_admin.messaging import Message, send
|
from firebase_admin.messaging import (
|
||||||
|
Message,
|
||||||
|
send,
|
||||||
|
AndroidConfig,
|
||||||
|
AndroidNotification,
|
||||||
|
APNSConfig,
|
||||||
|
APNSPayload,
|
||||||
|
Notification,
|
||||||
|
Aps,
|
||||||
|
)
|
||||||
|
from firebase_admin.exceptions import FirebaseError
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -16,6 +26,13 @@ from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
|
|
||||||
|
from firebase_admin import initialize_app
|
||||||
|
from firebase_admin import credentials
|
||||||
|
|
||||||
|
cred = credentials.Certificate("firebase.json")
|
||||||
|
initialize_app(cred)
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
def default_token_key():
|
def default_token_key():
|
||||||
"""Default token key"""
|
"""Default token key"""
|
||||||
|
@ -78,21 +95,29 @@ class MobileDevice(SerializerModel, Device):
|
||||||
|
|
||||||
return MobileDeviceSerializer
|
return MobileDeviceSerializer
|
||||||
|
|
||||||
def send_message(self):
|
def send_message(self, **context):
|
||||||
# See documentation on defining a message payload.
|
|
||||||
message = Message(
|
message = Message(
|
||||||
data={
|
notification=Notification(
|
||||||
'score': '850',
|
title="$GOOG up 1.43% on the day",
|
||||||
'time': '2:45',
|
body="$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.",
|
||||||
},
|
),
|
||||||
|
android=AndroidConfig(
|
||||||
|
priority="normal",
|
||||||
|
notification=AndroidNotification(icon="stock_ticker_update", color="#f45342"),
|
||||||
|
),
|
||||||
|
apns=APNSConfig(
|
||||||
|
payload=APNSPayload(
|
||||||
|
aps=Aps(badge=0),
|
||||||
|
interruption_level="time-sensitive",
|
||||||
|
),
|
||||||
|
),
|
||||||
token=self.firebase_token,
|
token=self.firebase_token,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
# Send a message to the device corresponding to the provided
|
response = send(message)
|
||||||
# registration token.
|
LOGGER.debug("Sent notification", id=response)
|
||||||
response = send(message)
|
except (ValueError, FirebaseError) as exc:
|
||||||
# Response is a message ID string.
|
LOGGER.warning("failed to push", exc=exc)
|
||||||
print('Successfully sent message:', response)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name) or str(self.user)
|
return str(self.name) or str(self.user)
|
||||||
|
|
|
@ -26,6 +26,7 @@ from authentik.root.middleware import ClientIPMiddleware
|
||||||
from authentik.stages.authenticator import match_token
|
from authentik.stages.authenticator import match_token
|
||||||
from authentik.stages.authenticator.models import Device
|
from authentik.stages.authenticator.models import Device
|
||||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||||
|
from authentik.stages.authenticator_mobile.models import MobileDevice
|
||||||
from authentik.stages.authenticator_sms.models import SMSDevice
|
from authentik.stages.authenticator_sms.models import SMSDevice
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||||
|
@ -176,6 +177,45 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def validate_challenge_mobile(device_pk: str, stage_view: StageView, user: User) -> Device:
|
||||||
|
device: MobileDevice = get_object_or_404(MobileDevice, pk=device_pk)
|
||||||
|
if device.user != user:
|
||||||
|
LOGGER.warning("device mismatch")
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
# Get additional context for push
|
||||||
|
push_context = {
|
||||||
|
__("Domain"): stage_view.request.get_host(),
|
||||||
|
}
|
||||||
|
if SESSION_KEY_APPLICATION_PRE in stage_view.request.session:
|
||||||
|
push_context[__("Application")] = stage_view.request.session.get(
|
||||||
|
SESSION_KEY_APPLICATION_PRE, Application()
|
||||||
|
).name
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = device.send_message(**push_context)
|
||||||
|
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
|
||||||
|
if response["result"] == "deny":
|
||||||
|
LOGGER.debug("mobile push response", result=response)
|
||||||
|
login_failed.send(
|
||||||
|
sender=__name__,
|
||||||
|
credentials={"username": user.username},
|
||||||
|
request=stage_view.request,
|
||||||
|
stage=stage_view.executor.current_stage,
|
||||||
|
device_class=DeviceClasses.MOBILE.value,
|
||||||
|
mobile_response=response,
|
||||||
|
)
|
||||||
|
raise ValidationError("Mobile denied access", code="denied")
|
||||||
|
return device
|
||||||
|
except RuntimeError as exc:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
message=f"Failed to Mobile authenticate user: {str(exc)}",
|
||||||
|
user=user,
|
||||||
|
).from_http(stage_view.request, user)
|
||||||
|
raise ValidationError("Mobile denied access", code="denied")
|
||||||
|
|
||||||
|
|
||||||
def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) -> Device:
|
def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) -> Device:
|
||||||
"""Duo authentication"""
|
"""Duo authentication"""
|
||||||
device = get_object_or_404(DuoDevice, pk=device_pk)
|
device = get_object_or_404(DuoDevice, pk=device_pk)
|
||||||
|
|
|
@ -20,6 +20,7 @@ class DeviceClasses(models.TextChoices):
|
||||||
WEBAUTHN = "webauthn", _("WebAuthn")
|
WEBAUTHN = "webauthn", _("WebAuthn")
|
||||||
DUO = "duo", _("Duo")
|
DUO = "duo", _("Duo")
|
||||||
SMS = "sms", _("SMS")
|
SMS = "sms", _("SMS")
|
||||||
|
MOBILE = "mobile", _("authentik Mobile")
|
||||||
|
|
||||||
|
|
||||||
def default_device_classes() -> list:
|
def default_device_classes() -> list:
|
||||||
|
|
|
@ -29,6 +29,7 @@ from authentik.stages.authenticator_validate.challenge import (
|
||||||
select_challenge,
|
select_challenge,
|
||||||
validate_challenge_code,
|
validate_challenge_code,
|
||||||
validate_challenge_duo,
|
validate_challenge_duo,
|
||||||
|
validate_challenge_mobile,
|
||||||
validate_challenge_webauthn,
|
validate_challenge_webauthn,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
|
@ -70,6 +71,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||||
code = CharField(required=False)
|
code = CharField(required=False)
|
||||||
webauthn = JSONDictField(required=False)
|
webauthn = JSONDictField(required=False)
|
||||||
duo = IntegerField(required=False)
|
duo = IntegerField(required=False)
|
||||||
|
mobile = CharField(required=False)
|
||||||
component = CharField(default="ak-stage-authenticator-validate")
|
component = CharField(default="ak-stage-authenticator-validate")
|
||||||
|
|
||||||
def _challenge_allowed(self, classes: list):
|
def _challenge_allowed(self, classes: list):
|
||||||
|
@ -100,6 +102,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||||
self.device = validate_challenge_duo(duo, self.stage, self.stage.get_pending_user())
|
self.device = validate_challenge_duo(duo, self.stage, self.stage.get_pending_user())
|
||||||
return duo
|
return duo
|
||||||
|
|
||||||
|
def validate_mobile(self, mobile: str) -> str:
|
||||||
|
"""Initiate mobile authentication"""
|
||||||
|
self._challenge_allowed([DeviceClasses.MOBILE])
|
||||||
|
self.device = validate_challenge_mobile(mobile, self.stage, self.stage.get_pending_user())
|
||||||
|
return mobile
|
||||||
|
|
||||||
def validate_selected_challenge(self, challenge: dict) -> dict:
|
def validate_selected_challenge(self, challenge: dict) -> dict:
|
||||||
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
||||||
# First check if the challenge is valid
|
# First check if the challenge is valid
|
||||||
|
@ -134,7 +142,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||||
def validate(self, attrs: dict):
|
def validate(self, attrs: dict):
|
||||||
# Checking if the given data is from a valid device class is done above
|
# Checking if the given data is from a valid device class is done above
|
||||||
# Here we only check if the any data was sent at all
|
# Here we only check if the any data was sent at all
|
||||||
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs:
|
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs and "mobile" not in attrs:
|
||||||
raise ValidationError("Empty response")
|
raise ValidationError("Empty response")
|
||||||
self.stage.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD, "auth_mfa")
|
self.stage.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD, "auth_mfa")
|
||||||
self.stage.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
|
self.stage.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||||
|
|
|
@ -6693,7 +6693,8 @@
|
||||||
"totp",
|
"totp",
|
||||||
"webauthn",
|
"webauthn",
|
||||||
"duo",
|
"duo",
|
||||||
"sms"
|
"sms",
|
||||||
|
"mobile"
|
||||||
],
|
],
|
||||||
"title": "Device classes"
|
"title": "Device classes"
|
||||||
},
|
},
|
||||||
|
|
11
schema.yml
11
schema.yml
|
@ -2307,11 +2307,7 @@ paths:
|
||||||
- mobile_device_token: []
|
- mobile_device_token: []
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
content:
|
description: Key successfully set
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: ''
|
|
||||||
'400':
|
'400':
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
@ -30985,6 +30981,9 @@ components:
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
duo:
|
duo:
|
||||||
type: integer
|
type: integer
|
||||||
|
mobile:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
AuthenticatorWebAuthnChallenge:
|
AuthenticatorWebAuthnChallenge:
|
||||||
type: object
|
type: object
|
||||||
description: WebAuthn Challenge
|
description: WebAuthn Challenge
|
||||||
|
@ -31897,6 +31896,7 @@ components:
|
||||||
- webauthn
|
- webauthn
|
||||||
- duo
|
- duo
|
||||||
- sms
|
- sms
|
||||||
|
- mobile
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
* `static` - Static
|
* `static` - Static
|
||||||
|
@ -31904,6 +31904,7 @@ components:
|
||||||
* `webauthn` - WebAuthn
|
* `webauthn` - WebAuthn
|
||||||
* `duo` - Duo
|
* `duo` - Duo
|
||||||
* `sms` - SMS
|
* `sms` - SMS
|
||||||
|
* `mobile` - authentik Mobile
|
||||||
DigestAlgorithmEnum:
|
DigestAlgorithmEnum:
|
||||||
enum:
|
enum:
|
||||||
- http://www.w3.org/2000/09/xmldsig#sha1
|
- http://www.w3.org/2000/09/xmldsig#sha1
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode";
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode";
|
||||||
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo";
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo";
|
||||||
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageMobile";
|
||||||
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn";
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn";
|
||||||
import { BaseStage, StageHost } from "@goauthentik/flow/stages/base";
|
import { BaseStage, StageHost } from "@goauthentik/flow/stages/base";
|
||||||
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
||||||
|
@ -118,6 +119,12 @@ export class AuthenticatorValidateStage
|
||||||
<p>${msg("Duo push-notifications")}</p>
|
<p>${msg("Duo push-notifications")}</p>
|
||||||
<small>${msg("Receive a push notification on your device.")}</small>
|
<small>${msg("Receive a push notification on your device.")}</small>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
case DeviceClassesEnum.Mobile:
|
||||||
|
return html`<i class="fas fa-mobile-alt"></i>
|
||||||
|
<div class="right">
|
||||||
|
<p>${msg("Push-notifications")}</p>
|
||||||
|
<small>${msg("Receive a push notification on your device.")}</small>
|
||||||
|
</div>`;
|
||||||
case DeviceClassesEnum.Webauthn:
|
case DeviceClassesEnum.Webauthn:
|
||||||
return html`<i class="fas fa-mobile-alt"></i>
|
return html`<i class="fas fa-mobile-alt"></i>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
@ -221,6 +228,14 @@ export class AuthenticatorValidateStage
|
||||||
.showBackButton=${(this.challenge?.deviceChallenges || []).length > 1}
|
.showBackButton=${(this.challenge?.deviceChallenges || []).length > 1}
|
||||||
>
|
>
|
||||||
</ak-stage-authenticator-validate-duo>`;
|
</ak-stage-authenticator-validate-duo>`;
|
||||||
|
case DeviceClassesEnum.Mobile:
|
||||||
|
return html` <ak-stage-authenticator-validate-mobile
|
||||||
|
.host=${this}
|
||||||
|
.challenge=${this.challenge}
|
||||||
|
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||||
|
.showBackButton=${(this.challenge?.deviceChallenges || []).length > 1}
|
||||||
|
>
|
||||||
|
</ak-stage-authenticator-validate-mobile>`;
|
||||||
}
|
}
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import "@goauthentik/elements/EmptyState";
|
||||||
|
import "@goauthentik/elements/forms/FormElement";
|
||||||
|
import "@goauthentik/flow/FormStatic";
|
||||||
|
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
|
||||||
|
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { CSSResult, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
|
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||||
|
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||||
|
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest,
|
||||||
|
DeviceChallenge,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-stage-authenticator-validate-mobile")
|
||||||
|
export class AuthenticatorValidateStageWebMobile extends BaseStage<
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
|
> {
|
||||||
|
@property({ attribute: false })
|
||||||
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
showBackButton = false;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(): void {
|
||||||
|
this.host?.submit({
|
||||||
|
duo: this.deviceChallenge?.deviceUid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
if (!this.challenge) {
|
||||||
|
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||||
|
</ak-empty-state>`;
|
||||||
|
}
|
||||||
|
const errors = this.challenge.responseErrors?.duo || [];
|
||||||
|
return html`<div class="pf-c-login__main-body">
|
||||||
|
<form
|
||||||
|
class="pf-c-form"
|
||||||
|
@submit=${(e: Event) => {
|
||||||
|
this.submitForm(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ak-form-static
|
||||||
|
class="pf-c-form__group"
|
||||||
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
|
<div slot="link">
|
||||||
|
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||||
|
>${msg("Not you?")}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ak-form-static>
|
||||||
|
|
||||||
|
${errors.length > 0
|
||||||
|
? errors.map((err) => {
|
||||||
|
if (err.code === "denied") {
|
||||||
|
return html` <ak-stage-access-denied-icon
|
||||||
|
errorMessage=${err.string}
|
||||||
|
>
|
||||||
|
</ak-stage-access-denied-icon>`;
|
||||||
|
}
|
||||||
|
return html`<p>${err.string}</p>`;
|
||||||
|
})
|
||||||
|
: html`${msg("Sending Duo push notification")}`}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="pf-c-login__main-footer">
|
||||||
|
<ul class="pf-c-login__main-footer-links">
|
||||||
|
${this.showBackButton
|
||||||
|
? html`<li class="pf-c-login__main-footer-links-item">
|
||||||
|
<button
|
||||||
|
class="pf-c-button pf-m-secondary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
|
if (!this.host) return;
|
||||||
|
(
|
||||||
|
this.host as AuthenticatorValidateStage
|
||||||
|
).selectedDeviceChallenge = undefined;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${msg("Return to device picker")}
|
||||||
|
</button>
|
||||||
|
</li>`
|
||||||
|
: html``}
|
||||||
|
</ul>
|
||||||
|
</footer>`;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue