While preparing this project and the post, Google’s passkey tutorial and Fido2 open source libraries are used.

https://developers.google.com/identity/passkeys
https://github.com/passwordless-lib/fido2-net-lib/tree/master

Also see the full project and star it to support me:

https://github.com/ycanatilgan/Authentication-Playground

Most people hate passwords, and they have good reasons. They must be complicated, unique, and kept securely by the user. Moreover, users have to prove their identity with a second step, which makes the authentication process harder. After all, it is not something user-friendly.

Passwords also cause security issues as well as practical issues. As of April 2024, according to FIDO Alliance, 81% of hacking-related breaches are caused by weak or stolen passwords. Also, there has been a 1,265% rise in malicious phishing emails since Q4 2022.

Let’s be realistic, passwords are not going to be wiped out any soon. But now we have an alternative! Yes, it does exist and is coming fast. Thanks to the Fido Alliance, a new authentication standard is coming: Passkeys. With passkeys, your users do not have to keep or even know their passwords, in a secure way. Many big companies already integrated it into their services.

An appropriate private key is created and kept secure by the client’s OS, and the app’s database keeps only the public key. When users want to authenticate, the users’ OS does the authentication – via biometrics or other type of screen lock – and gives the app a signature. The app only needs to verify the signature, and voila, easy and secure solution. Moreover, it can even work across devices via Bluetooth!

Since users don’t know what is their secret private key, it is also phishing-resistant.

Public-Private Key

So before diving in and implementing this awesome solution, let’s talk about the basics first. What exactly is the public-private key pair or public-key cryptography? As we know, typical cryptography involves encrypting and decrypting data with a single key. With a password you have, you lock and unlock, like door keys. But on the web, we sometimes don’t want to share our door key but still want to prove that it is our door and that we have its key. This is when public-key cryptography comes in.

Public-key cryptography relies on a one-way function. A secret, or private key, is created first. The one-way function uses complicated algorithms to create an output, which is called a public key. As the name implies public key is not a secret and can be known by anyone. What makes it special is it can only be created with the private key you have, so third parties can verify the signature and make sure it is you.

This is also used in passkeys. The user’s device keeps the private key, and the app keeps the public key. This is how you can authenticate users without keeping the “secret”. A similar mechanism is used for regular passwords as well, but the good thing about passkeys are the secret is kept and managed by the user’s devices and never even be known by the user itself.

General Usage

All the great things about the passkeys aside, the web is still in the adoption stage. As of May 2024, most services still force you to have a password even if you activate passkeys in your account. So you may want to implement it in the same way. Let your users create passkeys but they should still be able to log in via their passwords.

Secondly, passkeys are designed in a way that multi-factor authentication isn’t required. So let your users enjoy authenticating in a single step!

Packages

Firstly, let’s install the NuGet packages into our project:

Libraries

These libraries already exist in the Fido2 libraries, but I modified them for this use.

using Authentication_Playground_.Data;
using Authentication_Playground_.Models;
using Fido2NetLib;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Fido2Identity
{
    public class Fido2Storage
    {
        AppDbContext _dbContext;

        public Fido2Storage(AppDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public List<FidoStoredCredential> GetCredentialsByUsername(string username)
        {
            return _dbContext.FidoStoredCredentials.Where(c => c.Username == username).ToList();
        }

        public async Task RemoveCredentialsByUsername(string username)
        {
            var item = await _dbContext.FidoStoredCredentials.Where(c => c.Username == username).FirstOrDefaultAsync();
            if (item != null)
            {
                _dbContext.FidoStoredCredentials.Remove(item);
                await _dbContext.SaveChangesAsync();
            }
        }

        public async Task RemoveCredentialsById(int id)
        {
            var item = await _dbContext.FidoStoredCredentials.Where(c => c.ID == id).FirstOrDefaultAsync();
            if (item != null)
            {
                _dbContext.FidoStoredCredentials.Remove(item);
                await _dbContext.SaveChangesAsync();
            }
        }

        public async Task<FidoStoredCredential> GetCredentialById(byte[] id)
        {
            var credentialIdString = Base64Url.Encode(id);
            credentialIdString += "=";
            credentialIdString = credentialIdString.Replace('-', '+');
            credentialIdString = credentialIdString.Replace('_', '/');
            //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString);

            var cred = await _dbContext.FidoStoredCredentials
                .Where(c => c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

            return cred;
        }

        public Task<List<FidoStoredCredential>> GetCredentialsByUserHandleAsync(byte[] userHandle)
        {
            return Task.FromResult(_dbContext.FidoStoredCredentials.Where(c => c.UserHandle.SequenceEqual(userHandle)).ToList());
        }

        public async Task UpdateCounter(byte[] credentialId, uint counter)
        {
            var credentialIdString = Base64Url.Encode(credentialId);
            credentialIdString += "=";
            credentialIdString = credentialIdString.Replace('-', '+');
            credentialIdString = credentialIdString.Replace('_', '/');

            var cred = await _dbContext.FidoStoredCredentials
                .Where(c => c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

            cred.SignatureCounter = counter;
            cred.LastLogin = DateTime.Now;
            await _dbContext.SaveChangesAsync();
        }

        public async Task AddCredentialToUser(Fido2User user, FidoStoredCredential credential)
        {
            credential.UserId = user.Id;
            _dbContext.FidoStoredCredentials.Add(credential);
            await _dbContext.SaveChangesAsync();
        }

        public async Task<List<Fido2User>> GetUsersByCredentialIdAsync(byte[] credentialId)
        {
            var credentialIdString = Base64Url.Encode(credentialId);
            //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString);

            var cred = await _dbContext.FidoStoredCredentials
                .Where(c => c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

            if (cred == null)
            {
                return new List<Fido2User>();
            }

            return await _dbContext.Users
                .Where(u => Encoding.UTF8.GetBytes(u.UserHandle)
                .SequenceEqual(cred.UserId))
                .Select(u => new Fido2User
                {
                    DisplayName = u.Username,
                    Name = u.Username,
                    Id = Encoding.UTF8.GetBytes(u.UserHandle) // byte representation of userID is required
                }).ToListAsync();
        }
    }
}

using Fido2NetLib.Objects;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace Authentication_Playground_.Models
{
    public class FidoStoredCredential
    {
        public int ID { get; set; }
        public string Username { get; set; }
        public byte[] UserId { get; set; }
        public byte[] PublicKey { get; set; }
        public byte[] UserHandle { get; set; }
        public uint SignatureCounter { get; set; }
        public string CredType { get; set; }
        public DateTime RegDate { get; set; }
        public DateTime LastLogin { get; set; }
        public Guid AaGuid { get; set; }
        public string DeviceInfo { get; set; }

        [NotMapped]
        public PublicKeyCredentialDescriptor Descriptor
        {
            get { return string.IsNullOrWhiteSpace(DescriptorJson) ? null : JsonConvert.DeserializeObject<PublicKeyCredentialDescriptor>(DescriptorJson); }
            set { DescriptorJson = JsonConvert.SerializeObject(value); }
        }
        public string DescriptorJson { get; set; }
    }
}

Implementation

Let’s start coding! We first need to configure the middleware. Add this block to the program.cs before var app = builder.Build();

#region FIDO2
var fido2Configuration = new Fido2Configuration
{
    ServerDomain = "ycanindev.com",
    ServerName = "ycan in dev",
    Origin = "https://ycanindev.com",
    TimestampDriftTolerance = 300000,
    MDSCacheDirPath = null // Set this property if needed
};

// Configure the Fido2Configuration service with a delegate that configures the provided instance
builder.Services.Configure<Fido2Configuration>(options =>
{
    options.ServerDomain = fido2Configuration.ServerDomain;
    options.ServerName = fido2Configuration.ServerName;
    options.Origin = fido2Configuration.Origin;
    options.TimestampDriftTolerance = fido2Configuration.TimestampDriftTolerance;
    options.MDSCacheDirPath = fido2Configuration.MDSCacheDirPath;
});

// Add the configuration as a singleton service
builder.Services.AddSingleton(fido2Configuration);
#endregion

Replace the server domain and origin with yours. There are restrictions on how you can use them: https://www.w3.org/TR/webauthn-2/#rp-id
If you are working on the localhost,

Secondly, let’s prepare the backend for the registration.

#region Initializers and  variables
private readonly AppDbContext _dbContext;

private readonly Fido2 _lib;
private readonly IOptions<Fido2Configuration> _optionsFido2Configuration;

private readonly Fido2Storage fido2Storage;

public Fido2Controller(AppDbContext dbContext, IOptions<Fido2Configuration> optionsFido2Configuration) 
{ 
    _dbContext = dbContext;

    _optionsFido2Configuration = optionsFido2Configuration;
    _lib = new Fido2(new Fido2Configuration()
    {
        ServerDomain = _optionsFido2Configuration.Value.ServerDomain,
        ServerName = _optionsFido2Configuration.Value.ServerName,
        Origin = _optionsFido2Configuration.Value.Origin,
        TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance
    });

    fido2Storage = new Fido2Storage(dbContext);
}
#endregion

#region REGISTER PASSKEY
[HttpPost]
public IActionResult RegisterRequest()
{
    if (HttpContext.Session.GetInt32("UserId").HasValue)
    {
        Users? user = _dbContext.Users.Where(s => s.Id == HttpContext.Session.GetInt32("UserId").Value).FirstOrDefault();

        if (user == null)
            return BadRequest();

        if(user.UserHandle == null)
        {
            byte[] userIdBytes = new byte[32];
            new Random().NextBytes(userIdBytes);
            string userIdStr = Convert.ToBase64String(userIdBytes);

            user.UserHandle = userIdStr;
            _dbContext.SaveChanges();
        }

        var userHandle = Convert.FromBase64String(user.UserHandle);

        var fidoUser = new Fido2User
        {
            DisplayName = user.Username,
            Name = user.Username,
            Id = userHandle
        };

        // 2. Get user existing keys by user
        var items = fido2Storage.GetCredentialsByUsername(user.Username);
        var existingKeys = new List<PublicKeyCredentialDescriptor>();
        foreach (var publicKeyCredentialDescriptor in items)
        {
            existingKeys.Add(publicKeyCredentialDescriptor.Descriptor);
        }

        // 3. Create options
        var authenticatorSelection = new AuthenticatorSelection
        {
            RequireResidentKey = false,
            UserVerification = UserVerificationRequirement.Required
        };

        authenticatorSelection.AuthenticatorAttachment = AuthenticatorAttachment.Platform;

        var exts = new AuthenticationExtensionsClientInputs() { Extensions = true };

        var options = _lib.RequestNewCredential(fidoUser, existingKeys, authenticatorSelection, AttestationConveyancePreference.Direct, exts);
        options.Rp = new PublicKeyCredentialRpEntity("localhost", "YcanInDev", null);

        List<PubKeyCredParam> pubKeyCredParams = new List<PubKeyCredParam>
        {
            new PubKeyCredParam(COSE.Algorithm.ES256, PublicKeyCredentialType.PublicKey),
            new PubKeyCredParam(COSE.Algorithm.RS256, PublicKeyCredentialType.PublicKey)
        };
        options.PubKeyCredParams = pubKeyCredParams;
        // 4. Temporarily store options, session/in-memory cache/redis/db
        HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());

        // 5. return options to client
        return Json(options);
    }
    else
    {
        Response.StatusCode = 401;
        return Json("Unauthorized");
    }
}

[HttpPost]
public async Task<IActionResult> RegisterResponse([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
{
    try
    {
        if (HttpContext.Session.GetInt32("UserId").HasValue != false)
        {
            var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
            var options = CredentialCreateOptions.FromJson(jsonOptions);

            // 2. Create callback so that lib can verify credential id is unique to this user
            async Task<bool> callback(IsCredentialIdUniqueToUserParams args, CancellationToken token)
            {
                var users = await fido2Storage.GetUsersByCredentialIdAsync(args.CredentialId);
                if (users.Count > 0) 
                    return false;

                return true;
            };

            // 2. Verify and make the credentials
            var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

            string deviceInfo = "";
            try
            {
                var userAgent = HttpContext.Request.Headers["User-Agent"];
                var uaParser = Parser.GetDefault();
                ClientInfo c = uaParser.Parse(userAgent);

                deviceInfo = c.OS.Family.ToString();
            }
            catch { }

            // 3. Store the credentials in db
            await fido2Storage.AddCredentialToUser(options.User, new FidoStoredCredential
            {
                Username = options.User.Name,
                Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                PublicKey = success.Result.PublicKey,
                UserHandle = success.Result.User.Id,
                SignatureCounter = success.Result.Counter,
                CredType = success.Result.CredType,
                RegDate = DateTime.Now,
                LastLogin = DateTime.Now,
                AaGuid = success.Result.Aaguid,
                DeviceInfo = deviceInfo
            });

            // 4. return "ok" to the client
            return Ok();
        }
        else
        {
            Response.StatusCode = 401;
            return Json("Unauthorized");
        }
    }
    catch (Exception ex)
    {
        Response.StatusCode = 500;
        return Json("Unexpected error");
    }

}
#endregion

RegisterRequest returns configurations to the browser when user requests it. The browser will pop the native UI and confirms that user wants to create the passkey. If it is successful, it will send server the credential details in the RegisterResponse.

Then, let’s prepare the front-end to allow users to register their passkeys. There are some HTML elements and javascript code blocks. When the user clicks on the create passkey buttons, it will fetch the configurations from the server, and request browser to pop the native UI. If the user completes the steps, it will send the server the details and the server will save the credentials as we discussed.

<div>
    <p id="message" class="instructions"></p>
    <mwc-button id="create-passkey" class="hidden btn btn-primary ms-3 mb-3" icon="fingerprint" raised>
        Create Passkey
    </mwc-button>
</div>

<script type="module" async>

    export const base64url = {
        encode: function (buffer) {
            const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
            return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
        },
        decode: function (base64url) {
            const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
            const binStr = window.atob(base64);
            const bin = new Uint8Array(binStr.length);
            for (let i = 0; i < binStr.length; i++) {
                bin[i] = binStr.charCodeAt(i);
            }
            return bin.buffer;
        }
    }

    export async function _fetch(path, payload = '') {
        const headers = {
            'X-Requested-With': 'XMLHttpRequest',
        };
        if (payload && !(payload instanceof FormData)) {
            headers['Content-Type'] = 'application/json';
            payload = JSON.stringify(payload);
        }
        const res = await fetch(path, {
            method: 'POST',
            credentials: 'same-origin',
            headers: headers,
            body: payload,
        });
        if (res.status === 200) {
            // Server authentication succeeded
            return res.json();
        } else {
            // Server authentication failed
            const result = await res.json();
            throw new Error(result.error);
        }
    };


    const createPasskey = document.getElementById('create-passkey');

    createPasskey.addEventListener('click', registerCredential);

    // Feature detections
    if (window.PublicKeyCredential &&
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
        PublicKeyCredential.isConditionalMediationAvailable) {
        try {
            const results = await Promise.all([

                // Is platform authenticator available in this browser?
                PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

                // Is conditional UI available in this browser?
                PublicKeyCredential.isConditionalMediationAvailable()
            ]);
            if (results.every(r => r === true)) {

                // If conditional UI is available, reveal the Create a passkey button.
                createPasskey.classList.remove('hidden');
            } else {

                // If conditional UI isn't available, show a message.
                document.getElementById('message').textContent = 'This device does not support passkeys';
            }
        } catch (e) {
            console.error(e);
        }
    } else {

        // If WebAuthn isn't available, show a message.
        document.getElementById('message').textContent = 'This device does not support passkeys';
    }

    export async function registerCredential() {

        try {
            // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
            const options = await _fetch('/Fido2/RegisterRequest');
            //console.log(options);
            // TODO: Add an ability to create a passkey: Create a credential.

            if (options.excludeCredentials) {
                for (let cred of options.excludeCredentials) {
                    cred.id = base64url.decode(cred.id);
                }
            }
            options.authenticatorSelection = {
                authenticatorAttachment: 'platform',
                requireResidentKey: true
            }

            options.challenge = base64url.decode(options.challenge);
            options.user.id = base64url.decode(options.user.id);

            let newCredential;
            try {
                newCredential = await navigator.credentials.create({
                    publicKey: options
                });
            } catch (e) {
                var msg = "Could not create credentials in browser. Probably because the username is already registered with your authenticator. Please change username or authenticator."
                //console.error(msg, e);
                alert(msg);
            }

            try {
                registerNewCredential(newCredential);

            } catch (e) {
                alert("Could not create passkey");
            }
        }
        catch (e) {

            if (e.name === 'InvalidStateError') {
                alert("This device already has passkey for this service");


            } else if (e.name === 'NotAllowedError') {
                return;

            } else {
                alert("Could not create passkey");
            }
        }

    };

    async function registerNewCredential(newCredential) {
        // Move data into Arrays incase it is super long
        let attestationObject = new Uint8Array(newCredential.response.attestationObject);
        let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
        let rawId = new Uint8Array(newCredential.rawId);

        const data = {
            id: newCredential.id,
            rawId: coerceToBase64Url(rawId),
            type: newCredential.type,
            extensions: newCredential.getClientExtensionResults(),
            response: {
                AttestationObject: coerceToBase64Url(attestationObject),
                clientDataJson: coerceToBase64Url(clientDataJSON)
            }
        };

        let response;
        try {
            let res = await registerCredentialWithServer(data);

            if (res.ok == true) {

                alert("Passkey saved successfully");
            } else {

                alert("Could not create passkey");
            }
        } catch (e) {
            alert("Could not create passkey");
        }



    }

    async function registerCredentialWithServer(formData) {
        let response = await fetch('/Fido2/RegisterResponse', {
            method: 'POST', // or 'PUT'
            body: JSON.stringify(formData), // data can be `string` or {object}!
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        });

        //let data = await response.json();

        return response;
    }

    function coerceToBase64Url(input) {
        // Array or ArrayBuffer to Uint8Array
        if (Array.isArray(input)) {
            input = Uint8Array.from(input);
        }

        if (input instanceof ArrayBuffer) {
            input = new Uint8Array(input);
        }

        // Uint8Array to base64
        if (input instanceof Uint8Array) {
            var str = "";
            var len = input.byteLength;

            for (var i = 0; i < len; i++) {
                str += String.fromCharCode(input[i]);
            }
            input = window.btoa(str);
        }

        if (typeof input !== "string") {
            throw new Error("could not coerce to string");
        }

        // base64 to base64url
        // NOTE: "=" at the end of challenge is optional, strip it off here
        input = input.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

        return input;
    }

    function base64ToArrayBuffer(base64) {
        var binaryString = atob(base64);
        var bytes = new Uint8Array(binaryString.length);
        for (var i = 0; i < binaryString.length; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }
        return bytes.buffer;
    }
</script>

Finally, the signing in part. Similarly, there is one endpoint for the configuration and one for the response from the OS.

#region SIGN IN WITH PASS KEY
[HttpPost]
public async Task<JsonResult> SignInRequest()
{
    var existingCredentials = new List<PublicKeyCredentialDescriptor>();

    var exts = new AuthenticationExtensionsClientInputs() { Extensions = true };
    
    // 3. Create options
    var uv = UserVerificationRequirement.Required;
    var options = _lib.GetAssertionOptions(
        existingCredentials,
        uv,
        exts
    );

    options.RpId = "localhost";

    // 4. Temporarily store options, session/in-memory cache/redis/db
    HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson());

    // 5. Return options to client
    return Json(options);
}

[HttpPost]
public async Task<IActionResult> VerifyWebAuthn([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken)
{

    try
    {
        var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions");
        var options = AssertionOptions.FromJson(jsonOptions);

        // 2. Get registered credential from database
        var creds = await fido2Storage.GetCredentialById(clientResponse.Id);

        if (creds == null)
        {
            TempData["Unsuccessful"] = "Bilinmeyen kimlik bilgileri";
            return Unauthorized();
        }

        // 3. Get credential counter from database
        var storedCounter = creds.SignatureCounter;

        // 4. Create callback to check if userhandle owns the credentialId
        async Task<bool> callback(IsUserHandleOwnerOfCredentialIdParams args, CancellationToken token)
        {
            var storedCreds = await fido2Storage.GetCredentialsByUserHandleAsync(args.UserHandle);
            return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId));
        }

        // 5. Make the assertion
        var res = await _lib.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback);

        var userHandleFromRequest = Convert.ToBase64String(creds.UserHandle);

        var user = _dbContext.Users.Where(s => s.UserHandle == userHandleFromRequest).Select(s => new Users
        {
            Id = s.Id,
            UserHandle = s.UserHandle,
            Username = s.Username
        }).FirstOrDefault();

        if (user == null || string.IsNullOrEmpty(user.UserHandle))
        {
            ViewBag.SigninMessage = "User not found";
            return View("Login");
        }

        // 6. Store the updated counter
        await fido2Storage.UpdateCounter(res.CredentialId, res.Counter);

        HttpContext.Session.SetString("Username", user.Username);
        HttpContext.Session.SetInt32("UserId", user.Id);

        return Redirect("~/Home/Index");
    }
    catch (Exception ex)
    {
        ViewBag.SigninMessage = "Your passkey could not be verified";
        return View("Login");
    }
}
#endregion

And the frontend for this. As the HTML part, you only need to add the autocomplete=”webauthn” to your regular form. This is required for browsers to understand that it supports webauthn sign in.

<form asp-action="LoginUser" asp-controller="Account" method="post">
    <div class="row mb-3">
        <div class="col-md-12">
            <label class="form-label">Username</label>
            <div class="d-flex align-items-center">
                <input name="username"
                       class="form-control flex-grow-1"
                       maxlength="50"
                       autocomplete="webauthn"
                       required />
            </div>
        </div>
        <div class="col-md-12">
            <label class="form-label">Password</label>
            <div class="d-flex align-items-center">
                <input type="password" name="password"
                       class="form-control flex-grow-1"
                       required />
            </div>
        </div>
    </div>

    <div class="row">
        <button type="submit" class="btn btn-success">Login</button>
    </div>
</form>

@*PASSKEYS*@
<script type="module" async>

    export async function _fetch(path, payload = '') {
        const headers = {
            'X-Requested-With': 'XMLHttpRequest',
        };
        if (payload && !(payload instanceof FormData)) {
            headers['Content-Type'] = 'application/json';
            payload = JSON.stringify(payload);
        }
        const res = await fetch(path, {
            method: 'POST',
            credentials: 'same-origin',
            headers: headers,
            body: payload,
        });
        if (res.status === 200) {
            // Server authentication succeeded
            return res.json();
        } else {
            // Server authentication failed
            const result = await res.json();
            throw new Error(result.error);
        }
    };

    export const base64url = {
        encode: function (buffer) {
            const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
            return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
        },
        decode: function (base64url) {
            const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
            const binStr = window.atob(base64);
            const bin = new Uint8Array(binStr.length);
            for (let i = 0; i < binStr.length; i++) {
                bin[i] = binStr.charCodeAt(i);
            }
            return bin.buffer;
        }
    }

    function base64UrlToArrayBuffer(base64url) {
        // Replace '-' with '+' and '_' with '/' to make it compatible with base64 decoding
        const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');

        // Pad the string with '=' if necessary to make its length a multiple of 4
        //const paddedBase64 = base64 + '=='.substring(0, (4 - base64.length % 4) % 4);

        // Decode the base64 string
        const binaryString = atob(base64);

        // Create Uint8Array from the binary string
        const bytes = new Uint8Array(binaryString.length);
        for (let i = 0; i < binaryString.length; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }

        // Return the ArrayBuffer
        return bytes.buffer;
    }

    export async function authenticate() {


        const options = await _fetch('/Fido2/SigninRequest');

        options.challenge = base64UrlToArrayBuffer(options.challenge);
        options.allowCredentials = [];

        let credential;
        try {
            credential = await navigator.credentials.get({
                publicKey: options,

                // Request a conditional UI.
                mediation: 'conditional'
            });
        } catch (err) {
            return;
            alert(err.message ? err.message : err);
        }

        try {
            await verifyAssertionWithServer(credential);
        } catch (e) {
            alert('Could not sign in');
        }
    };

    if (window.PublicKeyCredential &&
        PublicKeyCredential.isConditionalMediationAvailable) {
        try {

            const cma = await PublicKeyCredential.isConditionalMediationAvailable();
            if (cma) {

                // If a conditional UI is available, invoke the authenticate() function.
                const user = await authenticate();
                /*if (user) {

                    // Proceed only when authentication succeeds.
                    $('#username').value = user;
                    location.href = '/homepage';
                } else {
                    throw new Error('User not found.');
                }*/
            }
        } catch (e) {

            // A NotAllowedError indicates that the user canceled the operation.
            if (e.name !== 'NotAllowedError') {

            } else {
                alert('An error occured during passkey process');
            }
        }
    }

    async function verifyAssertionWithServer(assertedCredential) {

        // Move data into Arrays incase it is super long
        let authData = new Uint8Array(assertedCredential.response.authenticatorData);
        let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
        let rawId = new Uint8Array(assertedCredential.rawId);
        let sig = new Uint8Array(assertedCredential.response.signature);
        const data = {
            id: assertedCredential.id,
            rawId: coerceToBase64Url(rawId),
            type: assertedCredential.type,
            extensions: assertedCredential.getClientExtensionResults(),
            response: {
                authenticatorData: coerceToBase64Url(authData),
                clientDataJson: coerceToBase64Url(clientDataJSON),
                signature: coerceToBase64Url(sig)
            }
        };

        let response;
        try {
            let res = await fetch("/Fido2/VerifyWebAuthn", {
                method: 'POST', // or 'PUT'
                body: JSON.stringify(data), // data can be `string` or {object}!
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                }
            });

            if (res.ok) {
                window.location.href = "/Home/Index";
            }
            else {
                alert('An error occured during passkey process');
            }
        } catch (e) {
            alert('An error occured during passkey process');
        }

    }

    function coerceToBase64Url(input) {
        // Array or ArrayBuffer to Uint8Array
        if (Array.isArray(input)) {
            input = Uint8Array.from(input);
        }

        if (input instanceof ArrayBuffer) {
            input = new Uint8Array(input);
        }

        // Uint8Array to base64
        if (input instanceof Uint8Array) {
            var str = "";
            var len = input.byteLength;

            for (var i = 0; i < len; i++) {
                str += String.fromCharCode(input[i]);
            }
            input = window.btoa(str);
        }

        if (typeof input !== "string") {
            throw new Error("could not coerce to string");
        }

        // base64 to base64url
        // NOTE: "=" at the end of challenge is optional, strip it off here
        input = input.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

        return input;
    }
</script>

By ycan

Leave a Reply

Your email address will not be published. Required fields are marked *