build(deps): bump webauthn from 0.4.7 to 1.0.0 (#1625)

* build(deps): bump webauthn from 0.4.7 to 1.0.0

Bumps [webauthn](https://github.com/duo-labs/py_webauthn) from 0.4.7 to 1.0.0.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v0.4.7...v1.0.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* stages/authenticator_webauthn: migrate to new library version

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/authenticator_validate: migrate to new version

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/authenticator_webauthn: add bytes_to_base64url_dict for json encoding

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* actually don't do that

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix missing response on web

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* more double json

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* more base64 stuff

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* working

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* ci: always sync

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
dependabot[bot] 2021-10-15 23:26:29 +02:00 committed by GitHub
parent 56a56ffdbf
commit 8040e2b6e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 169 additions and 210 deletions

View File

@ -150,7 +150,10 @@ jobs:
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
run: |
scripts/ci_prepare.sh
# Sync anyways since stable will have different dependencies
pipenv sync --dev
- name: run migrations to stable
run: pipenv run python -m lifecycle.migrate
- name: checkout current code

48
Pipfile.lock generated
View File

@ -80,6 +80,13 @@
"markers": "python_version >= '3.6'",
"version": "==3.4.1"
},
"asn1crypto": {
"hashes": [
"sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8",
"sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c"
],
"version": "==1.4.0"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
@ -489,13 +496,6 @@
"index": "pypi",
"version": "==3.1.0"
},
"future": {
"hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2"
},
"geoip2": {
"hashes": [
"sha256:f150bed3190d543712a17467208388d31bd8ddb49b2226fba53db8aaedb8ba89",
@ -991,6 +991,34 @@
"index": "pypi",
"version": "==3.11.0"
},
"pydantic": {
"hashes": [
"sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd",
"sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739",
"sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f",
"sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840",
"sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23",
"sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287",
"sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62",
"sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b",
"sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb",
"sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820",
"sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3",
"sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b",
"sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e",
"sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3",
"sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316",
"sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b",
"sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4",
"sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20",
"sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e",
"sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505",
"sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1",
"sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==1.8.2"
},
"pyjwt": {
"hashes": [
"sha256:a0b9a3b4e5ca5517cac9f1a6e9cd30bf1aa80be74fcdf4e28eded582ecfcfbae",
@ -1301,11 +1329,11 @@
},
"webauthn": {
"hashes": [
"sha256:238391b2e2cc60fb51a2cd2d2d6be149920b9af6184651353d9f95856617a9e7",
"sha256:8ad9072ff1d6169f3be30d4dc8733ea563dd266962397bc58b40f674a6af74ac"
"sha256:1c068b93ab0f03fc6905e42e42e6ad24caa4f2632ff13ab846d5e2ef0bf1aa37",
"sha256:6710b8b3d846010fcf303d4fb96ca42de154aeeec379ead4e18cc582a11f9abc"
],
"index": "pypi",
"version": "==0.4.7"
"version": "==1.0.0"
},
"websocket-client": {
"hashes": [

View File

@ -17,13 +17,10 @@ from django.views.generic import View
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from sentry_sdk import capture_exception
from structlog.stdlib import BoundLogger, get_logger
from authentik.api.throttle import SessionThrottle
from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import (
@ -100,33 +97,10 @@ class InvalidStageError(SentryIgnoredException):
"""Error raised when a challenge from a stage is not valid"""
class FlowPendingUserThrottle(ScopedRateThrottle):
"""Custom throttle based on which user is pending"""
def get_cache_key(self, request: Request, view) -> str:
if SESSION_KEY_PLAN not in request._request.session:
return ""
if PLAN_CONTEXT_PENDING_USER not in request._request.session[SESSION_KEY_PLAN].context:
return ""
user = request._request.session[SESSION_KEY_PLAN].context[PLAN_CONTEXT_PENDING_USER]
return f"authentik-throttle-flow-pending-{user.uid}"
def allow_request(self, request: Request, view) -> bool:
if SESSION_KEY_PLAN not in request._request.session:
return True
if PLAN_CONTEXT_PENDING_USER not in request._request.session[SESSION_KEY_PLAN].context:
return True
if request._request.user.is_superuser:
return True
return super().allow_request(request, view)
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views"""
throttle_classes = [SessionThrottle, FlowPendingUserThrottle]
throttle_scope = "flow_executor"
permission_classes = [AllowAny]
flow: Flow

View File

@ -205,10 +205,6 @@ REST_FRAMEWORK = {
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"DEFAULT_THROTTLE_RATES": {
"anon": "100/day",
"flow_executor": "100/day",
},
}
REDIS_PROTOCOL_PREFIX = "redis://"

View File

@ -1,4 +1,7 @@
"""Validation stage challenge checking"""
from json import dumps, loads
from typing import Optional
from django.http import HttpRequest
from django.http.response import Http404
from django.shortcuts import get_object_or_404
@ -8,12 +11,10 @@ from django_otp.models import Device
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
from webauthn.webauthn import (
AuthenticationRejectedException,
RegistrationRejectedException,
WebAuthnUserDataMissing,
)
from webauthn import generate_authentication_options, verify_authentication_response
from webauthn.helpers import base64url_to_bytes, options_to_json
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from webauthn.helpers.structs import AuthenticationCredential
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
@ -21,7 +22,7 @@ from authentik.lib.utils.http import get_client_ip
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_sms.models import SMSDevice
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
LOGGER = get_logger()
@ -42,40 +43,26 @@ def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
return {}
def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict:
def get_webauthn_challenge(request: HttpRequest, device: Optional[WebAuthnDevice] = None) -> dict:
"""Send the client a challenge that we'll check later"""
request.session.pop("challenge", None)
challenge = generate_challenge(32)
allowed_credentials = []
# 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("=")
if device:
# We want all the user's WebAuthn devices and merge their challenges
for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"):
user_device: WebAuthnDevice
allowed_credentials.append(user_device.descriptor)
assertion = {}
user = device.user
authentication_options = generate_authentication_options(
rp_id=get_rp_id(request),
allow_credentials=allowed_credentials,
)
# We want all the user's WebAuthn devices and merge their challenges
for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"):
webauthn_user = WebAuthnUser(
user.uid,
user.username,
user.name,
user.avatar,
user_device.credential_id,
user_device.public_key,
user_device.sign_count,
user_device.rp_id,
)
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
if assertion == {}:
assertion = webauthn_assertion_options.assertion_dict
else:
assertion["allowCredentials"] += webauthn_assertion_options.assertion_dict.get(
"allowCredentials"
)
request.session["challenge"] = authentication_options.challenge
return assertion
return loads(options_to_json(authentication_options))
def select_challenge(request: HttpRequest, device: Device):
@ -99,45 +86,32 @@ def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str:
return code
# pylint: disable=unused-argument
def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) -> dict:
"""Validate WebAuthn Challenge"""
challenge = request.session.get("challenge")
assertion_response = data
credential_id = assertion_response.get("id")
credential_id = data.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,
user.avatar,
device.credential_id,
device.public_key,
device.sign_count,
device.rp_id,
)
webauthn_assertion_response = WebAuthnAssertionResponse(
webauthn_user,
assertion_response,
challenge,
get_origin(request),
uv_required=False,
) # User Verification
try:
sign_count = webauthn_assertion_response.verify()
except (
AuthenticationRejectedException,
WebAuthnUserDataMissing,
RegistrationRejectedException,
) as exc:
authentication_verification = verify_authentication_response(
credential=AuthenticationCredential.parse_raw(dumps(data)),
expected_challenge=challenge,
expected_rp_id=get_rp_id(request),
expected_origin=get_origin(request),
credential_public_key=base64url_to_bytes(device.public_key),
credential_current_sign_count=device.sign_count,
require_user_verification=False,
)
except (InvalidAuthenticationResponse) as exc:
LOGGER.warning("Assertion failed", exc=exc)
raise ValidationError("Assertion failed") from exc
device.set_sign_count(sign_count)
device.set_sign_count(authentication_verification.new_sign_count)
return data

View File

@ -7,6 +7,7 @@ from django.utils.encoding import force_str
from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.exceptions import ValidationError
from rest_framework.test import APITestCase
from webauthn.helpers import bytes_to_base64url
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
@ -101,8 +102,8 @@ class AuthenticatorValidateStageTests(APITestCase):
webauthn_device = WebAuthnDevice.objects.create(
user=self.user,
public_key="qwerqwerqre",
credential_id="foobarbaz",
public_key=bytes_to_base64url(b"qwerqwerqre"),
credential_id=bytes_to_base64url(b"foobarbaz"),
sign_count=0,
rp_id="foo",
)
@ -113,14 +114,13 @@ class AuthenticatorValidateStageTests(APITestCase):
{
"allowCredentials": [
{
"id": "foobarbaz",
"transports": ["usb", "nfc", "ble", "internal"],
"id": "Zm9vYmFyYmF6",
"type": "public-key",
}
],
"rpId": "foo",
"rpId": "testserver",
"timeout": 60000,
"userVerification": "discouraged",
"userVerification": "preferred",
},
)

View File

@ -8,6 +8,8 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django_otp.models import Device
from rest_framework.serializers import BaseSerializer
from webauthn.helpers.base64url_to_bytes import base64url_to_bytes
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
@ -64,6 +66,11 @@ class WebAuthnDevice(Device):
created_on = models.DateTimeField(auto_now_add=True)
last_used_on = models.DateTimeField(default=now)
@property
def descriptor(self) -> PublicKeyCredentialDescriptor:
"""Get a publickeydescriptor for this device"""
return PublicKeyCredentialDescriptor(id=base64url_to_bytes(self.credential_id))
def set_sign_count(self, sign_count: int) -> None:
"""Set the sign_count and update the last_used_on datetime."""
self.sign_count = sign_count

View File

@ -1,16 +1,22 @@
"""WebAuthn stage"""
from json import dumps, loads
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from webauthn.webauthn import (
RegistrationRejectedException,
WebAuthnCredential,
WebAuthnMakeCredentialOptions,
WebAuthnRegistrationResponse,
from webauthn import generate_registration_options, options_to_json, verify_registration_response
from webauthn.helpers import bytes_to_base64url
from webauthn.helpers.exceptions import InvalidRegistrationResponse
from webauthn.helpers.structs import (
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
RegistrationCredential,
ResidentKeyRequirement,
UserVerificationRequirement,
)
from webauthn.registration.verify_registration_response import VerifiedRegistration
from authentik.core.models import User
from authentik.flows.challenge import (
@ -22,7 +28,7 @@ from authentik.flows.challenge import (
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin, get_rp_id
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
LOGGER = get_logger()
@ -47,46 +53,29 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
def validate_response(self, response: dict) -> dict:
"""Validate webauthn challenge response"""
# pylint: disable=no-name-in-module
from pydantic.error_wrappers import ValidationError as PydanticValidationError
challenge = self.request.session["challenge"]
trusted_attestation_cert_required = True
self_attestation_permitted = True
none_attestation_permitted = True
webauthn_registration_response = WebAuthnRegistrationResponse(
get_rp_id(self.request),
get_origin(self.request),
response,
challenge,
trusted_attestation_cert_required=trusted_attestation_cert_required,
self_attestation_permitted=self_attestation_permitted,
none_attestation_permitted=none_attestation_permitted,
uv_required=False,
) # User Verification
try:
webauthn_credential = webauthn_registration_response.verify()
except RegistrationRejectedException as exc:
registration: VerifiedRegistration = verify_registration_response(
credential=RegistrationCredential.parse_raw(dumps(response)),
expected_challenge=challenge,
expected_rp_id=get_rp_id(self.request),
expected_origin=get_origin(self.request),
)
except (InvalidRegistrationResponse, PydanticValidationError) as exc:
LOGGER.warning("registration failed", exc=exc)
raise ValidationError(f"Registration failed. Error: {exc}")
# Step 17.
#
# Check that the credentialId is not yet registered to any other user.
# If registration is requested for a credential that is already registered
# to a different user, the Relying Party SHOULD fail this registration
# ceremony, or it MAY decide to accept the registration, e.g. while deleting
# the older registration.
credential_id_exists = WebAuthnDevice.objects.filter(
credential_id=webauthn_credential.credential_id
credential_id=bytes_to_base64url(registration.credential_id)
).first()
if credential_id_exists:
raise ValidationError("Credential ID already exists.")
webauthn_credential.credential_id = str(webauthn_credential.credential_id, "utf-8")
webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8")
return webauthn_credential
return registration
class AuthenticatorWebAuthnStageView(ChallengeStageView):
@ -98,35 +87,26 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
# clear session variables prior to starting a new registration
self.request.session.pop("challenge", None)
challenge = generate_challenge(32)
# We strip the saved challenge of padding, so that we can do a byte
# comparison on the URL-safe-without-padding challenge we get back
# from the browser.
# We will still pass the padded version down to the browser so that the JS
# can decode the challenge into binary without too much trouble.
self.request.session["challenge"] = challenge.rstrip("=")
user = self.get_pending_user()
make_credential_options = WebAuthnMakeCredentialOptions(
challenge,
self.request.tenant.branding_title,
get_rp_id(self.request),
user.uid,
user.username,
user.name,
user.avatar,
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request),
rp_name=self.request.tenant.branding_title,
user_id=user.uid,
user_name=user.username,
user_display_name=user.name,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.PREFERRED,
user_verification=UserVerificationRequirement.PREFERRED,
),
)
registration_options.user.id = user.uid
registration_dict = make_credential_options.registration_dict
registration_dict["authenticatorSelection"] = {
"requireResidentKey": False,
"userVerification": "preferred",
}
self.request.session["challenge"] = registration_options.challenge
return AuthenticatorWebAuthnChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"registration": registration_dict,
"registration": loads(options_to_json(registration_options)),
}
)
@ -145,15 +125,15 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Webauthn Challenge has already been validated
webauthn_credential: WebAuthnCredential = response.validated_data["response"]
webauthn_credential: VerifiedRegistration = response.validated_data["response"]
existing_device = WebAuthnDevice.objects.filter(
credential_id=webauthn_credential.credential_id
credential_id=bytes_to_base64url(webauthn_credential.credential_id)
).first()
if not existing_device:
WebAuthnDevice.objects.create(
user=self.get_pending_user(),
public_key=webauthn_credential.public_key,
credential_id=webauthn_credential.credential_id,
public_key=bytes_to_base64url(webauthn_credential.credential_public_key),
credential_id=bytes_to_base64url(webauthn_credential.credential_id),
sign_count=webauthn_credential.sign_count,
rp_id=get_rp_id(self.request),
)

View File

@ -1,29 +1,7 @@
"""webauthn utils"""
import base64
import os
from django.http import HttpRequest
CHALLENGE_DEFAULT_BYTE_LEN = 32
def generate_challenge(challenge_len=CHALLENGE_DEFAULT_BYTE_LEN):
"""Generate a challenge of challenge_len bytes, Base64-encoded.
We use URL-safe base64, but we *don't* strip the padding, so that
the browser can decode it without too much hassle.
Note that if we are doing byte comparisons with the challenge in collectedClientData
later on, that value will not have padding, so we must remove the padding
before storing the value in the session.
"""
# If we know Python 3.6 or greater is available, we could replace this with one
# call to secrets.token_urlsafe
challenge_bytes = os.urandom(challenge_len)
challenge_base64 = base64.urlsafe_b64encode(challenge_bytes)
# Python 2/3 compatibility: b64encode returns bytes only in newer Python versions
if not isinstance(challenge_base64, str):
challenge_base64 = challenge_base64.decode("utf-8")
return challenge_base64
def get_rp_id(request: HttpRequest) -> str:
"""Get hostname from http request, without port"""

View File

@ -1,7 +1,5 @@
import * as base64js from "base64-js";
import { hexEncode } from "../../../utils";
export function b64enc(buf: Uint8Array): string {
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
@ -33,9 +31,11 @@ export interface Assertion {
id: string;
rawId: string;
type: string;
attObj: string;
clientData: string;
registrationClientExtensions: string;
response: {
clientDataJSON: string;
attestationObject: string;
};
}
/**
@ -55,9 +55,11 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
attObj: b64enc(attObj),
clientData: b64enc(clientDataJSON),
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
response: {
clientDataJSON: b64enc(clientDataJSON),
attestationObject: b64enc(attObj),
},
};
}
@ -91,10 +93,13 @@ export interface AuthAssertion {
id: string;
rawId: string;
type: string;
clientData: string;
authData: string;
signature: string;
assertionClientExtensions: string;
response: {
clientDataJSON: string;
authenticatorData: string;
signature: string;
userHandle: string | null;
};
}
/**
@ -113,9 +118,13 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
authData: b64RawEnc(authData),
clientData: b64RawEnc(clientDataJSON),
signature: hexEncode(sig),
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
response: {
clientDataJSON: b64RawEnc(clientDataJSON),
signature: b64RawEnc(sig),
authenticatorData: b64RawEnc(authData),
userHandle: null,
},
};
}

View File

@ -60,7 +60,9 @@ export class UserSettingsAuthenticatorDuo extends BaseUserSettings {
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/${encodeURIComponent("#/settings")}"
href="${this.configureUrl}?next=/${encodeURIComponent(
"#/settings;page-stages",
)}"
class="pf-c-button pf-m-primary"
>${t`Enable Duo authenticator`}
</a>`

View File

@ -60,7 +60,9 @@ export class UserSettingsAuthenticatorSMS extends BaseUserSettings {
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/${encodeURIComponent("#/settings")}"
href="${this.configureUrl}?next=/${encodeURIComponent(
"#/settings;page-stages",
)}"
class="pf-c-button pf-m-primary"
>${t`Enable SMS authenticator`}
</a>`

View File

@ -79,7 +79,9 @@ export class UserSettingsAuthenticatorStatic extends BaseUserSettings {
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/${encodeURIComponent("#/settings")}"
href="${this.configureUrl}?next=/${encodeURIComponent(
"#/settings;page-stages",
)}"
class="pf-c-button pf-m-primary"
>${t`Enable Static Tokens`}
</a>`

View File

@ -60,7 +60,9 @@ export class UserSettingsAuthenticatorTOTP extends BaseUserSettings {
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/${encodeURIComponent("#/settings")}"
href="${this.configureUrl}?next=/${encodeURIComponent(
"#/settings;page-stages",
)}"
class="pf-c-button pf-m-primary"
>${t`Enable TOTP`}
</a>`

View File

@ -119,7 +119,9 @@ export class UserSettingsAuthenticatorWebAuthn extends BaseUserSettings {
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/${encodeURIComponent("#/settings")}"
href="${this.configureUrl}?next=/${encodeURIComponent(
"#/settings;page-stages",
)}"
class="pf-c-button pf-m-primary"
>${t`Configure WebAuthn`}
</a>`