Skip to content

damienbod/MfaServer

Repository files navigation

FIDO2/passkeys MFA Server

Microsoft Entra ID: external authentication methods

Implement a Microsoft Entra ID external authentication method using ASP.NET Core and OpenIddict

.NET Deploy app to Entra ID Web App

The FIDO2/passkeys MFA Server is implemented as an ASP.NET Core Web application. Within this application, ASP.NET Core Identity is utilized to store and manage user data in an Azure SQL database. For FIDO2 authentication, the MFA server leverages the passwordless-lib fido2-net-lib. Additionally, OpenIddict is employed to implement the OpenID Connect flow.

flow

Setup Microsoft Entra ID

See the Microsoft Entra ID: external authentication methods documentation.

Microsoft Graph

Microsoft Graph is used to create the Microsoft Entra ID: external authentication method.

Requires the delegated Policy.ReadWrite.AuthenticationMethod permission

POST

https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations

{
    "@odata.type": "#microsoft.graph.externalAuthenticationMethodConfiguration",
    "displayName": "--name-of-provider--", // Displayed in login
    "state": "enabled",
    "appId": "--app-registration-clientId--", // external authentication app registration, see docs
    "openIdConnectSetting": {
        "clientId": "--your-client_id-from-external-provider--",
        "discoveryUrl": "--your-external-provider-url--/.well-known/openid-configuration"
    },
    "includeTarget": { // switch this if only specific users are required
        "targetType": "group",
        "id": "all_users"
    }
}

The https://developer.microsoft.com/en-us/graph/graph-explorer can be used to run this.

Setup FIDO2/passkeys MFA Server

(OpenIddict, fido2-net-lib, ASP.NET Core Identity)

Core Setup OpenIddict

The Microsoft Entra ID external authentication provider relies on an OpenID Connect server implementation for interaction. In our implementation, we utilize OpenIddict to achieve this functionality. It’s worth noting that any OpenID Connect implementation can be used, provided that you have the ability to customize the claims returned in the id_token. Additionally, we persist user data to the database using ASP.NET Core Identity.

See OpenIddict

OpenID Connect implicit flow is used and in OpenIddict, this can be configured in the Worker class. An id_token_hint is used to send the user data from Microsoft Entra ID.

Here is an example of a possible OpenIddict client setup:

await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
    ClientId = "oidc-implicit-mfa",
    ConsentType = ConsentTypes.Implicit,
    DisplayName = "OIDC Implicit Flow for MFA",
    DisplayNames =
    {
        [CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente MVC"
    },
    RedirectUris =
    {
        new Uri("https://login.microsoftonline.com/common/federation/externalauthprovider")
    },
    Permissions =
    {
        Permissions.Endpoints.Authorization,
        Permissions.Endpoints.Revocation,
        Permissions.GrantTypes.Implicit,
        Permissions.ResponseTypes.IdToken,
        Permissions.Scopes.Email,
        Permissions.Scopes.Profile,
        Permissions.Scopes.Roles
    }
});

Microsoft Entra ID uses the redirect_uri:

https://login.microsoftonline.com/common/federation/externalauthprovider

Setup Fido2/passkeys

The FIDO2/passkeys authentication was implement using the fido2-net-lib nuget package.

This was implemented using the AspNetCoreIdentityFido2Passwordless implementation. You need to replace all the ASP.NET Core Identity Razor Pages and add the WebAuthn js scripts from the wwwroot.

The Fido2 appsettings configuration must be changed to match the server deployment.

"Fido2": {
    // This must match the deployment domain
    "ServerDomain": "fidomfaserver.azurewebsites.net",
    "ServerName": "FidoMfaServer",
    "Origins": [ "https://fidomfaserver.azurewebsites.net" ],
    "TimestampDriftTolerance": 300000,
    "MDSAccessKey": null
},

Setup OpenIddict Implicit flow for ME-ID external authn

The default Implicit flow client handling needs to be adapted for the Microsoft Entra ID external authentication flow. This is implemented in the AuthorizationController. This server is only used for this purpose, if implementing this on an existing OpenID Connect server, you would need to leave the default for the other flows.

The code implements the validation like in the Microsoft Entra ID documentation. It is important to validate the id_token_hint (including the signature) and to create the returned id_token with the extra claims and changed claims required by Microsoft Entra ID external authentication methods.

case ConsentTypes.Implicit:
case ConsentTypes.External when authorizations.Any():
case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
    var principal = await _signInManager.CreateUserPrincipalAsync(user);

    // Note: in this sample, the granted scopes match the requested scope
    // but you may want to allow the user to uncheck specific scopes.
    // For that, simply restrict the list of scopes before calling SetScopes.
    principal.SetScopes(request.GetScopes());
    principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());

    // Automatically create a permanent authorization to avoid requiring explicit consent
    // for future authorization or token requests containing the same scopes.
    var authorization = authorizations.LastOrDefault();
    if (authorization == null)
    {
        authorization = await _authorizationManager.CreateAsync(
            principal: principal,
            subject: await _userManager.GetUserIdAsync(user),
            client: await _applicationManager.GetIdAsync(application),
            type: AuthorizationTypes.Permanent,
            scopes: principal.GetScopes());
    }

    principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));

    //get well known endpoints and validate access token sent in the assertion
    var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
        _idTokenHintValidationConfiguration.MetadataAddress,
        new OpenIdConnectConfigurationRetriever());

    var wellKnownEndpoints = await configurationManager.GetConfigurationAsync();

    var idTokenHintValidationResult = ValidateIdTokenHintRequestPayload.ValidateTokenAndSignature(
        request.IdTokenHint,
        _idTokenHintValidationConfiguration,
        wellKnownEndpoints.SigningKeys,
        _testingMode);

    if (!idTokenHintValidationResult.Valid)
    {
        return UnauthorizedValidationParametersFailed(idTokenHintValidationResult.Reason,
            "id_token_hint validation failed");
    }

    var requestedClaims = System.Text.Json.JsonSerializer.Deserialize<claims>(request.Claims);

    principal.AddClaim("acr", "possessionorinherence");

    var sub = idTokenHintValidationResult.ClaimsPrincipal
        .Claims.First(d => d.Type == "sub");

    principal.RemoveClaims("sub");
    principal.AddClaim(sub.Type, sub.Value);

    var claims = principal.Claims.ToList();
    claims.Add(new Claim("amr", "[\"fido\"]", JsonClaimValueTypes.JsonArray));

    ClaimsPrincipal cp = new();
    cp.AddIdentity(new ClaimsIdentity(claims, principal.Identity.AuthenticationType));

    foreach (var claim in cp.Claims)
    {
        claim.SetDestinations(GetDestinations(claim, cp));
    }

    var (Valid, Reason, Error) = ValidateIdTokenHintRequestPayload
        .IsValid(idTokenHintValidationResult.ClaimsPrincipal, 
        _idTokenHintValidationConfiguration,
        user.EntraIdOid,
        user.UserName);

    if (!Valid)
    {
        return UnauthorizedValidationParametersFailed(Reason, Error);
    }

    return SignIn(cp, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

The appsettings need to match your Microsoft Entra ID tenant and the used Azure App registration

"IdTokenHintValidationConfiguration": {
    "MetadataAddress": "https://login.microsoftonline.com/--your-tenant-id--/v2.0/.well-known/openid-configuration",
    "Issuer": "https://login.microsoftonline.com/--your-tenant-id--/v2.0",
    // client_id from the app we allow, i.e. MerillApp App Registration
    // We can enable or disable this validation if app aud are to be accepted.
    "Audience": "--your-client-id.app-using-the-mfa--",
    // If this is true, Audience (App registration client_id) is validated.
    "ValidateAudience": "False", 
    "TenantId": "--your-tenant-id--"
},

The IdTokenHintValidation Folder in the FidoMfaServer project implements the different flow validations.

Known Issues in demo server

  • Only a single FIDO2/passkeys key can be registered per user. In a productive system, multiple key registration must be possible.
  • User would need a recovery in a productive system.
  • Need to add a SCIM import of users
  • Register page should only allow validated Microsoft Entra ID users and use the OID from the id_token

Docs

https://learn.microsoft.com/en-gb/entra/identity/authentication/concept-authentication-external-method-provider

https://learn.microsoft.com/en-gb/entra/identity/authentication/how-to-authentication-external-method-manage

https://techcommunity.microsoft.com/t5/microsoft-entra-blog/public-preview-external-authentication-methods-in-microsoft/ba-p/4078808

Credits, non Microsoft projects used in this setup

https://documentation.openiddict.com/

https://github.com/passwordless-lib/fido2-net-lib

https://github.com/damienbod/AspNetCoreIdentityFido2Mfa