Getting Started with the WebAuthn component
Introduction
The WebAuthn component provides a simple way to implement a WebAuthn Relying Party server, enabling passwordless authentication in your web application.
A typical Relying Party consists of two entities: the server-side logic and the front-end script. The WebAuthn component implements the server-side logic. The implementation and communication between these two key entities is up to the application.
In this article, we will provide code snippets of the JavaScript front-end that interacts with the WebAuthn server-side backend (hosted by an ASP .NET Core project). For a more complete example, please refer to the demos included in the products installation directory.
Setup
Credential Storage
Before utilizing the component, the application should implement a credential repository, storage, or database used to store and retrieve user credentials. The exact implementation of this storage is up to the application. For example, implementations could store credentials in a file on disk, a database, etc.
For more information on when to store and retrieve user credentials, and what information is necessary, please refer to the registration and authentication sections below.
Configuring the Relying Party Server
A Relying Party Server has multiple properties that serve as identifiers to WebAuthn clients. These can be configured in the WebAuthn component by using the following properties:
- Origin
- RelyingPartyId
- RelyingPartyName
The Origin is a required property and specifies the full web origin, including the protocol (http or https) and domain, of the Relying Party. For example, this property may be set to https://login.example.com:7112. This property, along with RelyingPartyId, ensures the application's security by restricting requests to valid origins and domains, preventing unauthorized entities from attempting to use credentials.
The RelyingPartyId is a valid domain string used to identify the Relying Party during registration or authentication. By default, this value is empty, and will be calculated by parsing the default effective domain of the specified Origin. Using the previous example, this would mean login.example.com would be used as the RelyingPartyId.
The RelyingPartyId can be manually specified, though it should be ensured that a valid effective domain is defined for the given Origin. Using the previous example, example.com would suffice, however, m.login.example.com would be an invalid identifier.
The RelyingPartyName is a user-friendly identifier for the component, intended only for display. For example, this could be set to a company name, such as ACME Corporation.
Related Origins
If manually specified, the RelyingPartyId must be equal to an effective domain of the Origin. However, singular domains can prove difficult for deployments in larger environments, where multiple country-specific domains are in use.
As such, the Origin property may be used to specify a comma-separated list of possible origins, for example, https://example.com:7112,https://example.co.uk:7112. Implementations can allow clients to create and use a credential across this set of origins.
In this case, implementations must manually specify a RelyingPartyId to use across all operations from related origins. Additionally, a JSON document must be hosted at the webauthn well-known URL for the RelyingPartyId (e.g., hosted at https://RelyingPartyId/.well-known/webauthn) as described . This document should contain all origins specified in Origin.
Please see below for a simple example of configuring the mentioned properties. Note that at the very least, Origin must be set for each step of the registration and authentication processes below.
WebAuthn server = new WebAuthn();
// Automatically set RelyingPartyId to default effective domain
server.Origin = "https://login.example.com:7112";
server.RelyingPartyName = "Example Name";
Console.WriteLine(server.RelyingPartyId); // Prints "login.example.com"
// Manually set RelyingPartyId to a different effective domain
server.Origin = "https://login.example.com:7112";
server.RelyingPartyId = "example.com";
server.RelyingPartyName = "Example Name";
// Specify related origins
server.Origin = "https://login.example.com:7112,https://login.example.co.uk:7112";
server.RelyingPartyId = "example.com";
server.RelyingPArtyName = "Example Name";
Registration
Creating Registration Options
To create a new user credential, a user must first initiate the registration process. Typically, the user interacting with a browser will initiate a request to the front-end script, which should then communicate with the component to start this process. This example will assume a button on a form is pressed, with the following JavaScript code acting as a portion of the implementation of the button's event listener:
let username = event.target.UserName.value;
let displayname = event.target.UserDisplayName.value;
var formData = new FormData();
formData.append('UserName', username);
formData.append('UserDisplayName', displayname);
formData = new URLSearchParams(formData).toString();
let response = await fetch(/account/register?handler=CreateRegistrationRequest
, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
In this example, the endpoint /account/register?handler=CreateRegistrationRequest will pass the options provided in the request (i.e., a user and display name) to the WebAuthn component to start the server-side registration process.
During registration, the component is first required to build options for creating, or registering, a new user credential by calling CreateRegistrationRequest. After receiving a request from a front-end, several relevant properties should be set before building these options.
In addition to setting the Relying Party properties, as mentioned in the previous section, the only other required property to set is the UserName. The UserName property should be set to the user-friendly identifier for the user account attempting registration. Typically, the UserName is provided by the client in the front-end request. The client may also provide the UserDisplayName, specifying a name associated with the user account intended only for display.
The UserId property may be set to some unique identifier for the relevant UserName. By default, the UserId is empty, and will be calculated by the component as the SHA256 hash of the provided UserName. If manually specified, implementations should ensure that this identifier is unique to the user, and available in future operations related to this user.
Lastly, it may be that the specified UserName has existing credentials previously obtained from various authenticators. Before calling CreateRegistrationRequest, implementations should query their existing credential database for credentials associated with the specified UserName. Once identified, the UserCredentials collection should be populated by calling AddUserCredential for each credential. Doing so will ensure that a new credential is not created on an authenticator containing a credential mapped to this UserName. While not explicitly required, this step should be performed.
The following properties may also be set or modified for additional configuration of the produced registration options:
- AttestationType
- AuthenticatorAttachment
- DiscoverableCredentials
- Extensions
- PublicKeyAlgorithms
- Timeout
- UserVerification
Once the component is configured, CreateRegistrationRequest should be called, producing a JSON string of the relevant options that should be returned to the client. Implementations should store the created options somewhere for use during verification (see below). For example, these options could be stored in the HTTP session context, which should persist during registration.
The client should pass these options to the JavaScript function navigator.credentials.create(). After doing so, the client will then interact with the authenticator. For example, the client may enter a PIN or touch a security key. Assuming this is successful, navigator.credentials.create() will return the authenticator response, which should then be returned to the component.
Please see below for an example of configuring the component in this case, and storing the options in the HTTP context:
server.UserName = context.Request.Form["UserName"];
server.UserDisplayName = context.Request.Form["UserDisplayName"];
// Some List of WACredential type, search by UserName
List
for (int i = 0; i < existingCredentials.Count; i++) {
server.AddUserCredential(existingCredentials[i].IdB, existingCredentials[i].PublicKey, existingCredentials[i].SignCount, existingCredentials[i].SignAlgorithm)
}
// JSON options string that should be returned to the client and passed to navigator.credentials.create()
string ret = server.CreateRegistrationRequest();
// Store the options in the same context for later use during registration.
context.Session.SetString("registrationOptions", ret);
In the above example, the options ret will be returned to the front-end script. Once the options are returned to the code below, the script will convert the received options to the PublicKeyCredential type, and pass the credential to navigator.credentials.create().
let CreateRegistrationRequestJSON = await response.json(); // Options returned from the WebAuthn component
let CreateRegistrationRequest = PublicKeyCredential.parseCreationOptionsFromJSON(CreateRegistrationRequestJSON);
// Create the new credential below
let newCredential;
try {
newCredential = await navigator.credentials.create({
publicKey: CreateRegistrationRequest
});
} catch (e) {
reportError(e.message);
return;
}
Once the newCredential value is successfully created by the above code, the credential (in JSON format) will be passed back to the WebAuthn component to verify the authenticator response. In the below code, we pass the new credential to the /account/register?handler=VerifyRegistrationResponse endpoint.
const credentialJSON = newCredential.toJSON();
try {
let response = await fetch(/account/register?handler=VerifyRegistrationResponse
, {
method: 'POST', // or 'PUT'
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(credentialJSON) // data can be string
or {object}!
});
} catch (err) {
reportError(err.message);
}
The next section below assumes this step has been performed, and the mentioned endpoint has received the authenticator response.
Verifying the Registration Response
To verify the authenticator response, VerifyRegistrationResponse should be called, taking the options previously generated with CreateRegistrationRequest and the recently received response as parameters.
After calling VerifyRegistrationResponse, the RegistrationInfo event will fire, providing the Id of the recently created credential. During this event, implementations should search for the provided credential Id in their database. Credential Ids must be unique, i.e., no existing credential in the database may have the provided credential Id. If the provided credential Id exists for any user, verification should fail, and the Cancel parameter of RegistrationInfo should be set to true to do so.
Assuming the credential Id does not exist for any other user and the additional verification performed by the component succeeds, RegistrationComplete will fire. This event will provide various information about the newly created credential record. During this event, implementations should save this information to their existing credential database for use during future registration or authentication ceremonies. Specifically, implementations should save the following values associated with the credential record.
- The current UserName associated with the credential.
- If manually specified, the associated UserId.
- The CredentialId parameter of RegistrationComplete.
- The PublicKey parameter of RegistrationComplete.
- The SignCount parameter of RegistrationComplete.
- The Algorithm parameter of RegistrationComplete.
Additionally, implementations may wish to store the BackupEligible, BackupState, and UvInitialized configs as well.
Once the relevant credential information is saved, registration is officially complete, and the registered credential may be used in future authentication ceremonies.
Please see below for an example of the verification process:
server.OnRegistrationInfo += (o, e) => {
// Some List of WACredential type, search by Credential Id
existingCredentials = QueryCredentialsById(e.CredentialId);
if (existingCredentials.Count != 0) {
// Registration should fail since CredentialId exists
e.Cancel = true;
}
};
server.OnRegistrationComplete += (o, e) => {
// Save credential info for authentication
SaveCredential(server.UserName, e.CredentialIdB, e.PublicKey, e.SignCount, e.Algorithm);
};
string response = StreamReader(context.Request.Body).ReadToEnd();
string cachedOptions = context.Session.GetString("registrationOptions") ?? String.Empty;
server.VerifyRegistrationResponse(response, options);
Console.WriteLine("Registration Successful.");
A response should be returned to the front-end, which can typically let the user know that the registration was successful (200 OK), or otherwise, return an error as thrown by VerifyRegistrationResponse. As a simple example of handling an error code and displaying the error to the user on the front-end:
if (response.status != 200) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
// Now, the user can login
document.location.href = /account/login?message=registration-successful
;
Note that all JS code provided in this section was contained within the single event listener for the button pressed. Please see the code examples section for further reference.
Authentication
Creating Authentication Options
To log in using an existing credential, a user must first initiate the authentication process. Typically, the user interacting with a browser will initiate a request to the front-end script, which should communicate directly with the component to start this process. This example will assume a button on a form is pressed, with the following JavaScript code acting as a portion of the implementation of the button's event listener:
let username = event.target.UserName.value;
var formData = new FormData();
// Username is not required, implies use of client-side discoverable credentials.
if (username !== "") {
formData.append('UserName', username);
formData = new URLSearchParams(formData).toString();
}
let response = await fetch(/account/login?handler=CreateAuthenticationRequest
, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
In this example, the endpoint /account/login?handler=CreateAuthenticationRequest will pass the options provided in the request (i.e., a user and display name) to the WebAuthn component to start the server-side authentication process.
During authentication, the component is first required to build options for logging in using an existing user credential by calling CreateAuthenticationRequest. After receiving a request from a front-end, several relevant properties should be set before building these options.
In addition to setting the Relying Party properties, as mentioned in the first section, there are no required properties that must be set before calling CreateAuthenticationRequest.
Typically, the user attempting to log in will provide the username associated with their account, however, this is not a requirement. If the username is provided, implementations should query their existing credential database for any credentials associated with this username. Once identified, the UserCredentials collection should be populated by calling AddUserCredential for each user credential. When sending the options to the client in a later step, the authenticator will then allow the client to select from these credentials during authentication. Note that the UserName property should not be specified in this case, as it is not included in the options.
If the client does not specify a username, implementations should leave the UserCredentials collection empty. When sending the options to the client in a later step, the authenticator will then allow the client to utilize any discoverable credentials that were previously created. Discoverable credentials are made available to the client in this specific case, and the client may choose any of the existing discoverable credentials as presented by the authenticator. A discoverable credential can be created during registration. The component can indicate its preference regarding whether a discoverable credential is created using the DiscoverableCredentials property during this step. If no discoverable credentials exist, this will result in an error.
The following properties may also be set or modified for additional configuration of the produced login options:
- Extensions
- Timeout
Once the component is configured, CreateAuthenticationRequest should be called, producing a JSON string of the relevant options that should be returned to the client. Implementations should store the created options somewhere for use during verification (see below). For example, these options could be stored in the HTTP session context, which should persist during authentication.
The client should pass these options to the JavaScript function navigator.credentials.get(). After doing so, the client will then interact with the authenticator. For example, the client may enter a PIN or touch a security key. Assuming this is successful, navigator.credentials.get() will return the authenticator response, which should then be returned to the component.
Please see below for an example of configuring the component in this case, and storing the options in the HTTP context:
string userName = context.Request.Form["UserName"]; // If provided, optional
List
for (int i = 0; i < existingCredentials.Count; i++) {
server.AddUserCredential(existingCredentials[i].IdB, existingCredentials[i].PublicKey, existingCredentials[i].SignCount, existingCredentials[i].SignAlgorithm)
}
// JSON options string that should be returned to the client and passed to navigator.credentials.create()
string ret = server.CreateAuthenticationRequest();
// Store the options in the same context for later use during login.
context.Session.SetString("loginOptions", ret);
In the above example, the options ret will be returned to the front-end script. Once the options are returned to the code below, the script will convert the received options to the PublicKeyCredential type, and pass the credential to navigator.credentials.get().
let CreateAuthenticationRequestJSON = await response.json();
let CreateAuthenticationRequest = PublicKeyCredential.parseRequestOptionsFromJSON(CreateAuthenticationRequestJSON);
// ask browser for credentials (browser will ask connected authenticators)
let credential;
try {
credential = await navigator.credentials.get({ publicKey: CreateAuthenticationRequest })
} catch (err) {
reportError(err.message);
return;
}
Once the credential value is successfully retrieved by the above code, the credential (in JSON format) will be passed back to the WebAuthn component to verify the authenticator response. In the below code, we pass the credential to the /account/login?handler=VerifyAuthenticationResponse endpoint.
const credentialJSON = credential.toJSON();
try {
let response = await fetch(/account/login?handler=VerifyAuthenticationResponse
, {
method: 'POST', // or 'PUT'
body: JSON.stringify(credentialJSON),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
} catch (err) {
reportError(err.message);
}
The next section below assumes this step has been performed, and the mentioned endpoint has received the authenticator response.
Verifying the Authentication Response
To verify the authenticator response, VerifyAuthenticationResponse should be called, taking the recently received response, and the options previously generated with CreateAuthenticationRequest (stored in the HTTP context).
After calling VerifyAuthenticationResponse, the AuthenticationInfo event will fire, providing the Id of the recently created credential. During this event, implementations should search for the provided credential Id in their database. Since a user is attempting to use an existing credential, it is assumed this credential exists in the database. If the provided credential Id does not exist, the Cancel parameter of AuthenticationInfo should be set to true, and verification should fail.
Assuming the credential exists in the database, the following properties and event parameters should be set during AuthenticationInfo:
- The current UserName associated with the credential.
- If manually specified, the associated UserId.
- The PublicKey parameter of AuthenticationInfo.
- The SignCount parameter of AuthenticationInfo.
- The Algorithm parameter of AuthenticationInfo.
Assuming this information is correct, the component will continue the verification process accordingly, and AuthenticationComplete will fire. This event will provide necessary information regarding any updates to the existing credential. During this event, implementations should update the credential in their database for future use. Specifically, implementations should update the signature counter associated with the credential record by utilizing the SignCount parameter of AuthenticationComplete.
Implementations may wish to query the values of the BackupState and UvInitialized configs to update the stored credential record accordingly.
Once the relevant credential information is updated, authentication is complete.
Please see below for an example of the verification process:
server.OnAuthenticationInfo += (o, e) => {
// Search for single Credential Id
existingCredential = QueryCredentialById(e.CredentialId);
string user = QueryUserById(e.CredentialId);
if (existingCredential == null) {
// Authentication should fail since CredentialId does not exist
e.Cancel = true;
}
server.UserName = user;
e.PublicKey = existingCredential.PublicKey;
e.SignCount = existingCredential.SignCount;
e.Algorithm = existingCredential.SignAlgorithm;
};
server.OnAuthenticationComplete += (o, e) => {
// Update credential info
SaveCredential(e.CredentialIdB, e.SignCount);
};
string response = new StreamReader(context.Request.Body).ReadToEnd();
string cachedOptions = context.Session.GetString("loginOptions") ?? String.Empty;
server.VerifyAuthenticationResponse(response, options);
Console.WriteLine("Login Successful.");
A response should be returned to the front-end, which can typically let the user know that the authentication was successful (200 OK), or otherwise, return an error as thrown by VerifyAuthenticationResponse. As a simple example of handling an error code and displaying the error to the user on the front-end:
if (response.status !== 200) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
document.location.href = "/account/logout?message=login-successful";
Note that all JS code provided in this section was contained within a single event listener for the button pressed. Please see the code examples section for further reference.
Code Examples
Credential Storage
This section provides the relevant code utilized in the backend for storing credentials in a text file on disk. This implementation is only an example.
private static List
private static List
private static void SaveCredential(string userName, string credentialId, string publicKey, int signCount, string alg) {
string tmp = publicKey.Replace("\r\n", "*");
if (File.Exists(DB_FILE)) {
File.AppendAllText(DB_FILE, "\n");
}
File.AppendAllText(DB_FILE, userName + "|" + credentialId + "|" + tmp + "|" + signCount + "|" + alg);
}
private static void UpdateCredential(string credentialId, int signCount) {
List
private static string Base64UrlEncode(byte[] data) {
string base64 = Convert.ToBase64String(data);
return base64.Replace("+", "-").Replace("/", "_").Replace("=", "");
}
private static byte[] Base64UrlDecode(String data) {
switch (data.Length % 4) {
case 2: data += "=="; break;
case 3: data += "="; break;
}
data = data.Replace('-', '+').Replace('_', '/');
return Convert.FromBase64String(data);
}
Configuring the Relying Party Server
This section provides the implementation of the Init method used in the below .NET samples. This method is called in each operation, where a new instance of the WebAuthn component is configured with a persistent value for the Origin used in both registration and authentication.
public static void Init(nsoftware.IPWorksWebAuthn.WebAuthn server) {
server.Origin = "https://localhost";
server.RelyingPartyName = "WebAuthn";
//server.Config("LogLevel=3");
server.OnLog += (o, e) => {
if (File.Exists(LOG_FILE)) {
File.AppendAllText(LOG_FILE, "\n");
}
File.AppendAllText(LOG_FILE, DateTime.Now.ToString() + " " + e.Message);
};
}
Registration
This section provides relevant code samples referenced in the registration section.
JavaScript
This code represents the handler for the 'submit' button clicked on some browser form when a user initiates registration, containing fields for a username and display name.
document.getElementById('registerForm').addEventListener('submit', async (event) => {
event.preventDefault();
let username = event.target.UserName.value;
let displayname = event.target.UserDisplayName.value;
var formData = new FormData();
formData.append('UserName', username);
formData.append('UserDisplayName', displayname);
formData = new URLSearchParams(formData).toString();
// This request will be handled by the below .NET code, in WebAuthnServer.CreateRegistrationRequest
let response = await fetch(/account/register?handler=CreateRegistrationRequest
, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
let CreateRegistrationRequestJSON = await response.json();
let CreateRegistrationRequest = PublicKeyCredential.parseCreationOptionsFromJSON(CreateRegistrationRequestJSON);
let newCredential;
try {
newCredential = await navigator.credentials.create({
publicKey: CreateRegistrationRequest
});
} catch (e) {
reportError(e.message);
return;
}
const credentialJSON = newCredential.toJSON();
try {
// This request will be handled by the below .NET code, in WebAuthnServer.VerifyRegistrationResponse
let response = await fetch(/account/register?handler=VerifyRegistrationResponse
, {
method: 'POST', // or 'PUT'
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(credentialJSON) // data can be string
or {object}!
});
if (response.status != 200) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
document.location.href = /account/login?message=registration-successful
;
} catch (err) {
reportError(err.message);
}
});
async function reportError(errorMessage) {
document.location.href = /account/register?message=registration-failed&error=${encodeURIComponent(errorMessage)}
;
}
.NET
This code represents the handler for the endpoints /account/register?handler=CreateRegistrationRequest and /account/register?handler=VerifyRegistrationResponse. To note, this code is contained within a cshtml.cs file in the ASP .NET Core project. This code interacts with and provides the response to the relevant JavaScript code above.
public IActionResult OnPostCreateRegistrationRequest() {
ModelState.Clear();
if (TryValidateModel(form)) {
try {
string ret = WebAuthnServer.CreateRegistrationRequest(HttpContext);
return Content(ret, "application/json");
} catch (Exception ex) {
return BadRequest(ex.Message);
}
}
return Page();
}
public IActionResult OnPostVerifyRegistrationResponse() {
ModelState.Clear();
if (TryValidateModel(form)) {
try {
WebAuthnServer.VerifyRegistrationResponse(HttpContext);
} catch (Exception ex) {
return BadRequest(ex.Message);
}
}
return Page();
}
The implementations for the custom WebAuthnServer methods utilized above are shown below.
public static string CreateRegistrationRequest(HttpContext context) {
nsoftware.IPWorksWebAuthn.WebAuthn server = new nsoftware.IPWorksWebAuthn.WebAuthn();
Init(server);
server.UserName = context.Request.Form["UserName"];
server.UserDisplayName = context.Request.Form["UserDisplayName"];
List
string ret = server.CreateRegistrationRequest();
// Store the options in same context for later use during registration.
context.Session.SetString("registrationOptions", ret);
server.Dispose();
return ret;
}
public static void VerifyRegistrationResponse(HttpContext context) {
nsoftware.IPWorksWebAuthn.WebAuthn server = new nsoftware.IPWorksWebAuthn.WebAuthn();
Init(server);
string publicKey = "";
byte[] credentialId = null;
int signCount = 0;
string algorithm = "";
using (StreamReader reader = new StreamReader(context.Request.Body)) {
var task = reader.ReadToEndAsync();
task.Wait();
string data = task.Result;
// Check if credential with this Id already exists in database.
// If so, set Cancel to true and verification will fail
server.OnRegistrationInfo += (o, e) => {
List
// Save necessary credential information to database.
server.OnRegistrationComplete += (o, e) => {
publicKey = e.PublicKey;
credentialId = e.CredentialIdB;
signCount = e.SignCount;
algorithm = e.Algorithm;
SaveCredential(server.UserName, Base64UrlEncode(credentialId), publicKey, signCount, algorithm);
};
string cachedOptions = context.Session.GetString("registrationOptions") ?? String.Empty;
server.VerifyRegistrationResponse(data, cachedOptions);
}
server.Dispose();
}
Authentication
This section provides relevant code samples referenced in the authentication section.
JavaScript
This code represents the handler for the 'submit' button clicked on some browser form when a user initiates authentication, containing an optional field for a username.
document.getElementById('loginForm').addEventListener('submit', async (event) => {
event.preventDefault();
let username = event.target.UserName.value;
var formData = new FormData();
// Username is not required, implies use of client-side discoverable credentials.
if (username !== "") {
formData.append('UserName', username);
formData = new URLSearchParams(formData).toString();
}
// This request will be handled by the below .NET code, in WebAuthnServer.CreateAuthenticationRequest
let response = await fetch(/account/login?handler=CreateAuthenticationRequest
, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
let CreateAuthenticationRequestJSON = await response.json();
let CreateAuthenticationRequest = PublicKeyCredential.parseRequestOptionsFromJSON(CreateAuthenticationRequestJSON);
// ask browser for credentials (browser will ask connected authenticators)
let credential;
try {
credential = await navigator.credentials.get({ publicKey: CreateAuthenticationRequest })
} catch (err) {
reportError(err.message);
return;
}
const credentialJSON = credential.toJSON();
try {
// This request will be handled by the below .NET code, in WebAuthnServer.VerifyAuthenticationResponse
let response = await fetch(/account/login?handler=VerifyAuthenticationResponse
, {
method: 'POST', // or 'PUT'
body: JSON.stringify(credentialJSON),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
// show error
if (response.status !== 200) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
document.location.href = "/account/logout?message=login-successful";
} catch (err) {
reportError(err.message);
}
});
async function reportError(errorMessage) {
document.location.href = /account/login?message=login-failed&error=${encodeURIComponent(errorMessage)}
;
}
.NET
This code represents the handler for the endpoints /account/login?handler=CreateAuthenticationRequest and /account/login?handler=VerifyAuthenticationResponse. To note, this code is contained within a cshtml.cs file in the ASP .NET Core project. This code interacts with and provides the response to the relevant JavaScript code above.
public IActionResult OnPostCreateAuthenticationRequest() {
ModelState.Clear();
if (TryValidateModel(form)) {
try {
string ret = WebAuthnServer.CreateAuthenticationRequest(HttpContext);
return Content(ret, "application/json");
} catch (Exception ex) {
return BadRequest(ex.Message);
}
}
return Page();
}
public IActionResult OnPostVerifyAuthenticationResponse() {
ModelState.Clear();
if (TryValidateModel(form)) {
try {
WebAuthnServer.VerifyAuthenticationResponse(HttpContext);
} catch (Exception ex) {
return BadRequest(ex.Message);
}
}
return Page();
}
The implementations for the custom WebAuthnServer methods utilized above are shown below.
public static string CreateAuthenticationRequest(HttpContext context) {
nsoftware.IPWorksWebAuthn.WebAuthn server = new nsoftware.IPWorksWebAuthn.WebAuthn();
Init(server);
string username = context.Request.Form["UserName"];
List
string ret = server.CreateAuthenticationRequest();
server.Dispose();
// Store the options in same context for later use during login.
context.Session.SetString("loginOptions", ret);
return ret;
}
public static void VerifyAuthenticationResponse(HttpContext context) {
nsoftware.IPWorksWebAuthn.WebAuthn server = new nsoftware.IPWorksWebAuthn.WebAuthn();
Init(server);
using (StreamReader reader = new StreamReader(context.Request.Body)) {
var task = reader.ReadToEndAsync();
task.Wait();
string data = task.Result;
server.OnAuthenticationInfo += (o, e) => {
// Ensure credential with CredentialId exists in the database.
// If not, verification should fail
List
// Set relevant credential info for verification
server.UserName = existingCredential[0];
e.PublicKey = existingCredential[2];
e.SignCount = int.Parse(existingCredential[3]);
e.Algorithm = existingCredential[4];
};
server.OnAuthenticationComplete += (o, e) =>
{
UpdateCredential(Base64UrlEncode(e.CredentialIdB), e.SignCount);
};
string cachedOptions = context.Session.GetString("loginOptions") ?? string.Empty;
server.VerifyAuthenticationResponse(data, cachedOptions);
}
server.Dispose();
}
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.