Overview

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 order to add MFA after a traditional username & password flow.

Implementing MFA with authenticator app using the Authsignal pre-built UI

The solution in this example consists of 2 projects.

Example Repository

Duende IdentityServer + Authsignal example repository.

Configuration

Enabling authenticators

For the purposes of this example, we have enabled authenticator app 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

Authsignal’s 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.

/src/IdentityServer/Pages/Account/Login/Index.cshtml.cs

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,
      Action: "identity-server-login",
      Attributes: new TrackAttributes(
        Username: user.Username,
        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 Authsignal’s 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.

/src/IdentityServer/Pages/Account/Login/Callback.cshtml.cs

public async Task<IActionResult> OnGet(string returnUrl, string token)
{
  var validateChallengeRequest = new ValidateChallengeRequest(Token: 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.

MFA with email

What if you’re using email and password as your primary authentication step and you want to use an email-based Authsignal method (OTP or magic link) as the secondary MFA step? In this scenario you’re already capturing the user’s email in the first step, so you want to avoid prompting the user to input their email again in the second step.

This can be achieved by passing the user’s email with the track request.

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

Alternatively, you can use our Server SDK to programmatically enroll the user with an email-based authenticator.

In either case, you’ll likely also want to disable the “Self-service management” setting for your email authenticator in the Authsignal Portal. This means the user will not be able to input or edit their email in the Authsignal UI themselves.

Next steps