build(deps): use @simplewebauthn/browser

feat-otp-verification
James Elliott 2023-01-30 13:47:54 +11:00
parent a36c45f1e1
commit 3af20a7daf
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
14 changed files with 221 additions and 660 deletions

View File

@ -30,10 +30,15 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
middlewares.IdentityVerificationFinishArgs{
ActionClaim: ActionWebauthnRegistration,
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
}, SecondFactorWebauthnAttestationGET)
}, WebauthnAttestationGET)
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server.
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
// WebauthnAttestationGET returns the attestation challenge from the server.
func WebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
WebauthnRegistrationGET(ctx)
}
// WebauthnRegistrationGET returns the attestation challenge from the server.
func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
var (
w *webauthn.WebAuthn
user *model.WebauthnUser
@ -92,13 +97,8 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
}
}
// WebauthnAttestationPOST processes the attestation challenge response from the client.
func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
type requestPostData struct {
Credential json.RawMessage `json:"credential"`
Description string `json:"description"`
}
// WebauthnRegistrationPOST processes the attestation challenge response from the client.
func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
var (
err error
w *webauthn.WebAuthn
@ -106,9 +106,10 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
userSession session.UserSession
attestationResponse *protocol.ParsedCredentialCreationData
response *protocol.ParsedCredentialCreationData
credential *webauthn.Credential
postData *requestPostData
bodyJSON bodyRegisterWebauthnRequest
)
if userSession, err = ctx.GetSession(); err != nil {
@ -135,8 +136,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
return
}
err = json.Unmarshal(ctx.PostBody(), &postData)
if err != nil {
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertion request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -144,7 +144,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
return
}
if attestationResponse, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(postData.Credential)); err != nil {
if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertion for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -152,6 +152,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
return
}
ctx.Logger.WithField("att_format", response.Response.AttestationObject.Format).Debug("Response Data")
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
@ -160,7 +162,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
return
}
if credential, err = w.CreateCredential(user, *userSession.Webauthn, attestationResponse); err != nil {
if credential, err = w.CreateCredential(user, *userSession.Webauthn, response); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -168,6 +170,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
return
}
ctx.Logger.WithField("att_type", credential.AttestationType).Debug("Credential Data")
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
if err != nil && err != storage.ErrNoWebauthnDevice {
ctx.Logger.Errorf("Unable to load existing %s devices for for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
@ -178,8 +182,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
}
for _, existingDevice := range devices {
if existingDevice.Description == postData.Description {
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, postData.Description)
if existingDevice.Description == bodyJSON.Description {
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, bodyJSON.Description)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
ctx.SetStatusCode(fasthttp.StatusConflict)
@ -189,7 +193,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
}
}
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, postData.Description, credential)
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, bodyJSON.Description, credential)
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
@ -207,5 +211,5 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
ctx.ReplyOK()
ctx.SetStatusCode(fasthttp.StatusCreated)
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Description": postData.Description})
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Description": bodyJSON.Description})
}

View File

@ -137,7 +137,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
user *model.WebauthnUser
)
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)

View File

@ -1,6 +1,7 @@
package handlers
import (
"encoding/json"
"net/http"
"net/url"
@ -35,6 +36,14 @@ type bodySignWebauthnRequest struct {
TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
WorkflowID string `json:"workflowID"`
Response json.RawMessage `json:"response"`
}
type bodyRegisterWebauthnRequest struct {
Description string `json:"description"`
Response json.RawMessage `json:"response"`
}
type bodyEditWebauthnDeviceRequest struct {

View File

@ -96,23 +96,21 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
}
func identityVerificationValidateToken(ctx *AutheliaCtx) (*jwt.Token, error) {
var finishBody IdentityVerificationFinishBody
var bodyJSON IdentityVerificationFinishBody
b := ctx.PostBody()
err := json.Unmarshal(b, &finishBody)
err := json.Unmarshal(ctx.PostBody(), &bodyJSON)
if err != nil {
ctx.Error(err, messageOperationFailed)
return nil, err
}
if finishBody.Token == "" {
if bodyJSON.Token == "" {
ctx.Error(fmt.Errorf("No token provided"), messageOperationFailed)
return nil, err
}
token, err := jwt.ParseWithClaims(finishBody.Token, &model.IdentityVerificationClaim{},
token, err := jwt.ParseWithClaims(bodyJSON.Token, &model.IdentityVerificationClaim{},
func(token *jwt.Token) (any, error) {
return []byte(ctx.Configuration.JWTSecret), nil
})
@ -185,8 +183,7 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
return
}
err = ctx.Providers.StorageProvider.ConsumeIdentityVerification(ctx, claims.ID, model.NewNullIP(ctx.RemoteIP()))
if err != nil {
if err = ctx.Providers.StorageProvider.ConsumeIdentityVerification(ctx, claims.ID, model.NewNullIP(ctx.RemoteIP())); err != nil {
ctx.Error(err, messageOperationFailed)
return
}

View File

@ -238,7 +238,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
// Webauthn Endpoints.
r.POST("/api/secondfactor/webauthn/identity/start", middleware1FA(handlers.WebauthnIdentityStart))
r.POST("/api/secondfactor/webauthn/identity/finish", middleware1FA(handlers.WebauthnIdentityFinish))
r.POST("/api/secondfactor/webauthn/attestation", middleware1FA(handlers.WebauthnAttestationPOST))
r.GET("/api/secondfactor/register/webauthn", middleware1FA(handlers.WebauthnRegistrationGET))
r.POST("/api/secondfactor/register/webauthn", middleware1FA(handlers.WebauthnRegistrationPOST))
r.POST("/api/secondfactor/webauthn/attestation", middleware1FA(handlers.WebauthnRegistrationPOST))
r.GET("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionGET))
r.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionPOST))

View File

@ -28,6 +28,8 @@
"@mui/icons-material": "5.11.0",
"@mui/material": "5.11.6",
"@mui/styles": "5.11.2",
"@simplewebauthn/browser": "^7.0.1",
"@simplewebauthn/typescript-types": "^7.0.0",
"axios": "1.2.6",
"broadcast-channel": "4.20.2",
"classnames": "2.3.2",

View File

@ -14,6 +14,8 @@ specifiers:
'@mui/icons-material': 5.11.0
'@mui/material': 5.11.6
'@mui/styles': 5.11.2
'@simplewebauthn/browser': ^7.0.1
'@simplewebauthn/typescript-types': ^7.0.0
'@testing-library/jest-dom': 5.16.5
'@testing-library/react': 13.4.0
'@types/jest': 29.4.0
@ -76,6 +78,8 @@ dependencies:
'@mui/icons-material': 5.11.0_j5wvuqirnhynb4halegp2mqooy
'@mui/material': 5.11.6_rqh7qj4464ntrqrt6banhaqg4q
'@mui/styles': 5.11.2_3stiutgnnbnfnf3uowm5cip22i
'@simplewebauthn/browser': 7.0.1
'@simplewebauthn/typescript-types': 7.0.0
axios: 1.2.6
broadcast-channel: 4.20.2
classnames: 2.3.2
@ -3395,6 +3399,14 @@ packages:
resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==}
dev: true
/@simplewebauthn/browser/7.0.1:
resolution: {integrity: sha512-zFTTQojH8FWAvzBeB9WHMkZz5XrHaNQwKmdpGDfyRtXHJSLlWUjZN1egD2vWqU+gE0CKl0NRZJ56uLugBs9AUg==}
dev: false
/@simplewebauthn/typescript-types/7.0.0:
resolution: {integrity: sha512-bV+xACCFTsrLR/23ozHO06ZllHZaxC8LlI5YCo79GvU2BrN+rePDU2yXwZIYndNWcMQwRdndRdAhpafOh9AC/g==}
dev: false
/@sinclair/typebox/0.25.21:
resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==}
dev: true

View File

@ -11,7 +11,6 @@ import { WebauthnTouchState } from "@models/Webauthn";
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
interface Props {
timer: number;
onRetryClick: () => void;
webauthnTouchState: WebauthnTouchState;
}
@ -61,7 +60,7 @@ export default function WebauthnTryIcon(props: Props) {
);
return (
<Box className={styles.icon} sx={{ minHeight: 101 }} align="middle">
<Box className={styles.icon} sx={{ minHeight: 101 }}>
{touch}
{failure}
</Box>

View File

@ -1,5 +1,12 @@
import {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/typescript-types";
export interface PublicKeyCredentialCreationOptionsStatus {
options?: PublicKeyCredentialCreationOptions;
options?: PublicKeyCredentialCreationOptionsJSON;
status: number;
}
@ -7,15 +14,8 @@ export interface CredentialCreation {
publicKey: PublicKeyCredentialCreationOptionsJSON;
}
export interface PublicKeyCredentialCreationOptionsJSON
extends Omit<PublicKeyCredentialCreationOptions, "challenge" | "excludeCredentials" | "user"> {
challenge: string;
excludeCredentials?: PublicKeyCredentialDescriptorJSON[];
user: PublicKeyCredentialUserEntityJSON;
}
export interface PublicKeyCredentialRequestOptionsStatus {
options?: PublicKeyCredentialRequestOptions;
options?: PublicKeyCredentialRequestOptionsJSON;
status: number;
}
@ -23,63 +23,6 @@ export interface CredentialRequest {
publicKey: PublicKeyCredentialRequestOptionsJSON;
}
export interface PublicKeyCredentialRequestOptionsJSON
extends Omit<PublicKeyCredentialRequestOptions, "allowCredentials" | "challenge"> {
allowCredentials?: PublicKeyCredentialDescriptorJSON[];
challenge: string;
}
export interface PublicKeyCredentialDescriptorJSON extends Omit<PublicKeyCredentialDescriptor, "id"> {
id: string;
}
export interface PublicKeyCredentialUserEntityJSON extends Omit<PublicKeyCredentialUserEntity, "id"> {
id: string;
}
export interface AuthenticatorAssertionResponseJSON
extends Omit<AuthenticatorAssertionResponse, "authenticatorData" | "clientDataJSON" | "signature" | "userHandle"> {
authenticatorData: string;
clientDataJSON: string;
signature: string;
userHandle: string;
}
export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse {
getTransports?: () => AuthenticatorTransport[];
getAuthenticatorData?: () => ArrayBuffer;
getPublicKey?: () => ArrayBuffer;
getPublicKeyAlgorithm?: () => COSEAlgorithmIdentifier[];
}
export interface AttestationPublicKeyCredential extends PublicKeyCredential {
response: AuthenticatorAttestationResponseFuture;
}
export interface AuthenticatorAttestationResponseJSON
extends Omit<AuthenticatorAttestationResponseFuture, "clientDataJSON" | "attestationObject"> {
clientDataJSON: string;
attestationObject: string;
}
export interface AttestationPublicKeyCredentialJSON
extends Omit<AttestationPublicKeyCredential, "response" | "rawId" | "getClientExtensionResults"> {
rawId: string;
response: AuthenticatorAttestationResponseJSON;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
transports?: AuthenticatorTransport[];
}
export interface PublicKeyCredentialJSON
extends Omit<PublicKeyCredential, "rawId" | "response" | "getClientExtensionResults"> {
rawId: string;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
response: AuthenticatorAssertionResponseJSON;
targetURL?: string;
workflow?: string;
workflowID?: string;
}
export enum AttestationResult {
Success = 1,
Failure,
@ -93,21 +36,11 @@ export enum AttestationResult {
FailureToken,
}
export interface AttestationPublicKeyCredentialResult {
credential?: AttestationPublicKeyCredential;
export interface RegistrationResult {
response?: RegistrationResponseJSON;
result: AttestationResult;
}
export interface AttestationPublicKeyCredentialResultJSON {
credential?: AttestationPublicKeyCredentialJSON;
result: AttestationResult;
}
export interface AttestationFinishResult {
result: AttestationResult;
message: string;
}
export enum AssertionResult {
Success = 1,
Failure,
@ -121,18 +54,54 @@ export enum AssertionResult {
FailureUnrecognized,
}
export interface DiscoverableAssertionResult {
result: AssertionResult;
username: string;
export function AssertionResultFailureString(result: AssertionResult) {
switch (result) {
case AssertionResult.Success:
return "";
case AssertionResult.FailureUserConsent:
return "You cancelled the assertion request.";
case AssertionResult.FailureU2FFacetID:
return "The server responded with an invalid Facet ID for the URL.";
case AssertionResult.FailureSyntax:
return "The assertion challenge was rejected as malformed or incompatible by your browser.";
case AssertionResult.FailureWebauthnNotSupported:
return "Your browser does not support the WebAuthN protocol.";
case AssertionResult.FailureUnrecognized:
return "This device is not registered.";
case AssertionResult.FailureUnknownSecurity:
return "An unknown security error occurred.";
case AssertionResult.FailureUnknown:
return "An unknown error occurred.";
default:
return "An unexpected error occurred.";
}
}
export interface AssertionPublicKeyCredentialResult {
credential?: PublicKeyCredential;
result: AssertionResult;
export function AttestationResultFailureString(result: AttestationResult) {
switch (result) {
case AttestationResult.FailureToken:
return "You must open the link from the same device and browser that initiated the registration process.";
case AttestationResult.FailureSupport:
return "Your browser does not appear to support the configuration.";
case AttestationResult.FailureSyntax:
return "The attestation challenge was rejected as malformed or incompatible by your browser.";
case AttestationResult.FailureWebauthnNotSupported:
return "Your browser does not support the WebAuthN protocol.";
case AttestationResult.FailureUserConsent:
return "You cancelled the attestation request.";
case AttestationResult.FailureUserVerificationOrResidentKey:
return "Your device does not support user verification or resident keys but this was required.";
case AttestationResult.FailureExcluded:
return "You have registered this device already.";
case AttestationResult.FailureUnknown:
return "An unknown error occurred.";
}
return "";
}
export interface AssertionPublicKeyCredentialResultJSON {
credential?: PublicKeyCredentialJSON;
export interface AuthenticationResult {
response?: AuthenticationResponseJSON;
result: AssertionResult;
}
@ -156,7 +125,3 @@ export enum WebauthnTouchState {
InProgress = 2,
Failure = 3,
}
export interface WebauthnDeviceUpdateRequest {
description: string;
}

View File

@ -12,8 +12,7 @@ export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/i
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
export const WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
export const WebauthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish";
export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation";
export const WebauthnRegistrationPath = basePath + "/api/secondfactor/register/webauthn";
export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";

View File

@ -1,35 +1,32 @@
import { startAuthentication, startRegistration } from "@simplewebauthn/browser";
import {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/typescript-types";
import axios, { AxiosError, AxiosResponse } from "axios";
import {
AssertionPublicKeyCredentialResult,
AssertionResult,
AttestationFinishResult,
AttestationPublicKeyCredential,
AttestationPublicKeyCredentialJSON,
AttestationPublicKeyCredentialResult,
AttestationResult,
AuthenticatorAttestationResponseFuture,
AuthenticationResult,
CredentialCreation,
CredentialRequest,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialCreationOptionsStatus,
PublicKeyCredentialDescriptorJSON,
PublicKeyCredentialJSON,
PublicKeyCredentialRequestOptionsJSON,
PublicKeyCredentialRequestOptionsStatus,
RegistrationResult,
} from "@models/Webauthn";
import {
AuthenticationOKResponse,
OptionalDataServiceResponse,
ServiceResponse,
WebauthnAssertionPath,
WebauthnAttestationPath,
WebauthnDevicePath,
WebauthnIdentityFinishPath,
WebauthnRegistrationPath,
validateStatusAuthentication,
} from "@services/Api";
import { SignInResponse } from "@services/SignIn";
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from "@utils/Base64";
export function isWebauthnSecure(): boolean {
if (window.isSecureContext) {
@ -51,120 +48,6 @@ export async function isWebauthnPlatformAuthenticatorAvailable(): Promise<boolea
return window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}
function arrayBufferEncode(value: ArrayBuffer): string {
return getBase64WebEncodingFromBytes(new Uint8Array(value));
}
function arrayBufferDecode(value: string): ArrayBuffer {
return getBytesFromBase64(value);
}
function decodePublicKeyCredentialDescriptor(
descriptor: PublicKeyCredentialDescriptorJSON,
): PublicKeyCredentialDescriptor {
return {
id: arrayBufferDecode(descriptor.id),
type: descriptor.type,
transports: descriptor.transports,
};
}
function decodePublicKeyCredentialCreationOptions(
options: PublicKeyCredentialCreationOptionsJSON,
): PublicKeyCredentialCreationOptions {
return {
attestation: options.attestation,
authenticatorSelection: options.authenticatorSelection,
challenge: arrayBufferDecode(options.challenge),
excludeCredentials: options.excludeCredentials?.map(decodePublicKeyCredentialDescriptor),
extensions: options.extensions,
pubKeyCredParams: options.pubKeyCredParams,
rp: options.rp,
timeout: options.timeout,
user: {
displayName: options.user.displayName,
id: arrayBufferDecode(options.user.id),
name: options.user.name,
},
};
}
function decodePublicKeyCredentialRequestOptions(
options: PublicKeyCredentialRequestOptionsJSON,
): PublicKeyCredentialRequestOptions {
let allowCredentials: PublicKeyCredentialDescriptor[] | undefined = undefined;
if (options.allowCredentials?.length !== 0) {
allowCredentials = options.allowCredentials?.map(decodePublicKeyCredentialDescriptor);
}
return {
allowCredentials: allowCredentials,
challenge: arrayBufferDecode(options.challenge),
extensions: options.extensions,
rpId: options.rpId,
timeout: options.timeout,
userVerification: options.userVerification,
};
}
function encodeAttestationPublicKeyCredential(
credential: AttestationPublicKeyCredential,
): AttestationPublicKeyCredentialJSON {
const response = credential.response as AuthenticatorAttestationResponseFuture;
let transports: AuthenticatorTransport[] | undefined;
if (response?.getTransports !== undefined && typeof response.getTransports === "function") {
transports = response.getTransports();
}
return {
id: credential.id,
type: credential.type,
rawId: arrayBufferEncode(credential.rawId),
clientExtensionResults: credential.getClientExtensionResults(),
response: {
attestationObject: arrayBufferEncode(response.attestationObject),
clientDataJSON: arrayBufferEncode(response.clientDataJSON),
},
transports: transports,
};
}
function encodeAssertionPublicKeyCredential(
credential: PublicKeyCredential,
targetURL: string | undefined,
workflow: string | undefined,
workflowID: string | undefined,
): PublicKeyCredentialJSON {
const response = credential.response as AuthenticatorAssertionResponse;
let userHandle: string;
if (response.userHandle == null) {
userHandle = "";
} else {
userHandle = arrayBufferEncode(response.userHandle);
}
return {
id: credential.id,
type: credential.type,
rawId: arrayBufferEncode(credential.rawId),
clientExtensionResults: credential.getClientExtensionResults(),
response: {
authenticatorData: arrayBufferEncode(response.authenticatorData),
clientDataJSON: arrayBufferEncode(response.clientDataJSON),
signature: arrayBufferEncode(response.signature),
userHandle: userHandle,
},
targetURL: targetURL,
workflow: workflow,
workflowID: workflowID,
};
}
function getAttestationResultFromDOMException(exception: DOMException): AttestationResult {
// Docs for this section:
// https://w3c.github.io/webauthn/#sctn-op-make-cred
@ -193,7 +76,7 @@ function getAttestationResultFromDOMException(exception: DOMException): Attestat
function getAssertionResultFromDOMException(
exception: DOMException,
requestOptions: PublicKeyCredentialRequestOptions,
options: PublicKeyCredentialRequestOptionsJSON,
): AssertionResult {
// Docs for this section:
// https://w3c.github.io/webauthn/#sctn-op-get-assertion
@ -209,7 +92,7 @@ function getAssertionResultFromDOMException(
// § 6.3.3 Step 6 and Step 7.
return AssertionResult.FailureUserConsent;
case "SecurityError":
if (requestOptions.extensions?.appid !== undefined) {
if (options.extensions?.appid !== undefined) {
// § 10.1 and 10.2 Step 3.
return AssertionResult.FailureU2FFacetID;
} else {
@ -221,14 +104,10 @@ function getAssertionResultFromDOMException(
}
}
export async function getAttestationCreationOptions(
token: null | string,
): Promise<PublicKeyCredentialCreationOptionsStatus> {
export async function getAttestationCreationOptions(): Promise<PublicKeyCredentialCreationOptionsStatus> {
let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
response = await axios.post<ServiceResponse<CredentialCreation>>(WebauthnIdentityFinishPath, {
token: token,
});
response = await axios.get<ServiceResponse<CredentialCreation>>(WebauthnRegistrationPath);
if (response.data.status !== "OK" || response.data.data == null) {
return {
@ -237,12 +116,12 @@ export async function getAttestationCreationOptions(
}
return {
options: decodePublicKeyCredentialCreationOptions(response.data.data.publicKey),
options: response.data.data.publicKey,
status: response.status,
};
}
export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
export async function getAuthenticationOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
let response: AxiosResponse<ServiceResponse<CredentialRequest>>;
response = await axios.get<ServiceResponse<CredentialRequest>>(WebauthnAssertionPath);
@ -254,65 +133,55 @@ export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialR
}
return {
options: decodePublicKeyCredentialRequestOptions(response.data.data.publicKey),
options: response.data.data.publicKey,
status: response.status,
};
}
export async function getAttestationPublicKeyCredentialResult(
creationOptions: PublicKeyCredentialCreationOptions,
): Promise<AttestationPublicKeyCredentialResult> {
const result: AttestationPublicKeyCredentialResult = {
result: AttestationResult.Success,
export async function startWebauthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) {
const result: RegistrationResult = {
result: AttestationResult.Failure,
};
try {
result.credential = (await navigator.credentials.create({
publicKey: creationOptions,
})) as AttestationPublicKeyCredential;
result.response = await startRegistration(options);
} catch (e) {
result.result = AttestationResult.Failure;
const exception = e as DOMException;
if (exception !== undefined) {
result.result = getAttestationResultFromDOMException(exception);
return result;
} else {
console.error(`Unhandled exception occurred during WebAuthN attestation: ${e}`);
console.error(`Unhandled exception occurred during WebAuthn attestation: ${e}`);
}
}
if (result.credential != null) {
if (result.response != null) {
result.result = AttestationResult.Success;
}
return result;
}
export async function getAssertionPublicKeyCredentialResult(
requestOptions: PublicKeyCredentialRequestOptions,
): Promise<AssertionPublicKeyCredentialResult> {
const result: AssertionPublicKeyCredentialResult = {
export async function getAuthenticationResult(options: PublicKeyCredentialRequestOptionsJSON) {
const result: AuthenticationResult = {
result: AssertionResult.Success,
};
try {
result.credential = (await navigator.credentials.get({ publicKey: requestOptions })) as PublicKeyCredential;
result.response = await startAuthentication(options);
} catch (e) {
result.result = AssertionResult.Failure;
const exception = e as DOMException;
if (exception !== undefined) {
result.result = getAssertionResultFromDOMException(exception, requestOptions);
result.result = getAssertionResultFromDOMException(exception, options);
return result;
} else {
console.error(`Unhandled exception occurred during WebAuthN assertion: ${e}`);
console.error(`Unhandled exception occurred during WebAuthn authentication: ${e}`);
}
}
if (result.credential == null) {
if (result.response == null) {
result.result = AssertionResult.Failure;
} else {
result.result = AssertionResult.Success;
@ -321,84 +190,53 @@ export async function getAssertionPublicKeyCredentialResult(
return result;
}
async function postAttestationPublicKeyCredentialResult(
credential: AttestationPublicKeyCredential,
async function postRegistrationResponse(
response: RegistrationResponseJSON,
description: string,
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
const credentialJSON = encodeAttestationPublicKeyCredential(credential);
const postBody = {
credential: credentialJSON,
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, {
response: response,
description: description,
};
return axios.post<OptionalDataServiceResponse<any>>(WebauthnAttestationPath, postBody);
});
}
export async function postAssertionPublicKeyCredentialResult(
credential: PublicKeyCredential,
export async function postAuthenticationResponse(
response: AuthenticationResponseJSON,
targetURL: string | undefined,
workflow?: string,
workflowID?: string,
): Promise<AxiosResponse<ServiceResponse<SignInResponse>>> {
const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL, workflow, workflowID);
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, credentialJSON);
) {
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, {
response: response,
targetURL: targetURL,
workflow: workflow,
workflowID: workflowID,
});
}
export async function finishAttestationCeremony(
credential: AttestationPublicKeyCredential,
description: string,
): Promise<AttestationResult> {
export async function finishRegistration(response: RegistrationResponseJSON, description: string) {
let result = {
status: AttestationResult.Failure,
message: "Device registration failed.",
} as AttestationResult;
};
try {
const response = await postAttestationPublicKeyCredentialResult(credential, description);
if (response.data.status === "OK" && (response.status === 200 || response.status === 201)) {
const resp = await postRegistrationResponse(response, description);
if (resp.data.status === "OK" && (resp.status === 200 || resp.status === 201)) {
return {
status: AttestationResult.Success,
} as AttestationFinishResult;
message: "",
};
}
} catch (error) {
if (error instanceof AxiosError) {
if (error instanceof AxiosError && error.response !== undefined) {
result.message = error.response.data.message;
}
}
return result;
}
export async function performAssertionCeremony(
targetURL?: string,
workflow?: string,
workflowID?: string,
): Promise<AssertionResult> {
const assertionRequestOpts = await getAssertionRequestOptions();
if (assertionRequestOpts.status !== 200 || assertionRequestOpts.options == null) {
return AssertionResult.FailureChallenge;
}
const assertionResult = await getAssertionPublicKeyCredentialResult(assertionRequestOpts.options);
if (assertionResult.result !== AssertionResult.Success) {
return assertionResult.result;
} else if (assertionResult.credential == null) {
return AssertionResult.Failure;
}
const response = await postAssertionPublicKeyCredentialResult(
assertionResult.credential,
targetURL,
workflow,
workflowID,
);
if (response.data.status === "OK" && response.status === 200) {
return AssertionResult.Success;
}
return AssertionResult.Failure;
}
export async function deleteDevice(deviceID: string) {
return await axios<AuthenticationOKResponse>({
method: "DELETE",

View File

@ -1,209 +0,0 @@
/*
This file is a work taken from the following location: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
MIT License
Copyright (c) 2020 Egor Nepomnyaschih
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
// This constant can also be computed with the following algorithm:
const base64Chars = [],
A = "A".charCodeAt(0),
a = "a".charCodeAt(0),
n = "0".charCodeAt(0);
for (let i = 0; i < 26; ++i) {
base64Chars.push(String.fromCharCode(A + i));
}
for (let i = 0; i < 26; ++i) {
base64Chars.push(String.fromCharCode(a + i));
}
for (let i = 0; i < 10; ++i) {
base64Chars.push(String.fromCharCode(n + i));
}
base64Chars.push("+");
base64Chars.push("/");
*/
const base64Chars = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"+",
"/",
];
/*
// This constant can also be computed with the following algorithm:
const l = 256, base64codes = new Uint8Array(l);
for (let i = 0; i < l; ++i) {
base64codes[i] = 255; // invalid character
}
base64Chars.forEach((char, index) => {
base64codes[char.charCodeAt(0)] = index;
});
base64codes["=".charCodeAt(0)] = 0; // ignored anyway, so we just need to prevent an error
*/
const base64Codes = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255,
255, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
];
function getBase64Code(charCode: number) {
if (charCode >= base64Codes.length) {
throw new Error("Unable to parse base64 string.");
}
const code = base64Codes[charCode];
if (code === 255) {
throw new Error("Unable to parse base64 string.");
}
return code;
}
export function getBase64FromBytes(bytes: number[] | Uint8Array): string {
let result = "",
i,
l = bytes.length;
for (i = 2; i < l; i += 3) {
result += base64Chars[bytes[i - 2] >> 2];
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += base64Chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)];
result += base64Chars[bytes[i] & 0x3f];
}
if (i === l + 1) {
// 1 octet yet to write
result += base64Chars[bytes[i - 2] >> 2];
result += base64Chars[(bytes[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) {
// 2 octets yet to write
result += base64Chars[bytes[i - 2] >> 2];
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += base64Chars[(bytes[i - 1] & 0x0f) << 2];
result += "=";
}
return result;
}
export function getBase64WebEncodingFromBytes(bytes: number[] | Uint8Array): string {
return getBase64FromBytes(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
export function getBytesFromBase64(str: string): Uint8Array {
if (str.length % 4 !== 0) {
throw new Error("Unable to parse base64 string.");
}
const index = str.indexOf("=");
if (index !== -1 && index < str.length - 2) {
throw new Error("Unable to parse base64 string.");
}
let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0,
n = str.length,
result = new Uint8Array(3 * (n / 4)),
buffer;
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
buffer =
(getBase64Code(str.charCodeAt(i)) << 18) |
(getBase64Code(str.charCodeAt(i + 1)) << 12) |
(getBase64Code(str.charCodeAt(i + 2)) << 6) |
getBase64Code(str.charCodeAt(i + 3));
result[j] = buffer >> 16;
result[j + 1] = (buffer >> 8) & 0xff;
result[j + 2] = buffer & 0xff;
}
return result.subarray(0, result.length - missingOctets);
}

View File

@ -2,6 +2,7 @@ import React, { Fragment, MutableRefObject, useCallback, useEffect, useRef, useS
import { Box, Button, Grid, Stack, Step, StepLabel, Stepper, Theme, Typography } from "@mui/material";
import makeStyles from "@mui/styles/makeStyles";
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
@ -10,16 +11,15 @@ import InformationIcon from "@components/InformationIcon";
import SuccessIcon from "@components/SuccessIcon";
import WebauthnTryIcon from "@components/WebauthnTryIcon";
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
import { IdentityToken } from "@constants/SearchParams";
import { useNotifications } from "@hooks/NotificationsContext";
import { useQueryParam } from "@hooks/QueryParam";
import LoginLayout from "@layouts/LoginLayout";
import { AttestationPublicKeyCredential, AttestationResult, WebauthnTouchState } from "@models/Webauthn";
import {
finishAttestationCeremony,
getAttestationCreationOptions,
getAttestationPublicKeyCredentialResult,
} from "@services/Webauthn";
AttestationResult,
AttestationResultFailureString,
RegistrationResult,
WebauthnTouchState,
} from "@models/Webauthn";
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
const steps = ["Confirm device", "Choose name"];
@ -33,83 +33,60 @@ const RegisterWebauthn = function (props: Props) {
const { createErrorNotification } = useNotifications();
const [activeStep, setActiveStep] = React.useState(0);
const [credential, setCredential] = React.useState(null as null | AttestationPublicKeyCredential);
const [creationOptions, setCreationOptions] = useState(null as null | PublicKeyCredentialCreationOptions);
const [result, setResult] = React.useState(null as null | RegistrationResult);
const [options, setOptions] = useState(null as null | PublicKeyCredentialCreationOptionsJSON);
const [deviceName, setName] = useState("");
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
const [nameError, setNameError] = useState(false);
const processToken = useQueryParam(IdentityToken);
const handleBackClick = () => {
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
};
const finishAttestation = async () => {
if (!credential) {
if (!result || !result.response) {
return;
}
if (!deviceName.length) {
setNameError(true);
return;
}
const result = await finishAttestationCeremony(credential, deviceName);
switch (result.status) {
const res = await finishRegistration(result.response, deviceName);
switch (res.status) {
case AttestationResult.Success:
setActiveStep(2);
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
break;
case AttestationResult.Failure:
createErrorNotification(result.message);
createErrorNotification(res.message);
}
};
const startAttestation = useCallback(async () => {
const startRegistration = useCallback(async () => {
if (options === null) {
return;
}
try {
setState(WebauthnTouchState.WaitTouch);
setActiveStep(0);
const startResult = await getAttestationPublicKeyCredentialResult(creationOptions);
const res = await startWebauthnRegistration(options);
switch (startResult.result) {
case AttestationResult.Success:
if (startResult.credential == null) {
if (res.result === AttestationResult.Success) {
if (res.response == null) {
throw new Error("Attestation request succeeded but credential is empty");
}
setCredential(startResult.credential);
setResult(res);
setActiveStep(1);
return;
case AttestationResult.FailureToken:
createErrorNotification(
"You must open the link from the same device and browser that initiated the registration process.",
);
break;
case AttestationResult.FailureSupport:
createErrorNotification("Your browser does not appear to support the configuration.");
break;
case AttestationResult.FailureSyntax:
createErrorNotification(
"The attestation challenge was rejected as malformed or incompatible by your browser.",
);
break;
case AttestationResult.FailureWebauthnNotSupported:
createErrorNotification("Your browser does not support the WebAuthN protocol.");
break;
case AttestationResult.FailureUserConsent:
createErrorNotification("You cancelled the attestation request.");
break;
case AttestationResult.FailureUserVerificationOrResidentKey:
createErrorNotification(
"Your device does not support user verification or resident keys but this was required.",
);
break;
case AttestationResult.FailureExcluded:
createErrorNotification("You have registered this device already.");
break;
case AttestationResult.FailureUnknown:
createErrorNotification("An unknown error occurred.");
break;
}
createErrorNotification(AttestationResultFailureString(res.result));
setState(WebauthnTouchState.Failure);
} catch (err) {
console.error(err);
@ -117,26 +94,26 @@ const RegisterWebauthn = function (props: Props) {
"Failed to register your device. The identity verification process might have timed out.",
);
}
}, [creationOptions, createErrorNotification]);
}, [options, createErrorNotification]);
useEffect(() => {
if (creationOptions !== null) {
startAttestation();
if (options !== null) {
startRegistration();
}
}, [creationOptions, startAttestation]);
}, [options, startRegistration]);
useEffect(() => {
(async () => {
const result = await getAttestationCreationOptions(processToken);
if (result.status !== 200 || !result.options) {
const res = await getAttestationCreationOptions();
if (res.status !== 200 || !res.options) {
createErrorNotification(
"You must open the link from the same device and browser that initiated the registration process.",
);
return;
}
setCreationOptions(result.options);
setOptions(res.options);
})();
}, [processToken, setCreationOptions, createErrorNotification]);
}, [setOptions, createErrorNotification]);
function renderStep(step: number) {
switch (step) {
@ -144,10 +121,10 @@ const RegisterWebauthn = function (props: Props) {
return (
<Fragment>
<div className={styles.icon}>
<WebauthnTryIcon onRetryClick={startAttestation} webauthnTouchState={state} />
<WebauthnTryIcon onRetryClick={startRegistration} webauthnTouchState={state} />
</div>
<Typography className={styles.instruction}>Touch the token on your security key</Typography>
<Grid container align="center" spacing={1}>
<Grid container spacing={1}>
<Grid item xs={12}>
<Stack direction="row" spacing={1} justifyContent="center">
<Button color="primary" onClick={handleBackClick}>
@ -196,7 +173,7 @@ const RegisterWebauthn = function (props: Props) {
</Grid>
<Grid item xs={12}>
<Stack direction="row" spacing={1} justifyContent="center">
<Button color="primary" variant="outlined" onClick={startAttestation}>
<Button color="primary" variant="outlined" onClick={startRegistration}>
Back
</Button>
<Button color="primary" variant="contained" onClick={finishAttestation}>

View File

@ -5,13 +5,9 @@ import { RedirectionURL } from "@constants/SearchParams";
import { useIsMountedRef } from "@hooks/Mounted";
import { useQueryParam } from "@hooks/QueryParam";
import { useWorkflow } from "@hooks/Workflow";
import { AssertionResult, WebauthnTouchState } from "@models/Webauthn";
import { AssertionResult, AssertionResultFailureString, WebauthnTouchState } from "@models/Webauthn";
import { AuthenticationLevel } from "@services/State";
import {
getAssertionPublicKeyCredentialResult,
getAssertionRequestOptions,
postAssertionPublicKeyCredentialResult,
} from "@services/Webauthn";
import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/Webauthn";
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
export interface Props {
@ -42,55 +38,28 @@ const WebauthnMethod = function (props: Props) {
try {
setState(WebauthnTouchState.WaitTouch);
const assertionRequestResponse = await getAssertionRequestOptions();
const optionsStatus = await getAuthenticationOptions();
if (assertionRequestResponse.status !== 200 || assertionRequestResponse.options == null) {
if (optionsStatus.status !== 200 || optionsStatus.options == null) {
setState(WebauthnTouchState.Failure);
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
return;
}
const result = await getAssertionPublicKeyCredentialResult(assertionRequestResponse.options);
const result = await getAuthenticationResult(optionsStatus.options);
if (result.result !== AssertionResult.Success) {
if (!mounted.current) return;
switch (result.result) {
case AssertionResult.FailureUserConsent:
onSignInErrorCallback(new Error("You cancelled the assertion request."));
break;
case AssertionResult.FailureU2FFacetID:
onSignInErrorCallback(new Error("The server responded with an invalid Facet ID for the URL."));
break;
case AssertionResult.FailureSyntax:
onSignInErrorCallback(
new Error(
"The assertion challenge was rejected as malformed or incompatible by your browser.",
),
);
break;
case AssertionResult.FailureWebauthnNotSupported:
onSignInErrorCallback(new Error("Your browser does not support the WebAuthN protocol."));
break;
case AssertionResult.FailureUnrecognized:
onSignInErrorCallback(new Error("This device is not registered."));
break;
case AssertionResult.FailureUnknownSecurity:
onSignInErrorCallback(new Error("An unknown security error occurred."));
break;
case AssertionResult.FailureUnknown:
onSignInErrorCallback(new Error("An unknown error occurred."));
break;
default:
onSignInErrorCallback(new Error("An unexpected error occurred."));
break;
}
setState(WebauthnTouchState.Failure);
onSignInErrorCallback(new Error(AssertionResultFailureString(result.result)));
return;
}
if (result.credential == null) {
if (result.response == null) {
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
setState(WebauthnTouchState.Failure);
@ -101,12 +70,7 @@ const WebauthnMethod = function (props: Props) {
setState(WebauthnTouchState.InProgress);
const response = await postAssertionPublicKeyCredentialResult(
result.credential,
redirectionURL,
workflow,
workflowID,
);
const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID);
if (response.data.status === "OK" && response.status === 200) {
onSignInSuccessCallback(response.data.data ? response.data.data.redirect : undefined);