[.NET 10] Add .NET MAUI Blazor Hybrid sample using Entra External ID#649
[.NET 10] Add .NET MAUI Blazor Hybrid sample using Entra External ID#649mattleibow wants to merge 12 commits intodotnet:mainfrom
Conversation
800764c to
597b596
Compare
|
Let me know when you're ready. We'll ping Stephen, but we won't wait more than a day or two. I'd like to get this out there for the community as soon as we can. |
|
Moving to draft until I get clarity around mac. |
New .NET MAUI Blazor Hybrid + ASP.NET Core Web App sample that replaces ASP.NET Core Identity with Microsoft Entra External ID (CIAM) for authentication. Web server: - Dual auth via BearerOrCookie policy scheme: OIDC + Cookie for browser users, JWT Bearer for MAUI API calls - Uses Microsoft.Identity.Web instead of EF Core/Identity - No local user database — Entra manages all accounts - Login/logout/weather API endpoints MAUI app: - MSAL.NET (Microsoft.Identity.Client) for native authentication - Interactive sign-in via system browser, silent token refresh - Android: MsalActivity for MSAL redirect URI callback - iOS: CFBundleURLTypes for MSAL redirect URI scheme Infrastructure: - Setup-Azure.ps1: Interactive PowerShell script that creates Entra app registrations, exposes API scope, generates client secret, and patches all config files with real values - Teardown-Azure.ps1: Cleanup script to remove app registrations - README.md with architecture overview and quick start guide Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… setup improvements - Add Account.razor shared page with two-column claims display - Add /authentication/register endpoint with prompt=create for CIAM sign-up - Add Register and Account nav links (Web + MAUI) - iOS: Add MSAL callback in AppDelegate.cs, keychain security group - Android: Add OnActivityResult callback, Chrome Custom Tabs <queries> manifest - Mac Catalyst: Custom ICustomWebUi using ASWebAuthenticationSession (MSAL has no native Mac Catalyst support - issue #3527) - Upgrade Microsoft.Identity.Client 4.70.0 → 4.83.1 - Rewrite Setup-Azure.ps1 as guided 5-step interactive walkthrough - Update Teardown-Azure.ps1 to match new setup flow - Update README quick start section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add missing Platforms/iOS/Entitlements.plist with keychain-access-groups - Use $(AppIdentifierPrefix)$(CFBundleIdentifier) instead of hardcoded adalcache group - Update WithIosKeychainSecurityGroup to match bundle identifier - Align MacCatalyst entitlements to same pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Convert all .cs files to file-scoped namespaces (C# 10) - Use primary constructors for MsalAuthenticationStateProvider and WeatherService (C# 12) - Replace collection initializers with collection expressions (C# 12) - Remove custom MacCatalystWebUi (ASWebAuthenticationSession workaround) - Mac Catalyst now uses WithSystemWebViewOptions like iOS - Remove network.server entitlement (no longer needed without loopback) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On first call to GetAuthenticationStateAsync, attempt a silent token acquisition using MSAL's cached credentials. This restores the user's authenticated session automatically when the app restarts, without requiring an interactive sign-in. Also converts MsalConfig.cs to file-scoped namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Step 3: Check if app registrations exist by display name before creating. Reuse existing apps and preserve client secrets when appsettings.json already has a real value. Only generate a new secret when the config file still has a placeholder. - Step 4: Check if signup_signin user flow exists before creating. If it exists, verify linked apps and add any missing ones. This allows the script to be safely re-run without duplicating resources or invalidating secrets on other machines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace credential reset with Graph API addPassword so new secrets never invalidate existing ones on other machines. When app already exists, interactively ask whether to keep the current secret, paste one from another machine, or generate a new one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update PSScriptRoot paths to resolve parent directory and update README with new script locations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Microsoft.Identity.Client.Desktop.WinUI3 package for embedded WebView2
auth on Windows (WAM broker doesn't support CIAM tenants)
- Add Microsoft.Identity.Client.Broker package (Windows-only)
- Update MauiVersion to 10.0.50 to fix HybridWebView.js build issue
- Configure WithWindowsDesktopFeatures for embedded WebView2 + WAM broker
- Set http://localhost redirect URI for Windows (CIAM requires explicit URI,
WithDefaultRedirectUri resolves to nativeclient on MAUI which doesn't work)
- Pass WinUI3 Window object (not IntPtr handle) to WithParentActivityOrWindow
for embedded WebView2 compatibility
- Move MsalConfig.cs to project root
- Add platform-specific redirect URI via #if WINDOWS in MsalConfig
- Register http://localhost in Setup-Azure.ps1 alongside msal{ClientId}://auth
- Add Windows token cache persistence via SecureStorage (DPAPI-backed) since
MSAL only persists automatically on iOS (Keychain) and Android (SharedPrefs)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract MSAL client setup into MsalServiceExtensions.AddMsalClient() extension on IServiceCollection, cleaning up MauiProgram.cs - Extract platform-specific interactive auth config into WithPlatformOptions() extension on AcquireTokenInteractiveParameterBuilder, removing #if directives from MsalAuthenticationStateProvider - Extract Windows token cache persistence into private EnableSecureStorageTokenCachePersistence() method using MAUI SecureStorage (DPAPI-backed); iOS/Android persist natively via Keychain/SharedPreferences - Add null checks for platform window/activity in WithPlatformOptions() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align iOS keychain-access-groups entitlement with Mac Catalyst to use com.microsoft.adalcache (MSAL's default shared keychain group). Add WithIosKeychainSecurityGroup to the MSAL builder for iOS and Mac Catalyst so tokens persist correctly across app restarts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment was marked as outdated.
This comment was marked as outdated.
MSAL.NET doesn't ship a maccatalyst TFM (issue #3527), so the generic .NET assembly is used at runtime. This causes two problems: 1. AcquireTokenInteractive throws PlatformNotSupportedException because the generic assembly has no system browser integration. 2. The token cache is in-memory only — tokens are lost on app restart. Auth fix: Add MacCatalystWebUi implementing ICustomWebUi, which drives ASWebAuthenticationSession directly. Wired up via a WithMacCatalystWebView() extension method on AcquireTokenInteractiveParameterBuilder. Persistence fix: Enable SecureStorage-based token cache serialization for Mac Catalyst (same mechanism already used on Windows). Both workarounds reference MSAL issue #3527 and can be removed once MSAL ships native Mac Catalyst support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
597b596 to
555df7b
Compare
|
@guardrex I think this is what we are going to have to do. I am still not sure what the best solution for Mac Catalyst is, but this sample has just 1 file of things to make it work. It is documented as a workaround for now, and hopefully Entra can support Mac Catalyst a bit better as well as maybe make Windows smoother. But besides a few Entra extensions, the sample and the auth seems to work correctly. |
|
Thanks @mattleibow! 👍 Writing a companion article for the doc set would be our normal approach to raising visibility for the sample. If so, the article would be worked per a new issue on the main doc set repo. I can set up the issue and write the article draft. Then, you can review and provide feedback and additional content from there. Sound good? @halter73 ... Do you want to review this sample? |
New sample:
10.0/MauiBlazorWebEntraDemonstrates Microsoft Entra External ID (CIAM) authentication for a .NET 10 MAUI Blazor Hybrid app with a shared Blazor Web companion.
Architecture
Platform support
Key features
Setup-Azure.ps1— Guided 5-step interactive script to create CIAM tenant, register apps, configure user flowsTeardown-Azure.ps1— Clean removal of app registrations and user flowsprompt=createdeep-link (screen_hint=signupis not supported by CIAM)<queries>manifest entry for API 30+)ICustomWebUiusingASWebAuthenticationSessionsince MSAL has no native Mac Catalyst supportSetup
Run
Setup-Azure.ps1for guided configuration, or see README for manual steps.Known Issues and Platform Workarounds
Windows: Token Cache Persistence
Problem: On Windows, MSAL.NET uses an in-memory token cache by default (
InMemoryPartitionedUserTokenCacheAccessor). When the app closes, the cache is lost —GetAccountsAsync()returns empty on next launch, forcing the user to sign in interactively every time.This is not an issue on iOS or Android because MSAL overrides the base class with platform-native persistent storage:
iOSTokenCacheAccessor— usesSecKeyChain(iOS Keychain)AndroidTokenCacheAccessor— usesSharedPreferencesWindows has no such override — it falls through to the base
InMemoryPartitionedUserTokenCacheAccessor.Solution: We use MSAL's
SetBeforeAccessAsync/SetAfterAccessAsynccache serialization callbacks to persist the serialized token cache (viaSerializeMsalV3/DeserializeMsalV3) to .NET MAUI'sSecureStorage. On Windows,SecureStorageencrypts data usingDataProtectionProvider("LOCAL=user"), scoped to the current Windows user. Packaged (MSIX) apps store values inApplicationData.Current.LocalSettings; unpackaged (WindowsPackageType=None) apps persist to asecurestorage.datJSON file in AppData.The implementation is in
MsalServiceExtensions.EnableSecureStorageTokenCachePersistence()and is wired up via#if WINDOWS || MACCATALYST.Windows: Embedded WebView2 via
Microsoft.Identity.Client.Desktop.WinUI3WAM (Web Account Manager) broker does not support Entra External ID (CIAM) tenants — it only works with workforce Entra ID. When WAM encounters a CIAM authority, it falls back to the system browser, which is a poor UX (opens external browser, requires
http://localhostloopback listener).Instead, we use
WithWindowsDesktopFeatures(BrokerOptions)from theMicrosoft.Identity.Client.Desktop.WinUI3package. This registers WAM as the primary auth mechanism and an embedded WebView2 browser as fallback. For CIAM tenants, WAM fails gracefully and the embedded WebView2 shows the login page inside the app window — no external browser needed.The WebView2 intercepts navigation to the redirect URI (
http://localhost) in-process via theWebView2_NavigationStartingevent handler. No HTTP listener is opened and no network traffic occurs.Key requirement:
WithParentActivityOrWindow()must receive aMicrosoft.UI.Xaml.Windowobject (not anIntPtrhandle) for the embedded WebView2 to work — passing a window handle causesMsalException: Invalid owner window type.Windows: Redirect URI
http://localhostWithDefaultRedirectUri()on MAUI resolves tohttps://login.microsoftonline.com/common/oauth2/nativeclient(nothttp://localhostas it does on plain .NET Core). This legacynativeclientURI does not work with CIAM tenants. We explicitly setWithRedirectUri("http://localhost")which works for both the embedded WebView2 flow (intercepted in-process) and the system browser fallback (loopback port matching per RFC 8252 §7.3).The
Setup-Azure.ps1script registers bothmsal{ClientId}://auth(iOS/Android/Mac) andhttp://localhost(Windows) as redirect URIs on the app registration.Mac Catalyst: MSAL does not ship a
maccatalystTFM (#3527)Problem: MSAL.NET (as of v4.x) does not include a
net*-maccatalysttarget framework. When running on Mac Catalyst, NuGet resolves to the genericnet8.0assembly instead of a platform-specific one. This causes two issues:System browser auth throws
PlatformNotSupportedException— MSAL'sAcquireTokenInteractivecallsOpenBrowserAsyncwhich is not implemented in the generic assembly. The call chain goes throughPlatformProxy.CreateWebUiFactory()→InteractiveRequest→ throws because no system browser integration exists for the generic .NET target.Token cache is not persisted — The generic assembly uses
InMemoryPartitionedUserTokenCacheAccessor(same as Windows). Unlike iOS where MSAL'snet*-iosassembly overrides this withiOSTokenCacheAccessor(backed bySecKeyChain), the generic assembly has no Keychain integration. Tokens are lost on every app restart.Solution — Authentication: A custom
ICustomWebUiimplementation uses Apple'sASWebAuthenticationSessionto present the Entra login page. This is the same API that MSAL uses internally on iOS viaSystemWebUI— we just call it directly since MSAL's generic assembly doesn't know about it. The implementation:ASWebAuthenticationSessionwith the authorization URI and redirect schemeASWebAuthenticationPresentationContextProviderto anchor the session to the app windowWithMacCatalystWebView()extension method onAcquireTokenInteractiveParameterBuilderSolution — Token Persistence: The same
SecureStorage-based cache serialization used for Windows is also enabled for Mac Catalyst via#if WINDOWS || MACCATALYST. On macOS, MAUI'sSecureStorageis backed by the macOS Keychain, providing encrypted per-app token storage that survives app restarts.Both workarounds reference MSAL issue #3527 and can be removed once MSAL ships a native
net*-maccatalystTFM.Android: Chrome Custom Tabs Package Visibility
On Android API 30+, apps must declare which other apps they query. Without the
<queries>entry forandroid.support.customtabs.action.CustomTabsServiceinAndroidManifest.xml, MSAL cannot discover Chrome Custom Tabs and falls back to opening a full separate browser window — a degraded UX.CIAM: Sign-up Deep Link
Entra External ID (CIAM) does not support
screen_hint=signup(it is silently ignored). Useprompt=createinstead to deep-link to the sign-up page.