VisuAuth.EntraExternal
0.2.0
See the version list below for details.
dotnet add package VisuAuth.EntraExternal --version 0.2.0
NuGet\Install-Package VisuAuth.EntraExternal -Version 0.2.0
<PackageReference Include="VisuAuth.EntraExternal" Version="0.2.0" />
<PackageVersion Include="VisuAuth.EntraExternal" Version="0.2.0" />
<PackageReference Include="VisuAuth.EntraExternal" />
paket add VisuAuth.EntraExternal --version 0.2.0
#r "nuget: VisuAuth.EntraExternal, 0.2.0"
#:package VisuAuth.EntraExternal@0.2.0
#addin nuget:?package=VisuAuth.EntraExternal&version=0.2.0
#tool nuget:?package=VisuAuth.EntraExternal&version=0.2.0
VisuAuth.EntraExternal
Microsoft Entra External ID (formerly Azure AD B2C) adapter for VisuAuth. Targets customer-facing tenants where end users sign up and sign in with email + password local accounts (and optionally federated social providers) managed by Microsoft.
The admin surface VisuAuth exposes is the same /visuauth/admin/* pages the Workforce adapter and the Identity adapter feed. Capability flags swap the experience automatically:
- Login — the email + password form on
/visuauth/logindisappears in favour of a "Sign in with Microsoft" hint, becauseSupportsLocalLogin = false. The actual hosted login lives at{tenant}.ciamlogin.comand is wired in the consumer's app viaMicrosoft.Identity.Web(see v0.3 PR-C). - Admin —
/visuauth/admin/userslists customers live from Microsoft Graph. Create / disable / reset password / revoke sessions all hit Graph endpoints under the consumer's app-only credentials. - Roles —
/visuauth/admin/rolessurfaces the app roles declared in your registered app's manifest. Assign / remove via/users/{id}/appRoleAssignments.
Built against Microsoft.Graph 5.x with Azure.Identity ClientSecretCredential for the app-only flow. Shares the underlying Graph wiring with VisuAuth.Entra via the VisuAuth.EntraCore package.
When to pick this adapter over
VisuAuth.Entra. Workforce = employees of your company (Entra ID). External = customers of your SaaS who sign up themselves (Entra External ID / customer identity). If you'd be issuing invite links via email to colleagues, you want Workforce. If users land on a signup page and self-onboard, you want External.
Table of contents
- Install
- What this adapter does and doesn't do (capabilities)
- Set up an Entra External tenant + app registration
- Configure VisuAuth.EntraExternal in your app
- Operations supported
- Known limitations
- Troubleshooting
- Samples
Install
dotnet add package VisuAuth
dotnet add package VisuAuth.EntraExternal
VisuAuth is the meta-package (Admin UI + End-user UI + Abstractions). VisuAuth.EntraExternal brings Microsoft.Graph + Azure.Identity through the shared VisuAuth.EntraCore. Identity- or Workforce-adapter consumers don't pay this weight unless they reference VisuAuth.EntraExternal.
Minimal Program.cs for an External-only app (the one in samples/Sample.EntraExternalWebApp is ~30 lines):
using VisuAuth;
using VisuAuth.EntraExternal.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddVisuAuth()
.AddAdminUi()
.AddEndUserUi();
builder.Services.AddVisuAuthEntraExternal(builder.Configuration);
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapVisuAuth();
app.Run();
No AddIdentity, no AddDbContext, no JWT issuer, no OAuth providers. Everything Identity-shaped is opt-in via the other adapter.
What this adapter does and doesn't do (capabilities)
UserBackendCapabilities drive every conditional render in the UI. The Entra External adapter declares:
| Capability | Value | Why |
|---|---|---|
SupportsLocalLogin |
false |
Microsoft owns the hosted customer login UX at {tenant}.ciamlogin.com. The /visuauth/login page hides the email/password form on its own. |
SupportsRegistration |
true |
Admin Create at /admin/users/new works through Graph POST /users with an identities[] array. End-user self-service signup is hosted by Microsoft and wired via Microsoft.Identity.Web in v0.3 PR-C. |
SupportsPasswordReset |
true |
Admin reset issues a temporary password via Graph PATCH /users/{id} (passwordProfile). End-user SSPR lives on the hosted Microsoft surface. |
SupportsTwoFactor |
false |
Multi-factor enrolment happens on Microsoft's hosted surfaces. VisuAuth's TOTP pages don't apply. |
SupportsTwoFactorReset |
true |
Admin "reset 2FA" deletes the customer's registered authentication methods via Graph (shared with the Workforce adapter through EntraTwoFactorReset). Needs UserAuthenticationMethod.ReadWrite.All. |
SupportsLockout |
false |
Entra has smart lockout that admins don't toggle per-user. Admin "lock" is SetEnabled(false) instead. |
SupportsEmailConfirmation |
false |
Microsoft validates emails during the hosted signup flow. |
SupportsRoleManagement |
true |
App roles via Graph. Create/Rename/Delete throw NotSupported (declared in the application manifest); List + Assign + Remove work. |
SupportsSessionRevocation |
true |
Graph revokeSignInSessions invalidates every refresh token. |
SupportsExternalProviders |
false |
Federated providers (Google, Facebook, Apple) ARE supported by External ID, but they're configured at the tenant level and rendered by the hosted Microsoft login page — not by VisuAuth's providers admin section. |
SupportsCustomClaims |
true |
Graph extension properties + the user-attribute collection External ID ships with (v0.4 surfaces in the UI). |
SupportsAuditLog |
true |
Entra has its own auditLogs API. Opt in with AddVisuAuthEntraAuditLog() to surface sign-in + directory-change events on /admin/audit-log (needs AuditLog.Read.All + Entra ID P1). |
EmailDomainSuffix |
from EntraExternalOptions.DefaultEmailDomain |
When set, locks the Create-User email input to that suffix. |
Every flag above is a real decision point in the UI — flipping one reshapes what the admin sees.
Set up an Entra External tenant + app registration
Walkthrough that takes a fresh Microsoft account to a working VisuAuth.EntraExternal in ~25 minutes. Entra External tenants are free to create (unlike Workforce after the 2024 policy change), making this the lowest-friction path to try VisuAuth against a real Graph backend.
Step 1 — Create the External tenant
- Browse to
entra.microsoft.com, sign in. - Manage tenants (in the home page Shortcuts strip) → + Create.
- Pick External → Continue.
- Fill the form (
Tenant name,Initial domain name,Location) and Review + create. The initial domain becomes{name}.onmicrosoft.com— that's yourTenantDomainvalue (write it down). - After ~30 seconds, Go to tenant (or switch via the top-right account menu).
- From the tenant Overview card, copy the Tenant ID (GUID) — that's your
TenantIdconfig value.
Step 2 — Register the management app
- Sidebar Identity → Applications → App registrations → + New registration.
- Name (e.g.
VisuAuth Admin), Accounts in this organizational directory only, leave Redirect URI blank for now (PR-C will add an OIDC redirect for the customer-facing app — this management app doesn't need one). - After registering, copy from the Overview blade:
- Application (client) ID → goes into
VisuAuth:EntraExternal:ClientId.
- Application (client) ID → goes into
Step 3 — Microsoft Graph permissions
- App's sidebar → API permissions → + Add a permission → Microsoft Graph → Application permissions (NOT Delegated).
- Add all four:
User.Read.AllUser.ReadWrite.AllAppRoleAssignment.ReadWrite.AllApplication.Read.All
- Grant admin consent for {your tenant} at the top of the permissions table. Each row should show a green ✓ "Granted" check.
Forgetting "Grant admin consent" is the most common setup mistake — without it Graph returns
Authorization_RequestDeniedon every call.
Step 4 — Client secret
- App's sidebar → Certificates & secrets → + New client secret.
- Description (any), Expires 180 or 365 days for dev. Add.
- Immediately copy the Value column (not Secret ID) — it's only shown once.
Step 5 — (Optional) Declare app roles
Skip if you don't need the /admin/roles page populated. Otherwise:
- App's sidebar → App roles → + Create app role.
- Display name
Admin, valueadmin, Allowed member types: Users/Groups, Enabled. - Repeat for
Editoror whatever roles your app needs.
Step 6 — Populate user-secrets (dev) or environment (prod)
Dev:
cd path/to/your/app
dotnet user-secrets set "VisuAuth:EntraExternal:TenantId" "<directory-guid>"
dotnet user-secrets set "VisuAuth:EntraExternal:ClientId" "<application-guid>"
dotnet user-secrets set "VisuAuth:EntraExternal:ClientSecret" "<value-from-step-4>"
dotnet user-secrets set "VisuAuth:EntraExternal:TenantDomain" "<initial-domain-from-step-1>"
Production / containerised — use env vars with double-underscore (ASP.NET Core convention):
VisuAuth__EntraExternal__TenantId
VisuAuth__EntraExternal__ClientId
VisuAuth__EntraExternal__ClientSecret
VisuAuth__EntraExternal__TenantDomain
That's it — dotnet run, browse /visuauth/admin/users, and you'll see the tenant's directory.
Configure VisuAuth.EntraExternal in your app
EntraExternalOptions (everything bound from VisuAuth:EntraExternal):
| Property | Required | Default | Notes |
|---|---|---|---|
TenantId |
✅ | — | Directory GUID. Validate-on-start fails fast if missing. |
ClientId |
✅ | — | Application (client) ID. |
ClientSecret |
✅ | — | App secret VALUE (not Secret ID). |
TenantDomain |
✅ | — | Initial domain {tenant}.onmicrosoft.com. Used as the issuer when minting an External user's identities[] entry. |
AppRoleResourceId |
❌ | falls back to ClientId |
Application ID (not object ID!) of the app whose appRoles the role store surfaces. |
GraphBaseUrl |
❌ | https://graph.microsoft.com/v1.0 |
Override for sovereign clouds if Microsoft ships External there. |
DefaultEmailDomain |
❌ | null | Email domain to suggest in the admin Create User form. Unlike Workforce, External is permissive — customers can use any email domain. |
Two AddVisuAuthEntraExternal overloads — pick whichever fits the wiring style:
// Lambda — best when values come from code / env vars
builder.Services.AddVisuAuthEntraExternal(o =>
{
o.TenantId = builder.Configuration["VisuAuth:EntraExternal:TenantId"]!;
o.ClientId = builder.Configuration["VisuAuth:EntraExternal:ClientId"]!;
o.ClientSecret = builder.Configuration["VisuAuth:EntraExternal:ClientSecret"]!;
o.TenantDomain = builder.Configuration["VisuAuth:EntraExternal:TenantDomain"]!;
});
// Configuration section — best with appsettings / user-secrets / Key Vault
builder.Services.AddVisuAuthEntraExternal(builder.Configuration);
Both register the same service graph (TryAdd, so a consumer-registered test double wins): IUserStore → EntraExternalUserStore, IRoleStore → EntraExternalRoleStore, IAuthenticationFlow → EntraExternalAuthenticationFlow, plus stub IAuditWriter / IJwtIssuer / ITenantContext / IExternalLoginFlow so the End-user UI can resolve without Identity wired alongside.
Operations supported
IUserStore (admin user management)
| Method | Status | Graph endpoint(s) |
|---|---|---|
ListAsync |
✅ | GET /users with identities in the select; the search clause matches against identities/issuerAssignedId so the customer-typed email is searchable |
GetAsync / GetDetailAsync |
✅ | GET /users/{id} + role resolution against /servicePrincipals |
CreateAsync |
✅ | POST /users with identities[].signInType = emailAddress and issuer = TenantDomain |
UpdateAsync |
✅ partial | PATCH /users/{id} — DisplayName + BusinessPhones only. Identities / UPN / mail are deliberately not patched (rewriting the customer's identity from the admin UI would lock them out). |
DeleteAsync |
✅ | DELETE /users/{id} |
SetEnabledAsync |
✅ | PATCH /users/{id} (accountEnabled) |
ResetPasswordAsync |
✅ | PATCH /users/{id} (passwordProfile with forceChangePasswordNextSignIn = true) |
RevokeSessionsAsync |
✅ | POST /users/{id}/revokeSignInSessions |
ResetTwoFactorAsync |
✅ | Lists GET /users/{id}/authentication/methods and deletes each removable method via its typed endpoint (password left in place). Needs UserAuthenticationMethod.ReadWrite.All. |
IRoleStore (app role management)
Identical surface to the Workforce adapter — app roles are tenant-family-agnostic in Graph. See the Workforce adapter README for the per-method table.
IAuthenticationFlow (end-user surface — capability-driven)
Every method returns either RedirectToExternalProvider (SignInWithPasswordAsync) or a UserResult.Failure with the "use Microsoft" message. The Login page hides the password form on its own via SupportsLocalLogin = false; this flow exists as a safety net for direct-API / CLI callers. The real customer-facing OIDC redirect lands in v0.3 PR-C via Microsoft.Identity.Web.
Known limitations
| What | Why | Workaround |
|---|---|---|
| Identities / UPN / mail not editable from the admin UI | Rewriting a customer's identity from a generic admin form would lock them out of their own account | A dedicated email-change flow with verification mail is on the v0.4 roadmap. For now, edit identities in the Entra portal |
signInActivity is not in the default user $select |
Field needs AuditLog.Read.All + Entra ID P1 license; free External tenants get a 403 on the whole list call |
UserSummary.LastSignInAt stays null; UI renders "—". Subclass / override the store if you're on a paid tier |
| User list shows no total count | Graph doesn't return a total alongside a page (a $count call is a separate round-trip) |
PagedResult.TotalCount is null; the admin UI shows a per-page count and a working "Next" (cursor-based, following @odata.nextLink) instead of "page N of M" |
End-user /visuauth/login needs the OIDC wiring |
The hosted sign-in button is wired by VisuAuth.EntraExternal.Web |
Add the VisuAuth.EntraExternal.Web package + AddVisuAuthEntraExternalSignIn(...) (see its README) |
| Federated identities (Google, Facebook, Apple) not surfaced in the admin UI | The identities[] array can hold multiple entries; v0.3 detail page renders only the local-account one |
Configure providers in the Entra portal's "Identity providers" blade; v0.4+ may render the full identities list on the detail page |
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
OptionsValidationException: The TenantDomain field is required (startup) |
Missing user-secret / env var | Set VisuAuth:EntraExternal:TenantDomain to the initial domain {tenant}.onmicrosoft.com |
Authorization_RequestDenied: Insufficient privileges |
App permissions weren't granted admin consent, or wrong type (Delegated instead of Application) | Re-check API permissions: must be Application, must show "Granted for {tenant}" |
One or more properties contains invalid values on Create |
Identities array missing or the issuer doesn't match the configured TenantDomain |
Confirm TenantDomain matches the tenant's actual initial domain (no https://, no trailing slash) |
A null value was found for the property named 'businessPhones' |
Sending null where Graph expects an array | The mapper sends [] for "no phone" — file a bug if you still hit this |
Samples
samples/Sample.EntraExternalWebApp — minimal External-only reference
A ~30-line Program.cs showing the shortest path to a working VisuAuth admin against Microsoft Graph for an External tenant. No Identity / SQLite / JWT / OAuth wire-up. Mirrors samples/Sample.EntraWebApp (the Workforce equivalent) for easy comparison.
cd samples/Sample.EntraExternalWebApp
dotnet run
Lands on http://localhost:5260/visuauth/admin (the launch profile opens the browser there directly).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Azure.Identity (>= 1.17.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.0)
- Microsoft.Graph (>= 5.95.0)
- Microsoft.Kiota.Abstractions (>= 1.22.0)
- System.IdentityModel.Tokens.Jwt (>= 8.18.0)
- VisuAuth.Abstractions (>= 0.2.0)
- VisuAuth.EntraCore (>= 0.2.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on VisuAuth.EntraExternal:
| Package | Downloads |
|---|---|
|
VisuAuth.EntraExternal.Web
End-user OIDC sign-in for the VisuAuth.EntraExternal adapter. Wraps Microsoft.Identity.Web so the consumer's /visuauth/login renders a working "Sign in with Microsoft" button that round-trips through the External tenant's hosted sign-in page ({tenant}.ciamlogin.com). Replaces the no-op IExternalLoginFlow shipped by VisuAuth.EntraCore with a real implementation that reads the OIDC-authenticated principal, verifies the user against Microsoft Graph, and returns the success outcome the existing End-user UI expects. Consumers who only need admin CRUD (no End-user sign-in) skip this package and pay nothing. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.3.0-alpha.77 | 32 | 7/30/2026 |
| 0.3.0-alpha.76 | 34 | 7/29/2026 |
| 0.3.0-alpha.75 | 29 | 7/29/2026 |
| 0.3.0-alpha.74 | 31 | 7/29/2026 |
| 0.3.0-alpha.73 | 30 | 7/29/2026 |
| 0.3.0-alpha.72 | 40 | 7/29/2026 |
| 0.3.0-alpha.71 | 19 | 7/29/2026 |
| 0.3.0-alpha.70 | 31 | 7/29/2026 |
| 0.3.0-alpha.69 | 27 | 7/29/2026 |
| 0.3.0-alpha.68 | 48 | 7/15/2026 |
| 0.3.0-alpha.67 | 47 | 7/15/2026 |
| 0.3.0-alpha.66 | 50 | 7/15/2026 |
| 0.3.0-alpha.65 | 47 | 7/15/2026 |
| 0.3.0-alpha.64 | 53 | 7/15/2026 |
| 0.3.0-alpha.63 | 53 | 7/15/2026 |
| 0.3.0-alpha.62 | 51 | 7/14/2026 |
| 0.3.0-alpha.61 | 53 | 7/14/2026 |
| 0.3.0-alpha.60 | 51 | 7/14/2026 |
| 0.3.0-alpha.59 | 47 | 7/14/2026 |
| 0.2.0 | 125 | 5/29/2026 |
See https://github.com/VisuAuth/visuauth/blob/main/CHANGELOG.md for release notes.