Duende IdentityServer is an ASP.NET Core framework for building your own login server in compliance with OpenID Connect and OAuth 2.0 standards.

This guide shows how to integrate IdentityServer with Authsignal in 2 steps:

  1. Adding MFA to a traditional username & password login flow
  2. Allowing passkey login as an secure and user-friendly alternative to username & password

Authsignal can be used to facilitate both MFA on login (step 1) and passwordless login (step 2). This guide demonstrates how to progressively transition users from the former to the latter.

1. MFA with authenticator app

2. Login with passkey

Code example

You can find the full code example referenced in this guide on Github here.

The solution in this example consists of 2 projects.


Enabling authenticators

For the purposes of this example, we have enabled Authenticator App (for step 1) and Passkey (for step 2) on our tenant in the Authsignal Portal.

Configuring authenticators

API keys & region URL

We also need to get the API keys and region URL for our tenant.

Retrieving API keys

The secret and the region URL should be set in appsettings.json and the tenant ID and url are needed in the client-side JS snippet used for passkeys.

Adding MFA on login

The quickest way to add MFA to IdentityServer is to use Authsignal’s pre-built UI. We will redirect the user here after validating their username and password.

MFA challenge via Authsignal pre-built UI

The Authsignal pre-built UI can be highly customized to align with your login server’s existing branding.

Initiating the MFA challenge

To initiate an MFA challenge using the pre-built UI, we can track an action and use the URL that is returned.


public async Task<IActionResult> OnPost()
  if (_users.ValidateCredentials(Input.Username, Input.Password))
    var user = _users.FindByUsername(Input.Username);

    var trackRequest = new TrackRequest(
      UserId: user.SubjectId,
      Username: user.Username,
      Action: "identity-server-login",
      RedirectUrl: "https://localhost:5001/Account/Login/Callback?returnUrl=" + returnUrl

    var trackResponse = await _authsignal.Track(trackRequest);

    if (!trackResponse.IsEnrolled || trackResponse.State == UserActionState.CHALLENGE_REQUIRED)
      return Redirect(trackResponse.Url);

For convenience we are prompting the user to enroll for MFA on login if they are not yet enrolled - but you can enroll users at a different point in your user journey as required.

The RedirectUrl we pass to the track request here will be a callback endpoint that we will add to IdentityServer to validate the result of the MFA challenge.

Validating the MFA challenge

Once the user has been redirected back to IdentityServer, we need to validate the result. We do this by implementing a callback page which uses the token that the Authsignal pre-built UI appends as a URL query param when redirecting the user back to IdentityServer. This token is used to lookup the result of the challenge server-side.


public async Task<IActionResult> OnGet(string returnUrl, string token)
  var validateChallengeRequest = new ValidateChallengeRequest(token);

  var validateChallengeResponse = await _authsignal.ValidateChallenge(validateChallengeRequest);

  var userId = validateChallengeResponse.UserId;

  var user = _users.FindBySubjectId(userId);

  if (validateChallengeResponse.State != UserActionState.CHALLENGE_SUCCEEDED)
    // The user did not complete the MFA challenge successfully
    // Redirect them back to the login page
    return Redirect("https://localhost:5001/Account/Login?ReturnUrl=" + returnUrl);

  // Proceed with authentication and issue session cookie

Restricting MFA to authenticator apps

By default all authenticators which have been configured will be available to use on the pre-built UI, including passkeys. We only want to allow users to use authenticator apps for MFA. To achieve this we can go to the Settings tab of our identity-server-login action and update the permitted authenticators.

Implementing passkey login

Configuring passkeys

In the Authsignal Portal we have configured localhost as the Relying Party for the purposes of local development. We also whitelist both the login server and the application server as expected origins.

Configuring the relying party ID and expected origins

This means the user can enroll a passkey directly from our web app running on https://localhost:5002, then reauthenticate with the same passkey when logging in to IdentityServer running on https://localhost:5001.

Enrolling a passkey

Once the user has logged in with MFA, we can use the Authsignal Web SDK to allow the user to add a passkey.

This can be done by adding a button to the application server (i.e. the WebClient).

Adding a passkey

To implement this we add a new page /src/WebClient/Pages/Passkeys.cshtml.

@model PasskeysModel

@Html.HiddenFor(m => m.enrollmentToken)

<button id="add-passkey" onClick="addPasskey()">Add a passkey</button>

<script src="https://unpkg.com/@@authsignal/browser@@0.3.0/dist/index.min.js"></script>
<script src="~/js/passkeys.js"></script>

The enrollmentToken here is fetched server-side in /src/WebClient/Pages/Passkeys.cshtml.cs when the page loads.

public async Task<IActionResult> OnGet()
  var userId = User.Claims.First(x => x.Type == "sub").Value!;
  var action = "add-passkey";
  var scope = "add:authenticators";

  var trackRequest = new TrackRequest(UserId: userId, Action: action, Scope: scope);

  var trackResponse = await _authsignal.Track(trackRequest);

  this.enrollmentToken = trackResponse.Token;

  return Page();

Finally, we need to add the client-side JS implementation for the addPasskey function which uses the Authsignal Web SDK.

function addPasskey() {
  var client = new window.authsignal.Authsignal({
    tenantId: "YOUR_TENANT_ID",
    baseUrl: "https://api.authsignal.com/v1", // Update for your region

  var token = document.getElementById("enrollmentToken").value;

  client.passkey.signUp({ token }).then((resultToken) => {
    if (resultToken) {
      alert("Passkey added");

Logging in with a passkey

Now that the user has enrolled a passkey, the next time they login via IdentityServer we would like to give them the option to use their passkey instead of their username & password.

This can be achieved simply by adding a small client-side JavaScript snippet which initializes the existing username field to allow passkeys.

First we need to ensure that the input field has the value username webauthn in the autocomplete attribute.

  autocomplete="username webauthn"

Then we add some JavaScript to run when the page loads.

function initPasskeyAutofill() {
  var client = new window.authsignal.Authsignal({
    tenantId: "YOUR_TENANT_ID",
    baseUrl: "https://api.authsignal.com/v1", // Update for your region

  client.passkey.signIn({ autofill: true }).then((token) => {
    if (token) {
      var returnUrl = document.getElementById("Input_ReturnUrl").value;

      window.location = `https://localhost:5001/Account/Login/Callback?returnUrl=${returnUrl}&token=${token}`;

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", initPasskeyAutofill);
} else {

This will initialize the input field for passkey autofill, which means if a user has enrolled a passkey then they should be able to select it when focusing the text field.

Logging in with a passkey

The validation process here is the same as for logging in with MFA in step 1 - the only difference here is that we now receive a token directly from the Authsignal Web SDK. In both cases, we pass this token to our callback page, which uses the token to validate the result of the challenge server-side and log the user in.


That’s it! You’ve successfully added both MFA and passkey login by integrating IdentityServer with Authsignal.

For more information, check out or passkeys documentation or to test out passkey compatibility on your browser you can play with our demo site.