{"id":15410603,"url":"https://github.com/damienbod/mfaserver","last_synced_at":"2026-03-08T17:34:26.773Z","repository":{"id":237861129,"uuid":"744600443","full_name":"damienbod/MfaServer","owner":"damienbod","description":"Microsoft Entra ID external authentication methods","archived":false,"fork":false,"pushed_at":"2025-02-03T21:06:13.000Z","size":3894,"stargazers_count":14,"open_issues_count":5,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-06T06:15:59.952Z","etag":null,"topics":["aspnetcore","dotnet","entraid","fido2","graph","mfa","oidc","openidconnect","passkeys"],"latest_commit_sha":null,"homepage":"https://learn.microsoft.com/en-gb/entra/identity/authentication/concept-authentication-external-method-provider","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/damienbod.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-17T16:23:24.000Z","updated_at":"2025-02-03T21:02:39.000Z","dependencies_parsed_at":"2024-05-22T18:55:04.411Z","dependency_job_id":null,"html_url":"https://github.com/damienbod/MfaServer","commit_stats":null,"previous_names":["damienbod/mfaserver"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FMfaServer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FMfaServer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FMfaServer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FMfaServer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/damienbod","download_url":"https://codeload.github.com/damienbod/MfaServer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249191304,"owners_count":21227540,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnetcore","dotnet","entraid","fido2","graph","mfa","oidc","openidconnect","passkeys"],"created_at":"2024-10-01T16:45:16.351Z","updated_at":"2026-03-08T17:34:26.760Z","avatar_url":"https://github.com/damienbod.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FIDO2/passkeys MFA Server\n\n_Microsoft Entra ID: external authentication methods_\n\n[Implement a Microsoft Entra ID external authentication method using ASP.NET Core and OpenIddict](https://damienbod.com/2024/05/27/implement-a-microsoft-entra-id-external-authentication-method-using-asp-net-core-and-openiddict/)\n\n[![.NET](https://github.com/damienbod/MfaServer/actions/workflows/dotnet.yml/badge.svg)](https://github.com/damienbod/MfaServer/actions/workflows/dotnet.yml)  [![Deploy app to Entra ID Web App](https://github.com/damienbod/MfaServer/actions/workflows/azure-webapps-dotnet-core.yml/badge.svg)](https://github.com/damienbod/MfaServer/actions/workflows/azure-webapps-dotnet-core.yml)\n\nThe 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.\n\n![flow](https://github.com/damienbod/MfaServer/blob/main/images/me-id_external-authn-flows_01.png)\n\n## Setup Microsoft Entra ID\n\nSee the Microsoft Entra ID: external authentication methods [documentation](https://learn.microsoft.com/en-gb/entra/identity/authentication/concept-authentication-external-method-provider).\n\n### Microsoft Graph\n\nMicrosoft Graph is used to create the Microsoft Entra ID: external authentication method.\n\nRequires the delegated **Policy.ReadWrite.AuthenticationMethod** permission \n\nPOST \n\nhttps://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations\n\n```\n{\n    \"@odata.type\": \"#microsoft.graph.externalAuthenticationMethodConfiguration\",\n    \"displayName\": \"--name-of-provider--\", // Displayed in login\n    \"state\": \"enabled\",\n    \"appId\": \"--app-registration-clientId--\", // external authentication app registration, see docs\n    \"openIdConnectSetting\": {\n        \"clientId\": \"--your-client_id-from-external-provider--\",\n        \"discoveryUrl\": \"--your-external-provider-url--/.well-known/openid-configuration\"\n    },\n    \"includeTarget\": { // switch this if only specific users are required\n        \"targetType\": \"group\",\n        \"id\": \"all_users\"\n    }\n}\n```\n\nThe https://developer.microsoft.com/en-us/graph/graph-explorer can be used to run this.\n\n\n## Setup FIDO2/passkeys MFA Server \n\n_(OpenIddict, fido2-net-lib, ASP.NET Core Identity)_\n\n### Core Setup OpenIddict\n\nThe 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.\n\nSee [OpenIddict](https://documentation.openiddict.com/guides/getting-started/creating-your-own-server-instance.html)\n\nOpenID 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. \n\nHere is an example of a possible OpenIddict client setup:\n\n```csharp\nawait manager.CreateAsync(new OpenIddictApplicationDescriptor\n{\n    ClientId = \"oidc-implicit-mfa\",\n    ConsentType = ConsentTypes.Implicit,\n    DisplayName = \"OIDC Implicit Flow for MFA\",\n    DisplayNames =\n    {\n        [CultureInfo.GetCultureInfo(\"fr-FR\")] = \"Application cliente MVC\"\n    },\n    RedirectUris =\n    {\n        new Uri(\"https://login.microsoftonline.com/common/federation/externalauthprovider\")\n    },\n    Permissions =\n    {\n        Permissions.Endpoints.Authorization,\n        Permissions.Endpoints.Revocation,\n        Permissions.GrantTypes.Implicit,\n        Permissions.ResponseTypes.IdToken,\n        Permissions.Scopes.Email,\n        Permissions.Scopes.Profile,\n        Permissions.Scopes.Roles\n    }\n});\n```\n\nMicrosoft Entra ID uses the **redirect_uri**: \n\nhttps://login.microsoftonline.com/common/federation/externalauthprovider\n\n### Setup Fido2/passkeys\n\nThe FIDO2/passkeys authentication was implement using the [fido2-net-lib](https://github.com/passwordless-lib/fido2-net-lib) nuget package.\n\nThis was implemented using the [AspNetCoreIdentityFido2Passwordless](https://github.com/damienbod/AspNetCoreIdentityFido2Mfa/tree/main/AspNetCoreIdentityFido2Passwordless) implementation. You need to replace all the ASP.NET Core Identity Razor Pages and add the WebAuthn js scripts from the wwwroot.\n\nThe Fido2 appsettings configuration must be changed to match the server deployment.\n\n```\n\"Fido2\": {\n    // This must match the deployment domain\n    \"ServerDomain\": \"fidomfaserver.azurewebsites.net\",\n    \"ServerName\": \"FidoMfaServer\",\n    \"Origins\": [ \"https://fidomfaserver.azurewebsites.net\" ],\n    \"TimestampDriftTolerance\": 300000,\n    \"MDSAccessKey\": null\n},\n```\n\n### Setup OpenIddict Implicit flow for ME-ID external authn\n\nThe 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. \n\nThe 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.\n\n```csharp\ncase ConsentTypes.Implicit:\ncase ConsentTypes.External when authorizations.Any():\ncase ConsentTypes.Explicit when authorizations.Any() \u0026\u0026 !request.HasPrompt(Prompts.Consent):\n    var principal = await _signInManager.CreateUserPrincipalAsync(user);\n\n    // Note: in this sample, the granted scopes match the requested scope\n    // but you may want to allow the user to uncheck specific scopes.\n    // For that, simply restrict the list of scopes before calling SetScopes.\n    principal.SetScopes(request.GetScopes());\n    principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());\n\n    // Automatically create a permanent authorization to avoid requiring explicit consent\n    // for future authorization or token requests containing the same scopes.\n    var authorization = authorizations.LastOrDefault();\n    if (authorization == null)\n    {\n        authorization = await _authorizationManager.CreateAsync(\n            principal: principal,\n            subject: await _userManager.GetUserIdAsync(user),\n            client: await _applicationManager.GetIdAsync(application),\n            type: AuthorizationTypes.Permanent,\n            scopes: principal.GetScopes());\n    }\n\n    principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));\n\n    //get well known endpoints and validate access token sent in the assertion\n    var configurationManager = new ConfigurationManager\u003cOpenIdConnectConfiguration\u003e(\n        _idTokenHintValidationConfiguration.MetadataAddress,\n        new OpenIdConnectConfigurationRetriever());\n\n    var wellKnownEndpoints = await configurationManager.GetConfigurationAsync();\n\n    var idTokenHintValidationResult = ValidateIdTokenHintRequestPayload.ValidateTokenAndSignature(\n        request.IdTokenHint,\n        _idTokenHintValidationConfiguration,\n        wellKnownEndpoints.SigningKeys,\n        _testingMode);\n\n    if (!idTokenHintValidationResult.Valid)\n    {\n        return UnauthorizedValidationParametersFailed(idTokenHintValidationResult.Reason,\n            \"id_token_hint validation failed\");\n    }\n\n    var requestedClaims = System.Text.Json.JsonSerializer.Deserialize\u003cclaims\u003e(request.Claims);\n\n    principal.AddClaim(\"acr\", \"possessionorinherence\");\n\n    var sub = idTokenHintValidationResult.ClaimsPrincipal\n        .Claims.First(d =\u003e d.Type == \"sub\");\n\n    principal.RemoveClaims(\"sub\");\n    principal.AddClaim(sub.Type, sub.Value);\n\n    var claims = principal.Claims.ToList();\n    claims.Add(new Claim(\"amr\", \"[\\\"fido\\\"]\", JsonClaimValueTypes.JsonArray));\n\n    ClaimsPrincipal cp = new();\n    cp.AddIdentity(new ClaimsIdentity(claims, principal.Identity.AuthenticationType));\n\n    foreach (var claim in cp.Claims)\n    {\n        claim.SetDestinations(GetDestinations(claim, cp));\n    }\n\n    var (Valid, Reason, Error) = ValidateIdTokenHintRequestPayload\n        .IsValid(idTokenHintValidationResult.ClaimsPrincipal, \n        _idTokenHintValidationConfiguration,\n        user.EntraIdOid,\n        user.UserName);\n\n    if (!Valid)\n    {\n        return UnauthorizedValidationParametersFailed(Reason, Error);\n    }\n\n    return SignIn(cp, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);\n\n```\n\nThe appsettings need to match your Microsoft Entra ID tenant and the used Azure App registration\n\n```\n\"IdTokenHintValidationConfiguration\": {\n    \"MetadataAddress\": \"https://login.microsoftonline.com/--your-tenant-id--/v2.0/.well-known/openid-configuration\",\n    \"Issuer\": \"https://login.microsoftonline.com/--your-tenant-id--/v2.0\",\n    // client_id from the app we allow, i.e. MerillApp App Registration\n    // We can enable or disable this validation if app aud are to be accepted.\n    \"Audience\": \"--your-client-id.app-using-the-mfa--\",\n    // If this is true, Audience (App registration client_id) is validated.\n    \"ValidateAudience\": \"False\", \n    \"TenantId\": \"--your-tenant-id--\"\n},\n```\n\nThe **IdTokenHintValidation** Folder in the FidoMfaServer project implements the different flow validations.\n\n## Known Issues in demo server\n\n- Only a single FIDO2/passkeys key can be registered per user. In a productive system, multiple key registration must be possible.\n- User would need a recovery in a productive system.\n- Need to add a SCIM import of users\n- Register page should only allow validated Microsoft Entra ID users and use the OID from the id_token\n\n## Docs\n\nhttps://learn.microsoft.com/en-gb/entra/identity/authentication/concept-authentication-external-method-provider\n\nhttps://learn.microsoft.com/en-gb/entra/identity/authentication/how-to-authentication-external-method-manage\n\nhttps://techcommunity.microsoft.com/t5/microsoft-entra-blog/public-preview-external-authentication-methods-in-microsoft/ba-p/4078808\n\n## Credits, non Microsoft projects used in this setup\n\nhttps://documentation.openiddict.com/\n\nhttps://github.com/passwordless-lib/fido2-net-lib\n\nhttps://github.com/damienbod/AspNetCoreIdentityFido2Mfa\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdamienbod%2Fmfaserver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdamienbod%2Fmfaserver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdamienbod%2Fmfaserver/lists"}