Skip to content
THaack on GitHub Thomas on LinkedIn

Setting Up Azure MFA with NPS for Meraki Client VPN (And Everything That Can Go Wrong)

I spent an entire day getting Azure MFA working with our Meraki Client VPN through NPS. What should've been a two-hour job turned into a full-day debugging session involving certificates, registry annoyingness, service principals, and authentication protocols that don't behave the way you'd expect. I'm a n00b, so I probably ran into some problems others wouldn't and some of the issues I ran into were my fault - but the guidance wasn't clear. If you're doing this yourself, maybe this'll save you some time.

Here's the rough setup process and every problem I hit.

Starting Point

  • Windows Server 2022 with NPS role
  • Hybrid AD syncing to Entra ID via Azure AD Connect
  • Meraki MX handling L2TP/IPsec Client VPN
  • Users already enrolled in MFA for Office 365
  • Internal Microsoft ADCS for certificates

Basic NPS Configuration

Before touching the MFA extension, get basic NPS working first.

Install the NPS Role

Server Manager > Add Roles > Network Policy and Access Services. Standard wizard, nothing special.

Get a Server Certificate

You need a certificate for RADIUS. With ADCS:

  1. Open MMC, add Certificates snap-in (Computer account)
  2. Personal > All Tasks > Request New Certificate
  3. Use Computer or Domain Controller template
  4. Verify Server Authentication is in enhanced key usage
  5. Check that subject alternative name has the server's FQDN

Register NPS in AD

NPS console > right-click NPS (Local) > Register server in Active Directory. This lets NPS read user properties.

Configure the Meraki MX as a RADIUS Client

NPS > RADIUS Clients and Servers > RADIUS Clients > New

  • Friendly name: Meraki MX (or whatever)
  • Address: Internal IP of the Meraki MX (not public IP)
  • Shared secret: Generate something random and strong, save it

Create a Network Policy

NPS > Network Policies > New

  • Name: Meraki Client VPN - MFA
  • Conditions: Add your VPN users security group (Windows Groups)
  • Permission: Grant access
  • Authentication Methods: MS-CHAP v2 only (uncheck everything else)
  • Settings > RADIUS Attributes > Standard:
    • Service-Type = Framed
    • Framed-Protocol = PPP

Don't add NAS Port Type as a condition yet - we want to ensure we can get basic auth working. It can prevent the policy from matching. Add it later.

Create a Connection Request Policy

If you don't have one already:

  • Condition: Client IPv4 Address = your Meraki MX internal IP
  • Settings > Authentication: Authenticate requests on this server

Configure Meraki

Meraki Dashboard > Security & SD-WAN > Client VPN

RADIUS servers:

  • Host: NPS server IP
  • Port: 1812
  • Secret: the shared secret from earlier

Authentication: RADIUS

Test basic authentication at this point. If it doesn't work, fix it before installing the MFA extension. Seriously, don't skip this step. You can't debug NPS and the MFA extension at the same time.

Installing the Azure MFA NPS Extension

Download the extension from Microsoft (aka.ms/npsmfa) and run the installer. During installation it creates a certificate and tries to register it with Azure. Pay attention to any errors during this process.

After installation, there's a PowerShell script at

C:\Program Files\Microsoft\AzureMfa\Config\AzureMfaNpsExtnConfigSetup.ps1

that finishes the configuration. Run it. It'll authenticate to your tenant and set up the registry.

If something goes wrong and you're tempted to uninstall and reinstall - don't. Each installation creates a new certificate but doesn't clean up old certificates from Azure. You'll end up with multiple certificates registered in Azure and none of them matching your current NPS server. If the installation fails, fix the specific problem instead of starting over.

Problem 1: Policy Not Matching After MFA Extension Install

After installing the extension, test connections failed immediately:

Reason Code: 21
Reason: An NPS extension dynamic link library (DLL) that is installed on the NPS server rejected the connection request.

Azure MFA logs showed:

NPS Extension for Azure MFA only performs Secondary Auth for Radius requests in AccessAccept State. 
Request received for User with response state AccessReject, ignoring request.

This means NPS rejected the user before MFA could run. The extension only works on requests that pass primary authentication.

Looking at event logs, "Network Policy Name" was blank. The policy wasn't matching at all. I'd added a NAS Port Type condition (Virtual/VPN) which seemed logical, but Meraki's RADIUS requests don't include it in a way NPS recognizes. Removed that condition, problem fixed.

Problem 2: Dial-In Permission Denial

Users still got rejected with reason code 16 (authentication failed), but passwords were correct.

The issue was in ADUC. Each user has a Dial-in tab with "Network Access Permission" that can be set to Allow, Deny, or Control through NPS Policy. If it's set to Deny, RADIUS auth fails before NPS even checks credentials.

Set it to "Control access through NPS Network Policy" for each VPN user. This is ancient RADIUS stuff that's easy to forget.

Problem 3: HTTP 401 from Azure

With basic auth working (I uninstalled MFA extension to verify), I reinstalled and got:

ErrorCode: HTTPS_COMMUNICATION_ERROR 
Msg: Unexpected response from the Azure AD MFA service. [HttpStatusCode:401]

HTTP 401 means the NPS extension can't authenticate to Azure. The extension uses certificate auth with a service principal called "Azure Multi-Factor Auth Client" (fixed App ID: 981f26a1-7f43-403b-a875-f8b09b8cd720).

First check: are the enterprise apps enabled?

Connect-MsolService
Get-MsolServicePrincipal -AppPrincipalId "981f26a1-7f43-403b-a875-f8b09b8cd720" | Select DisplayName, AccountEnabled

Both the Client and Connector apps were enabled, so that wasn't it.

Problem 4: Registry Missing Certificate Thumbprint

Checked HKLM\SOFTWARE\Microsoft\AzureMfa:

CLIENT_CERT_IDENTIFIER : CN=..., OU=Microsoft NPS Extension
CERTIFICATE_THUMBPRINT : (blank)

CERTIFICATE_THUMBPRINT was missing entirely. The extension needs this to locate the cert. Found the cert in certlm.msc (Personal\Certificates, subject has "Microsoft NPS Extension") and added it manually:

$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*Microsoft NPS Extension*"}
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\AzureMfa" -Name "CERTIFICATE_THUMBPRINT" -Value $cert.Thumbprint -Type String

Still got 401 errors.

Problem 5: Certificate Not Registered in Azure

Used Microsoft Graph to check what certs Azure knew about:

Connect-MgGraph -Scopes 'Application.Read.All'
(Get-MgServicePrincipal -Filter "appid eq '981f26a1-7f43-403b-a875-f8b09b8cd720'" -Property "KeyCredentials").KeyCredentials | Format-List KeyId, DisplayName, StartDateTime, EndDateTime

Azure had two certificates registered. Neither matched my NPS server's cert. One was from a previous failed install attempt.

This is where things got messy. At some point during troubleshooting I'd uninstalled and reinstalled the NPS extension trying to fix other issues. Each install creates a new certificate, but the old certificates don't get cleaned up from Azure automatically. So Azure had certificates from old installs, my NPS server had a certificate from the current install, and they didn't match.

The lesson here: don't uninstall and reinstall the extension unless you're prepared to manually clean up certificate registrations in Azure. If I'd known this upfront, I would've just fixed the registry instead of reinstalling.

You can't fix this through the Azure Portal. The "Azure Multi-Factor Auth Client" is a Microsoft first-party app, so it doesn't have the normal Certificates & secrets interface. You need PowerShell:

# Export local cert
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*Microsoft NPS Extension*"}
$cerPath = "C:\Temp\NPSCert.cer"
Export-Certificate -Cert $cert -FilePath $cerPath -Type CERT

# Read cert bytes
$certBytes = [System.IO.File]::ReadAllBytes($cerPath)

# Connect to Graph
Connect-MgGraph -Scopes 'Application.ReadWrite.All'

# Get service principal
$sp = Get-MgServicePrincipal -Filter "appid eq '981f26a1-7f43-403b-a875-f8b09b8cd720'"

# Create key credential
$keyCredential = @{
    Type = "AsymmetricX509Cert"
    Usage = "Verify"
    Key = $certBytes
}

# Replace all certs with just this one
Update-MgServicePrincipal -ServicePrincipalId $sp.Id -KeyCredentials @($keyCredential)

Verify it worked:

(Get-MgServicePrincipal -Filter "appid eq '981f26a1-7f43-403b-a875-f8b09b8cd720'" -Property "KeyCredentials").KeyCredentials | Format-List KeyId, DisplayName, StartDateTime, EndDateTime, @{Name = "Thumbprint"; Expression = { [Convert]::ToBase64String($_.CustomKeyIdentifier)}}

Thumbprint now matched. Still got 401 errors.

Problem 6: Wrong CLIENT_ID in Registry

Out of desperation I checked the CLIENT_ID in the registry:

$clientId = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\AzureMfa").CLIENT_ID

It was wrong. During troubleshooting I'd attempted to create my own app registration and put that CLIENT_ID in the registry. Complete mistake.

The NPS extension must use the Microsoft first-party app with the fixed App ID of 981f26a1-7f43-403b-a875-f8b09b8cd720. Fixed it:

Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\AzureMfa" -Name "CLIENT_ID" -Value "981f26a1-7f43-403b-a875-f8b09b8cd720" -Type String
Restart-Service IAS

401 errors stopped. MFA challenges were now being sent.

Problem 7: No MFA Prompt Appearing

Azure MFA logs showed:

Challenge requested in Authentication Ext for User with state [guid]

But users never got an MFA prompt. VPN just timed out.

Here's what was happening: our users only had number matching configured in Microsoft Authenticator. That's the newer MFA method where you have to type in a two-digit number shown on the screen. Newer NPS extension versions (1.2.2216.1+) try to use number matching or TOTP by default.

The problem is VPN clients can't display a number for the user to type into their authenticator app. The VPN client just sends credentials and waits for a response. There's no mechanism for interactive challenges beyond the initial username/password.

Force the extension to use old-style push notifications instead:

Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\AzureMfa" -Name "OVERRIDE_NUMBER_MATCHING_WITH_OTP" -Value "FALSE" -Type String
Restart-Service IAS

After this, users got simple Approve/Deny push notifications in Microsoft Authenticator. That works fine with VPN since it doesn't require any interaction beyond tapping Approve on your phone.

If you're planning this deployment, you might want to have users set up the old-style push notification method in addition to number matching, or just accept that VPN will use the simpler approval method. The security difference is minimal for this use case.

Problem 8: Username Format Matters

Authentication behavior changed based on username format when attempting to sign in with the built in VPN client:

NetBIOS format (DOMAIN\username):

  • Network Policy didn't match (blank in logs)
  • Primary auth failed before MFA ran

UPN format (username@domain.com):

  • Network Policy matched correctly
  • MFA challenge sent
  • Connection worked after MFA approval

Use UPN format. The NPS extension and Azure work better with it.

Working Configuration

Final registry at HKLM\SOFTWARE\Microsoft\AzureMfa:

CLIENT_ID: 981f26a1-7f43-403b-a875-f8b09b8cd720
CLIENT_CERT_IDENTIFIER: CN=..., OU=Microsoft NPS Extension
CERTIFICATE_THUMBPRINT: [cert thumbprint]
TENANT_ID: [your tenant ID]
STS_URL: https://login.microsoftonline.com/
REQUIRE_USER_MATCH: 1 (DWORD)
OVERRIDE_NUMBER_MATCHING_WITH_OTP: FALSE (String)

Key points:

  • CLIENT_ID must be the Microsoft first-party app ID
  • Both CLIENT_CERT_IDENTIFIER and CERTIFICATE_THUMBPRINT must exist
  • Certificate on NPS must match what's in Azure
  • OVERRIDE_NUMBER_MATCHING_WITH_OTP should be FALSE for VPN
  • REQUIRE_USER_MATCH should be 1 to enforce MFA

Network Policy Settings

Working NPS Network Policy:

Conditions:

  • Windows Groups: VPN users security group
  • Optionally: NAS IPv4 Address = Meraki MX internal IP

Constraints:

  • Authentication Methods: MS-CHAP v2 only

Settings:

  • Standard RADIUS attributes: Service-Type = Framed, Framed-Protocol = PPP

NAS Port Type conditions broke my auth but YMMV.

Connection Request Policy

Keep it simple:

Conditions:

  • Client IPv4 Address = Meraki MX internal IP

Settings:

  • Authenticate requests on this server

Meraki Configuration

Security & SD-WAN > Client VPN:

RADIUS servers:

  • Host: NPS server internal IP
  • Port: 1812
  • Secret: strong shared secret matching NPS RADIUS client config

Client VPN settings:

  • Authentication: RADIUS

Windows VPN Client Setup

For the Windows built-in VPN client:

  1. VPN type: L2TP/IPsec with pre-shared key
  2. Server address: Meraki MX public IP or hostname
  3. Pre-shared key: from Meraki Client VPN settings
  4. Authentication: MS-CHAP v2
  5. Username format: UPN (user@domain.com)

Registry keys needed on client machines for L2TP behind NAT:

HKLM\SYSTEM\CurrentControlSet\Services\PolicyAgent
  AssumeUDPEncapsulationContextOnSendRule = 2 (DWORD)

What to Check When Things Break

Certificate problems:

  • Verify thumbprint in registry matches cert in local store
  • Check same cert is registered in Azure with the service principal
  • Confirm Network Service has read access to cert's private key

Authentication flow:

  • Extension only runs after primary auth succeeds (AccessAccept state)
  • If you see "AccessReject, ignoring request," the problem is NPS policy or AD authentication, not MFA

User requirements:

  • User exists in on-prem AD
  • User synced to Entra ID
  • User has MFA method registered in Entra ID
  • User is in the Windows Group from the NPS Network Policy

Testing:

  • Uninstall MFA extension temporarily to verify basic NPS auth works
  • Test with one user you know has MFA set up correctly
  • Use UPN format for usernames
  • Watch Event Viewer in real-time: NPS logs under Custom Views > Server Roles > Network Policy and Access Services, Azure MFA logs under Applications and Services Logs > Microsoft > AzureMfa

Useful Commands

Check service principal:

Connect-MsolService
Get-MsolServicePrincipal -AppPrincipalId "981f26a1-7f43-403b-a875-f8b09b8cd720" | Select DisplayName, AccountEnabled

View certs in Azure:

Connect-MgGraph -Scopes 'Application.Read.All'
(Get-MgServicePrincipal -Filter "appid eq '981f26a1-7f43-403b-a875-f8b09b8cd720'" -Property "KeyCredentials").KeyCredentials | Format-List

Check cert on NPS:

Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*Microsoft NPS Extension*"} | Format-List Subject, Thumbprint, NotAfter, HasPrivateKey

Check registry:

Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\AzureMfa" | Format-List

Test Azure connectivity:

Test-NetConnection adnotifications.windowsazure.com -Port 443
Test-NetConnection login.microsoftonline.com -Port 443

Takeaways

The NPS extension works once it's configured correctly, but troubleshooting it sucks. Error messages are misleading (like "extension rejected" when the real problem is policy not matching)

The biggest mistakes I made:

  1. Uninstalling and reinstalling the extension - This creates new certificates each time but doesn't clean up the old ones in Azure. Just fix the registry instead of reinstalling.
  2. Not checking user MFA methods first - Our users only had number matching configured, which doesn't work with VPN. Should've checked this before starting.
  3. Assuming error messages were accurate - "Extension rejected request" usually meant something else was wrong earlier in the authentication chain.

When you get a 401 error, check these in order:

  1. CLIENT_ID in registry is 981f26a1-7f43-403b-a875-f8b09b8cd720
  2. CERTIFICATE_THUMBPRINT is set in registry
  3. Certificate on NPS is valid and has private key
  4. Same certificate is registered in Azure with the service principal
  5. Service principal is enabled

For VPN specifically, remember that OVERRIDE_NUMBER_MATCHING_WITH_OTP needs to be FALSE. VPN clients can't do interactive MFA challenges. The extension will send MFA requests but users won't be able to respond if it's trying to use number matching or TOTP.

Once everything's working, it's pretty solid. Users connect to VPN, enter credentials in UPN format, tap Approve on their phone, and they're in. Getting there just takes more troubleshooting than it should.