/// <summary>
        /// GetCredentialCreateOptions method implementation (RequestNewCredential)
        /// </summary>
        /// <returns>CredentialCreateOptions including a challenge to be sent to the browser/authr to create new credentials</returns>
        /// <param name="attestationPreference">This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance. The default is none.</param>
        /// <param name="excludeCredentials">Recommended. This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.</param>
        public CredentialCreateOptions GetRegisterCredentialOptions(Fido2User user, List <PublicKeyCredentialDescriptor> excludeCredentials, AuthenticatorSelection authenticatorSelection, AttestationConveyancePreference attestationPreference, AuthenticationExtensionsClientInputs extensions = null)
        {
            var challenge = new byte[_config.ChallengeSize];

            _crypto.GetBytes(challenge);

            var options = CredentialCreateOptions.Create(_config, challenge, user, authenticatorSelection, attestationPreference, excludeCredentials, extensions);

            return(options);
        }
        public async Task <AttestationVerificationSuccess> VerifyAsync(CredentialCreateOptions originalOptions, Fido2Configuration config, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, IMetadataService metadataService, byte[] requestTokenBindingId)
        {
            // https://www.w3.org/TR/webauthn/#registering-a-new-credential
            // 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON.
            // 2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext.
            // Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm.
            // Above handled in base class constructor

            // 3. Verify that the value of C.type is webauthn.create
            if (Type != "webauthn.create")
            {
                throw new VerificationException("AttestationResponse is not type webauthn.create");
            }

            // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
            // 5. Verify that the value of C.origin matches the Relying Party's origin.
            // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.
            // If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
            BaseVerify(config.Origin, originalOptions.Challenge, requestTokenBindingId);

            if (Raw.Id == null || Raw.Id.Length == 0)
            {
                throw new VerificationException("AttestationResponse is missing Id");
            }

            if (Raw.Type != PublicKeyCredentialType.PublicKey)
            {
                throw new VerificationException("AttestationResponse is missing type with value 'public-key'");
            }

            var authData = new AuthenticatorData(AttestationObject.AuthData);

            // 7. Compute the hash of response.clientDataJSON using SHA-256.
            byte[] clientDataHash, rpIdHash;
            using (var sha = CryptoUtils.GetHasher(HashAlgorithmName.SHA256))
            {
                clientDataHash = sha.ComputeHash(Raw.Response.ClientDataJson);
                rpIdHash       = sha.ComputeHash(Encoding.UTF8.GetBytes(originalOptions.Rp.Id));
            }

            // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation statement attStmt.
            // Handled in AuthenticatorAttestationResponse::Parse()

            // 9. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party
            if (false == authData.RpIdHash.SequenceEqual(rpIdHash))
            {
                throw new VerificationException("Hash mismatch RPID");
            }

            // 10. Verify that the User Present bit of the flags in authData is set.
            if (false == authData.UserPresent)
            {
                throw new VerificationException("User Present flag not set in authenticator data");
            }

            // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
            // see authData.UserVerified
            // TODO: Make this a configurable option and add check to require

            // 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected,
            // considering the client extension input values that were given as the extensions option in the create() call.  In particular, any extension identifier values
            // in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e.,
            // no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.

            // TODO?: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x);

            if (false == authData.HasAttestedCredentialData)
            {
                throw new VerificationException("Attestation flag not set on attestation data");
            }

            // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values.
            // An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name
            // https://www.w3.org/TR/webauthn/#defined-attestation-formats
            AttestationVerifier verifier = default;

            switch (AttestationObject.Fmt)
            {
            case "none":
                verifier = new None();                  // https://www.w3.org/TR/webauthn/#none-attestation
                break;

            case "tpm":
                verifier = new Tpm();                   // https://www.w3.org/TR/webauthn/#tpm-attestation
                break;

            case "android-key":
                verifier = new AndroidKey();            // https://www.w3.org/TR/webauthn/#android-key-attestation
                break;

            case "android-safetynet":
                verifier = new AndroidSafetyNet(metadataService.TimestampDriftTolerance);      // https://www.w3.org/TR/webauthn/#android-safetynet-attestation
                break;

            case "fido-u2f":
                verifier = new FidoU2f();               // https://www.w3.org/TR/webauthn/#fido-u2f-attestation
                break;

            case "packed":
                verifier = new Packed();                // https://www.w3.org/TR/webauthn/#packed-attestation
                break;

            case "apple":
                verifier = new Apple();                 // https://www.w3.org/TR/webauthn/#apple-anonymous-attestation
                break;

            default:
                throw new VerificationException("Missing or unknown attestation type");
            }
            ;

            // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature,
            // by using the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized client data computed in step 7
            (var attType, var trustPath) = verifier.Verify(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash);

            // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from a trusted source or from policy.
            // For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
            var entry = metadataService?.GetEntry(authData.AttestedCredentialData.AaGuid);

            // while conformance testing, we must reject any authenticator that we cannot get metadata for
            if (metadataService?.ConformanceTesting() == true && null == entry && AttestationType.None != attType && "fido-u2f" != AttestationObject.Fmt)
            {
                throw new VerificationException("AAGUID not found in MDS test metadata");
            }

            if (null != trustPath)
            {
                // If the authenticator is listed as in the metadata as one that should produce a basic full attestation, build and verify the chain
                if ((entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_BASIC_FULL.ToEnumMemberValue()) ?? false) ||
                    (entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_PRIVACY_CA.ToEnumMemberValue()) ?? false))
                {
                    var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates
                                                      .Select(x => new X509Certificate2(Convert.FromBase64String(x)))
                                                      .ToArray();

                    if (false == CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates))
                    {
                        throw new VerificationException("Invalid certificate chain");
                    }
                }

                // If the authenticator is not listed as one that should produce a basic full attestation, the certificate should be self signed
                if ((!entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_BASIC_FULL.ToEnumMemberValue()) ?? false) &&
                    (!entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_PRIVACY_CA.ToEnumMemberValue()) ?? false) &&
                    (!entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_ANONCA.ToEnumMemberValue()) ?? false))
                {
                    if (trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer)
                    {
                        throw new VerificationException("Attestation with full attestation from authenticator that does not support full attestation");
                    }
                }
            }

            // Check status resports for authenticator with undesirable status
            foreach (var report in entry?.StatusReports ?? Enumerable.Empty <StatusReport>())
            {
                if (true == Enum.IsDefined(typeof(UndesiredAuthenticatorStatus), (UndesiredAuthenticatorStatus)report.Status))
                {
                    throw new VerificationException("Authenticator found with undesirable status");
                }
            }

            // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows:
            // If self attestation was used, check if self attestation is acceptable under Relying Party policy.
            // If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the set of acceptable trust anchors obtained in step 15.
            // Otherwise, use the X.509 certificates returned by the verification procedure to verify that the attestation public key correctly chains up to an acceptable root certificate.

            // 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
            if (false == await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User)))
            {
                throw new VerificationException("CredentialId is not unique to this user");
            }

            // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the new credential with the account that was denoted in the options.user passed to create(),
            // by associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as appropriate for the Relying Party's system.
            var result = new AttestationVerificationSuccess()
            {
                CredentialId = authData.AttestedCredentialData.CredentialID,
                PublicKey    = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(),
                User         = originalOptions.User,
                Counter      = authData.SignCount,
                CredType     = AttestationObject.Fmt,
                Aaguid       = authData.AttestedCredentialData.AaGuid,
            };

            return(result);
            // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, the Relying Party SHOULD fail the registration ceremony.
            // This implementation throws if the outputs are not trustworthy for a particular attestation type.
        }
        /// <summary>
        /// SetRegisterCredentialResult method implementation
        /// </summary>
        private int SetRegisterCredentialResult(AuthenticationContext ctx, string jsonResponse, out string error)
        {
            bool isDeserialized = false;

            try
            {
                string jsonOptions = ctx.CredentialOptions;
                if (string.IsNullOrEmpty(jsonOptions))
                {
                    throw new ArgumentNullException(jsonOptions);
                }
                if (string.IsNullOrEmpty(jsonResponse))
                {
                    throw new ArgumentNullException(jsonResponse);
                }

                MFAWebAuthNUser user = RuntimeRepository.GetUser(Config, ctx.UPN);
                if (user != null)
                {
                    CredentialCreateOptions options = CredentialCreateOptions.FromJson(jsonOptions);

#pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
                    IsCredentialIdUniqueToUserAsyncDelegate callback = async(args) =>
#pragma warning restore CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
                    {
                        var users = RuntimeRepository.GetUsersByCredentialId(Config, user, args.CredentialId);
                        if (users.Count > 0)
                        {
                            return(false);
                        }
                        return(true);
                    };
                    AuthenticatorAttestationRawResponse attestationResponse = JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(jsonResponse);
                    isDeserialized = true;
                    CredentialMakeResult success = _webathn.SetRegisterCredentialResult(attestationResponse, options, callback).Result;

                    RuntimeRepository.AddUserCredential(Config, user, new MFAUserCredential
                    {
                        Descriptor       = new MFAPublicKeyCredentialDescriptor(success.Result.CredentialId),
                        PublicKey        = success.Result.PublicKey,
                        UserHandle       = success.Result.User.Id,
                        SignatureCounter = success.Result.Counter,
                        CredType         = success.Result.CredType,
                        RegDate          = DateTime.Now,
                        AaGuid           = success.Result.Aaguid,
                        NickName         = ctx.NickName
                    });
                    error = string.Empty;
                    return((int)AuthenticationResponseKind.Biometrics);
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), System.Diagnostics.EventLogEntryType.Error, 5000);
                    error = string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !");
                    return((int)AuthenticationResponseKind.Error);
                }
            }
            catch (Exception e)
            {
                if (isDeserialized)
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), EventLogEntryType.Error, 5000);
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, jsonResponse), EventLogEntryType.Error, 5000);
                }
                error = e.Message;
                return((int)AuthenticationResponseKind.Error);
            }
        }
        /// <summary>
        /// GetRegisterCredentialOptions method implementation
        /// </summary>
        private string GetRegisterCredentialOptions(AuthenticationContext ctx)
        {
            try
            {
                if (string.IsNullOrEmpty(ctx.UPN))
                {
                    throw new ArgumentNullException(ctx.UPN);
                }
                string attType  = this.ConveyancePreference;                                      // none, direct, indirect
                string authType = this.Attachement;                                               // <empty>, platform, cross-platform
                UserVerificationRequirement userVerification = this.UserVerificationRequirement;  // preferred, required, discouraged
                bool requireResidentKey = this.RequireResidentKey;                                // true,false

                MFAWebAuthNUser user = RuntimeRepository.GetUser(Config, ctx.UPN);
                if (user != null)
                {
                    List <MFAPublicKeyCredentialDescriptor> existingKeys = RuntimeRepository.GetCredentialsByUser(Config, user).Select(c => c.Descriptor).ToList();

                    // 3. Create options
                    AuthenticatorSelection authenticatorSelection = new AuthenticatorSelection
                    {
                        RequireResidentKey = requireResidentKey,
                        UserVerification   = userVerification
                    };
                    if (!string.IsNullOrEmpty(authType))
                    {
                        authenticatorSelection.AuthenticatorAttachment = authType.ToEnum <AuthenticatorAttachment>();
                    }

                    AuthenticationExtensionsClientInputs exts = new AuthenticationExtensionsClientInputs()
                    {
                        Extensions             = this.Extentions,
                        UserVerificationMethod = this.UserVerificationMethod
                    };
                    CredentialCreateOptions options = null;

                    if (existingKeys.Count > 0)
                    {
                        options = _webathn.GetRegisterCredentialOptions(user.ToCore(), existingKeys.ToCore(), authenticatorSelection, attType.ToEnum <AttestationConveyancePreference>(), exts);
                    }
                    else
                    {
                        options = _webathn.GetRegisterCredentialOptions(user.ToCore(), null, authenticatorSelection, attType.ToEnum <AttestationConveyancePreference>(), exts);
                    }
                    string result = options.ToJson();
                    ctx.CredentialOptions = result;
                    return(result);
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), EventLogEntryType.Error, 5000);
                    string result = (new CredentialMakeResult {
                        Status = "error", ErrorMessage = string.Format("{0}", "User does not exists !")
                    }).ToJson();
                    ctx.CredentialOptions = result;
                    return(result);
                }
            }
            catch (Exception e)
            {
                Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), System.Diagnostics.EventLogEntryType.Error, 5000);
                string result = (new CredentialMakeResult {
                    Status = "error", ErrorMessage = string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "")
                }).ToJson();
                ctx.CredentialOptions = result;
                return(result);
            }
        }
        /// <summary>
        /// Verifies the response from the browser/authr after creating new credentials (MakeNewCredentialAsync)
        /// </summary>
        /// <param name="attestationResponse"></param>
        /// <param name="origChallenge"></param>
        /// <returns></returns>
        public async Task <CredentialMakeResult> SetRegisterCredentialResult(AuthenticatorAttestationRawResponse attestationResponse, CredentialCreateOptions origChallenge, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, byte[] requestTokenBindingId = null)
        {
            var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
            var success        = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId);

            try
            {
                return(new CredentialMakeResult
                {
                    Status = "ok",
                    ErrorMessage = string.Empty,
                    Result = success
                });
            }
            catch (Exception e)
            {
                return(new CredentialMakeResult
                {
                    Status = "error",
                    ErrorMessage = e.Message,
                    Result = success
                });
            }
        }