Send MFA signin requirement to OpenID Connect server using ASP.NET Core Identity and IdentityServer4

This post adds the custom ASP.NET Core Identity, IdentityServer4 logic to check for the “acr_values” and react if a client application requests MFA for authentication. The “acr_values” parameter is used to pass the mfa value from the client to the server in the authentication request.

Code: https://github.com/damienbod/AspNetCoreHybridFlowWithApi

Blogs in this series

OpenID Connect ASP.NET Core client

The Razor Page ASP.NET Core Open ID Connnect Client application uses the AddOpenIdConnect method to login to the Open ID Connect server. The “acr_values” parameter is set with the “mfa” value and sent with the authentication request. The OpenIdConnectEvents is used to add this.

public void ConfigureServices(IServiceCollection services)
{
	//...
	
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
	.AddCookie()
	.AddOpenIdConnect(options =>
	{
		options.SignInScheme = "Cookies";
		options.Authority = "https://localhost:44352";
		options.RequireHttpsMetadata = true;
		options.ClientId = "AspNetCoreRequireMfaOidc";
		options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
		options.ResponseType = "code id_token";
		options.Scope.Add("profile");
		options.Scope.Add("offline_access");
		options.SaveTokens = true;
		options.Events = new OpenIdConnectEvents
		{
			OnRedirectToIdentityProvider = context =>
			{
				context.ProtocolMessage.SetParameter("acr_values", Amr.Mfa);

				return Task.FromResult(0);
			}
		};
	});

	//...
}

OpenID Connect IdentityServer 4 server with ASP.NET Core Identity

On the OpenID Connect server, which is implemented using ASP.NET Core Identity with MVC views, a new view ErrorEnable2FA.cshtml is created, and added.

This view will be displayed if the Identity comes from an application which requires MFA but the user has not activated this in Identity. The view informs the user, and adds a link to activate this.

@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:


<br />

<a asp-controller="Manage" asp-action="TwoFactorAuthentication">Enable MFA</a>

In the Login method, the IIdentityServerInteractionService interface implementation _interaction is used to access the Open ID Connnect request parameters. The “acr_values” is accessed using the AcrValues. As the client sent this as mfa, this can then be checked.

If MFA is required, and the user in ASP.NET Core Identity has 2FA enabled, then the login continues. If the user has no 2FA enabled, the user is redirected to the custom view ErrorEnable2FA.cshtml. Then ASP.NET Core Identity signs the user in.

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
	var returnUrl = model.ReturnUrl;
	var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(model.Email);
	if(user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToAction(nameof(ErrorEnable2FA));
	}

The ExternalLoginCallback works like the local Identity login. The AcrValues property is checked for the “mfa” value and if it is sent, the 2FA is forced before the login completes, ie redirected to the ErrorEnable2FA view.

//
// GET: /Account/ExternalLoginCallback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
	var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	if (remoteError != null)
	{
		ModelState.AddModelError(string.Empty, _sharedLocalizer["EXTERNAL_PROVIDER_ERROR", remoteError]);
		return View(nameof(Login));
	}
	var info = await _signInManager.GetExternalLoginInfoAsync();

	if (info == null)
	{
		return RedirectToAction(nameof(Login));
	}

	var email = info.Principal.FindFirstValue(ClaimTypes.Email);

	if (!string.IsNullOrEmpty(email))
	{
		var user = await _userManager.FindByNameAsync(email);
		if (user != null && !user.TwoFactorEnabled && requires2Fa)
		{
			return RedirectToAction(nameof(ErrorEnable2FA));
		}
	}

	// Sign in the user with this external login provider if the user already has a login.
	var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
	

If the user is already logged in, the client application still validates the “amr” claim, and can setup the MFA then with a link to the ASP.NET Core Identity view.

Links:

https://tools.ietf.org/html/draft-ietf-oauth-amr-values-04

https://openid.net/specs/openid-connect-core-1_0.html

Forcing reauthentication with Azure AD

https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-mfa-howitworks

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc

3 comments

  1. […] Send MFA signin requirement to OpenID Connect server using ASP.NET Core Identity and IdentityServer4 […]

  2. […] Send MFA signin requirement to OpenID Connect server using ASP.NET Core Identity and IdentityServer4 (Damien Bowden) […]

  3. […] Send MFA signin requirement to OpenID Connect server using ASP.NET Core Identity and IdentityServer4 – Daamien Bowden […]

Leave a Reply to The Morning Brew - Chris Alcock » The Morning Brew #2899 Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: