stages/authenticator_validate: send challenge for each device
This commit is contained in:
parent
3894895d32
commit
8878fac4e7
137
authentik/stages/authenticator_validate/challenge.py
Normal file
137
authentik/stages/authenticator_validate/challenge.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""Validation stage challenge checking"""
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_otp import match_token
|
||||
from django_otp.models import Device
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from rest_framework.fields import CharField, JSONField
|
||||
from rest_framework.serializers import Serializer, ValidationError
|
||||
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
|
||||
from webauthn.webauthn import (
|
||||
AuthenticationRejectedException,
|
||||
RegistrationRejectedException,
|
||||
WebAuthnUserDataMissing,
|
||||
)
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.templatetags.authentik_utils import avatar
|
||||
from authentik.stages.authenticator_validate.models import DeviceClasses
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge
|
||||
|
||||
|
||||
class DeviceChallenge(Serializer):
|
||||
"""Single device challenge"""
|
||||
|
||||
device_class = CharField()
|
||||
device_uid = CharField()
|
||||
challenge = JSONField()
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
|
||||
"""Generate challenge for a single device"""
|
||||
if isinstance(device, (TOTPDevice, StaticDevice)):
|
||||
# Code-based challenges have no hints
|
||||
return {}
|
||||
return get_webauthn_challenge(request, device)
|
||||
|
||||
|
||||
def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict:
|
||||
"""Send the client a challenge that we'll check later"""
|
||||
request.session.pop("challenge", None)
|
||||
|
||||
challenge = generate_challenge(32)
|
||||
|
||||
# We strip the padding from the challenge stored in the session
|
||||
# for the reasons outlined in the comment in webauthn_begin_activate.
|
||||
request.session["challenge"] = challenge.rstrip("=")
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
device.user.uid,
|
||||
device.user.username,
|
||||
device.user.name,
|
||||
avatar(device.user),
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
|
||||
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
|
||||
|
||||
return webauthn_assertion_options.assertion_dict
|
||||
|
||||
|
||||
def validate_challenge(
|
||||
challenge: DeviceChallenge, request: HttpRequest, user: User
|
||||
) -> DeviceChallenge:
|
||||
"""main entry point for challenge validation"""
|
||||
if challenge.validated_data["device_class"] in (
|
||||
DeviceClasses.TOTP,
|
||||
DeviceClasses.STATIC,
|
||||
):
|
||||
return validate_challenge_code(challenge, request, user)
|
||||
return validate_challenge_webauthn(challenge, request, user)
|
||||
|
||||
|
||||
def validate_challenge_code(
|
||||
challenge: DeviceChallenge, request: HttpRequest, user: User
|
||||
) -> DeviceChallenge:
|
||||
"""Validate code-based challenges. We test against every device, on purpose, as
|
||||
the user mustn't choose between totp and static devices."""
|
||||
device = match_token(user, challenge.validated_data["challenge"].get("code", None))
|
||||
if not device:
|
||||
raise ValidationError(_("Invalid Token"))
|
||||
return challenge
|
||||
|
||||
|
||||
def validate_challenge_webauthn(
|
||||
challenge: DeviceChallenge, request: HttpRequest, user: User
|
||||
) -> DeviceChallenge:
|
||||
"""Validate WebAuthn Challenge"""
|
||||
challenge = request.session.get("challenge")
|
||||
assertion_response = challenge.validated_data["challenge"]
|
||||
credential_id = assertion_response.get("id")
|
||||
|
||||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||
if not device:
|
||||
raise ValidationError("Device does not exist.")
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
user.uid,
|
||||
user.username,
|
||||
user.name,
|
||||
avatar(user),
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
|
||||
webauthn_assertion_response = WebAuthnAssertionResponse(
|
||||
webauthn_user,
|
||||
assertion_response,
|
||||
challenge,
|
||||
request.build_absolute_uri("/"),
|
||||
uv_required=False,
|
||||
) # User Verification
|
||||
|
||||
try:
|
||||
sign_count = webauthn_assertion_response.verify()
|
||||
except (
|
||||
AuthenticationRejectedException,
|
||||
WebAuthnUserDataMissing,
|
||||
RegistrationRejectedException,
|
||||
) as exc:
|
||||
raise ValidationError("Assertion failed") from exc
|
||||
|
||||
device.set_sign_count(sign_count)
|
||||
return challenge
|
|
@ -1,9 +1,11 @@
|
|||
"""Authenticator Validation"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django_otp import devices_for_user, user_has_device
|
||||
from rest_framework.fields import CharField, DictField, IntegerField, JSONField, ListField
|
||||
from django.http.request import QueryDict
|
||||
from django_otp import devices_for_user
|
||||
from rest_framework.fields import ListField
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import (
|
||||
ChallengeResponse,
|
||||
ChallengeTypes,
|
||||
|
@ -12,6 +14,11 @@ from authentik.flows.challenge import (
|
|||
from authentik.flows.models import NotConfiguredAction
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
DeviceChallenge,
|
||||
get_challenge_for_device,
|
||||
validate_challenge,
|
||||
)
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
@ -20,17 +27,20 @@ LOGGER = get_logger()
|
|||
class AuthenticatorChallenge(WithUserInfoChallenge):
|
||||
"""Authenticator challenge"""
|
||||
|
||||
users_device_classes = ListField(child=CharField())
|
||||
class_challenges = DictField(JSONField())
|
||||
device_challenges = ListField(child=DeviceChallenge())
|
||||
|
||||
|
||||
class AuthenticatorChallengeResponse(ChallengeResponse):
|
||||
"""Challenge used for Code-based authenticators"""
|
||||
|
||||
device_challenges = DictField(JSONField())
|
||||
response = DeviceChallenge()
|
||||
|
||||
def validate_device_challenges(self, value: dict[str, dict]):
|
||||
return value
|
||||
request: HttpRequest
|
||||
user: User
|
||||
|
||||
def validate_response(self, value: DeviceChallenge):
|
||||
"""Validate response"""
|
||||
return validate_challenge(value, self.request, self.user)
|
||||
|
||||
|
||||
class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
|
@ -38,7 +48,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
|
||||
response_class = AuthenticatorChallengeResponse
|
||||
|
||||
allowed_device_classes: set[str]
|
||||
challenges: list[DeviceChallenge]
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Check if a user is set, and check if the user has any devices
|
||||
|
@ -47,22 +57,27 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
if not user:
|
||||
LOGGER.debug("No pending user, continuing")
|
||||
return self.executor.stage_ok()
|
||||
has_devices = user_has_device(user)
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
|
||||
self.challenges = []
|
||||
user_devices = devices_for_user(self.get_pending_user())
|
||||
user_device_classes = set(
|
||||
[
|
||||
device.__class__.__name__.lower().replace("device", "")
|
||||
for device in user_devices
|
||||
]
|
||||
)
|
||||
stage_device_classes = set(self.executor.current_stage.device_classes)
|
||||
self.allowed_device_classes = user_device_classes.intersection(stage_device_classes)
|
||||
|
||||
# User has no devices, or the devices they have don't overlap with the allowed
|
||||
# classes
|
||||
if not has_devices or len(self.allowed_device_classes) < 1:
|
||||
for device in user_devices:
|
||||
device_class = device.__class__.__name__.lower().replace("device", "")
|
||||
if device_class not in stage.device_classes:
|
||||
continue
|
||||
self.challenges.append(
|
||||
DeviceChallenge(
|
||||
data={
|
||||
"device_class": device_class,
|
||||
"device_uid": device.pk,
|
||||
"challenge": get_challenge_for_device(request, device),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# No allowed devices
|
||||
if len(self.challenges) < 1:
|
||||
if stage.not_configured_action == NotConfiguredAction.SKIP:
|
||||
LOGGER.debug("Authenticator not configured, skipping stage")
|
||||
return self.executor.stage_ok()
|
||||
|
@ -76,10 +91,16 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
data={
|
||||
"type": ChallengeTypes.native,
|
||||
"component": "ak-stage-authenticator-validate",
|
||||
"users_device_classes": self.allowed_device_classes,
|
||||
"device_challenges": self.challenges,
|
||||
}
|
||||
)
|
||||
|
||||
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
|
||||
response: AuthenticatorChallengeResponse = super().get_response_instance(data)
|
||||
response.request = self.request
|
||||
response.user = self.get_pending_user()
|
||||
return response
|
||||
|
||||
def challenge_valid(
|
||||
self, challenge: AuthenticatorChallengeResponse
|
||||
) -> HttpResponse:
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
|
||||
from webauthn.webauthn import (
|
||||
AuthenticationRejectedException,
|
||||
RegistrationRejectedException,
|
||||
WebAuthnUserDataMissing,
|
||||
)
|
||||
|
||||
class BeginAssertion(FlowUserRequiredView):
|
||||
"""Send the client a challenge that we'll check later"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Send the client a challenge that we'll check later"""
|
||||
request.session.pop("challenge", None)
|
||||
|
||||
challenge = generate_challenge(32)
|
||||
|
||||
# We strip the padding from the challenge stored in the session
|
||||
# for the reasons outlined in the comment in webauthn_begin_activate.
|
||||
request.session["challenge"] = challenge.rstrip("=")
|
||||
|
||||
devices = WebAuthnDevice.objects.filter(user=self.user)
|
||||
if not devices.exists():
|
||||
return HttpResponseBadRequest()
|
||||
device: WebAuthnDevice = devices.first()
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
self.user.uid,
|
||||
self.user.username,
|
||||
self.user.name,
|
||||
avatar(self.user),
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
|
||||
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
|
||||
|
||||
return JsonResponse(webauthn_assertion_options.assertion_dict)
|
||||
|
||||
|
||||
class VerifyAssertion(FlowUserRequiredView):
|
||||
"""Verify assertion result that we've sent to the client"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Verify assertion result that we've sent to the client"""
|
||||
challenge = request.session.get("challenge")
|
||||
assertion_response = request.POST
|
||||
credential_id = assertion_response.get("id")
|
||||
|
||||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||
if not device:
|
||||
return JsonResponse({"fail": "Device does not exist."}, status=401)
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
self.user.uid,
|
||||
self.user.username,
|
||||
self.user.name,
|
||||
avatar(self.user),
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
|
||||
webauthn_assertion_response = WebAuthnAssertionResponse(
|
||||
webauthn_user, assertion_response, challenge, ORIGIN, uv_required=False
|
||||
) # User Verification
|
||||
|
||||
try:
|
||||
sign_count = webauthn_assertion_response.verify()
|
||||
except (
|
||||
AuthenticationRejectedException,
|
||||
WebAuthnUserDataMissing,
|
||||
RegistrationRejectedException,
|
||||
) as exc:
|
||||
return JsonResponse({"fail": "Assertion failed. Error: {}".format(exc)})
|
||||
|
||||
device.set_sign_count(sign_count)
|
||||
request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True
|
||||
return JsonResponse(
|
||||
{"success": "Successfully authenticated as {}".format(self.user.username)}
|
||||
)
|
|
@ -20,9 +20,7 @@ from authentik.lib.templatetags.authentik_utils import avatar
|
|||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge
|
||||
|
||||
RP_ID = "localhost"
|
||||
RP_NAME = "authentik"
|
||||
ORIGIN = "http://localhost:8000"
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -54,8 +52,8 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
|||
none_attestation_permitted = True
|
||||
|
||||
webauthn_registration_response = WebAuthnRegistrationResponse(
|
||||
RP_ID,
|
||||
ORIGIN,
|
||||
self.request.get_host(),
|
||||
self.request.build_absolute_uri("/"),
|
||||
response,
|
||||
challenge,
|
||||
trusted_attestation_cert_required=trusted_attestation_cert_required,
|
||||
|
@ -112,7 +110,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||
make_credential_options = WebAuthnMakeCredentialOptions(
|
||||
challenge,
|
||||
RP_NAME,
|
||||
RP_ID,
|
||||
self.request.get_host(),
|
||||
user.uid,
|
||||
user.username,
|
||||
user.name,
|
||||
|
@ -156,7 +154,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||
public_key=webauthn_credential.public_key,
|
||||
credential_id=webauthn_credential.credential_id,
|
||||
sign_count=webauthn_credential.sign_count,
|
||||
rp_id=RP_ID,
|
||||
rp_id=self.request.get_host(),
|
||||
)
|
||||
else:
|
||||
return self.executor.stage_invalid(
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
"""WebAuthn urls"""
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from authentik.stages.authenticator_webauthn.views import (
|
||||
UserSettingsView,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.views import UserSettingsView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
|
|
|
@ -1,29 +1,12 @@
|
|||
"""webauthn views"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.lib.templatetags.authentik_utils import avatar
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
AuthenticateWebAuthnStage,
|
||||
WebAuthnDevice,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.stage import (
|
||||
SESSION_KEY_WEBAUTHN_AUTHENTICATED,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge
|
||||
|
||||
LOGGER = get_logger()
|
||||
RP_ID = "localhost"
|
||||
RP_NAME = "authentik"
|
||||
ORIGIN = "http://localhost:8000"
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
|
|
|
@ -11048,6 +11048,7 @@ definitions:
|
|||
type: string
|
||||
enum:
|
||||
- skip
|
||||
- deny
|
||||
device_classes:
|
||||
description: ''
|
||||
type: array
|
||||
|
|
|
@ -42,7 +42,7 @@ export class AuthenticatorStaticStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submit(e); }}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
|
|
|
@ -30,7 +30,7 @@ export class AuthenticatorTOTPStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submit(e); }}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
|
|
|
@ -9,13 +9,18 @@ export enum DeviceClasses {
|
|||
WEBAUTHN = "webauthn",
|
||||
}
|
||||
|
||||
export interface DeviceChallenge {
|
||||
device_class: DeviceClasses;
|
||||
device_uid: string;
|
||||
challenge: unknown;
|
||||
}
|
||||
|
||||
export interface AuthenticatorValidateStageChallenge extends WithUserInfoChallenge {
|
||||
users_device_classes: DeviceClasses[];
|
||||
class_challenges: { [key in DeviceClasses]: unknown };
|
||||
device_challenges: DeviceChallenge[];
|
||||
}
|
||||
|
||||
export interface AuthenticatorValidateStageChallengeResponse {
|
||||
device_challenges: { [key in DeviceClasses]: unknown} ;
|
||||
response: DeviceChallenge;
|
||||
}
|
||||
|
||||
@customElement("ak-stage-authenticator-validate")
|
||||
|
@ -24,13 +29,24 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
|||
@property({ attribute: false })
|
||||
challenge?: AuthenticatorValidateStageChallenge;
|
||||
|
||||
renderDeviceClass(deviceClass: DeviceClasses): TemplateResult {
|
||||
switch (deviceClass) {
|
||||
@property({attribute: false})
|
||||
selectedDeviceChallenge?: DeviceChallenge;
|
||||
|
||||
renderDeviceChallenge(): TemplateResult {
|
||||
if (!this.selectedDeviceChallenge) {
|
||||
return html``;
|
||||
}
|
||||
switch (this.selectedDeviceChallenge?.device_class) {
|
||||
case DeviceClasses.STATIC:
|
||||
case DeviceClasses.TOTP:
|
||||
// TODO: Create input for code
|
||||
return html``;
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<ak-stage-authenticator-validate-webauthn .host=${this} .challenge=${this.challenge}></ak-stage-authenticator-validate-webauthn>`;
|
||||
return html`<ak-stage-authenticator-validate-webauthn
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}>
|
||||
</ak-stage-authenticator-validate-webauthn>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,9 +56,13 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
|||
|
||||
render(): TemplateResult {
|
||||
// User only has a single device class, so we don't show a picker
|
||||
if (this.challenge?.users_device_classes.length === 1) {
|
||||
return this.renderDeviceClass(this.challenge.users_device_classes[0]);
|
||||
if (this.challenge?.device_challenges.length === 1) {
|
||||
this.selectedDeviceChallenge = this.challenge.device_challenges[0];
|
||||
}
|
||||
if (this.selectedDeviceChallenge) {
|
||||
return this.renderDeviceChallenge();
|
||||
}
|
||||
// TODO: Create picker between challenges
|
||||
return html`ak-stage-authenticator-validate`;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { customElement, html, property, TemplateResult } from "lit-element";
|
|||
import { SpinnerSize } from "../../Spinner";
|
||||
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
|
||||
import { BaseStage } from "../base";
|
||||
import { AuthenticatorValidateStageChallenge, DeviceClasses } from "./AuthenticatorValidateStage";
|
||||
import { AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
|
||||
|
||||
@customElement("ak-stage-authenticator-validate-webauthn")
|
||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage {
|
||||
|
@ -11,6 +11,9 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
|
|||
@property({attribute: false})
|
||||
challenge?: AuthenticatorValidateStageChallenge;
|
||||
|
||||
@property({attribute: false})
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
@property({ type: Boolean })
|
||||
authenticateRunning = false;
|
||||
|
||||
|
@ -20,7 +23,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
|
|||
async authenticate(): Promise<void> {
|
||||
// convert certain members of the PublicKeyCredentialRequestOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.challenge?.class_challenges[DeviceClasses.WEBAUTHN];
|
||||
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.deviceChallenge?.challenge;
|
||||
const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
|
||||
|
||||
// request the authenticator to create an assertion signature using the
|
||||
|
@ -44,7 +47,11 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
|
|||
// post the assertion to the server for verification.
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.set(`response[${DeviceClasses.WEBAUTHN}]`, JSON.stringify(transformedAssertionForServer));
|
||||
formData.set("response", JSON.stringify(<DeviceChallenge>{
|
||||
device_class: this.deviceChallenge?.device_class,
|
||||
device_uid: this.deviceChallenge?.device_uid,
|
||||
challenge: transformedAssertionForServer,
|
||||
}));
|
||||
await this.host?.submit(formData);
|
||||
} catch (err) {
|
||||
throw new Error(gettext(`Error when validating assertion on server: ${err}`));
|
||||
|
|
|
@ -36,7 +36,7 @@ export class ConsentStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submit(e); }}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
|
|
|
@ -26,7 +26,7 @@ export class EmailStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submit(e); }}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
${gettext("Check your Emails for a password reset link.")}
|
||||
|
|
|
@ -74,7 +74,7 @@ export class IdentificationStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submit(e);}}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
||||
${this.challenge.application_pre ?
|
||||
html`<p>
|
||||
${gettext(`Login to continue to ${this.challenge.application_pre}.`)}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class PasswordStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submit(e);}}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
|
|
|
@ -119,7 +119,7 @@ export class PromptStage extends BaseStage {
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submit(e);}}>
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
||||
${this.challenge.fields.map((prompt) => {
|
||||
return html`<ak-form-element
|
||||
label="${prompt.label}"
|
||||
|
|
Reference in a new issue