Skip to content

Commit 555df7b

Browse files
mattleibowCopilot
andcommitted
Add Mac Catalyst auth workaround and token persistence
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>
1 parent eaeb0dc commit 555df7b

3 files changed

Lines changed: 138 additions & 4 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using AuthenticationServices;
2+
using Foundation;
3+
using Microsoft.Identity.Client.Extensibility;
4+
using UIKit;
5+
6+
namespace Microsoft.Identity.Client;
7+
8+
/// <summary>
9+
/// Extension method to configure Mac Catalyst authentication using
10+
/// ASWebAuthenticationSession. MSAL doesn't ship a maccatalyst TFM yet,
11+
/// so the built-in system browser flow throws PlatformNotSupportedException.
12+
/// This provides an equivalent experience using ICustomWebUi.
13+
///
14+
/// Remove this file once MSAL ships Mac Catalyst support:
15+
/// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527
16+
/// </summary>
17+
internal static class MacCatalystWebViewExtensions
18+
{
19+
/// <summary>
20+
/// Configures the interactive token request to use ASWebAuthenticationSession
21+
/// on Mac Catalyst, working around the missing maccatalyst TFM in MSAL.
22+
/// Replace with <c>.WithSystemWebViewOptions(new SystemWebViewOptions())</c>
23+
/// once MSAL ships Mac Catalyst support.
24+
/// </summary>
25+
public static AcquireTokenInteractiveParameterBuilder WithMacCatalystWebView(
26+
this AcquireTokenInteractiveParameterBuilder builder) =>
27+
builder.WithCustomWebUi(new MacCatalystWebUi());
28+
29+
private class MacCatalystWebUi : ICustomWebUi
30+
{
31+
public async Task<Uri> AcquireAuthorizationCodeAsync(
32+
Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken)
33+
{
34+
var tcs = new TaskCompletionSource<Uri>();
35+
36+
using var registration = cancellationToken.Register(() => tcs.TrySetCanceled());
37+
38+
var callbackScheme = redirectUri.Scheme;
39+
40+
MainThread.BeginInvokeOnMainThread(() =>
41+
{
42+
// The callback-based constructor is deprecated on macCat 17.4+ in favor
43+
// of ASWebAuthenticationSessionCallback, but remains the simplest approach
44+
// for broad compatibility. Suppress the warning until MSAL ships native
45+
// Mac Catalyst support and this file can be deleted entirely.
46+
#pragma warning disable CA1422
47+
var session = new ASWebAuthenticationSession(
48+
new NSUrl(authorizationUri.AbsoluteUri),
49+
callbackScheme,
50+
(callbackUrl, error) =>
51+
{
52+
if (error is not null)
53+
{
54+
if (error.Code == (long)ASWebAuthenticationSessionErrorCode.CanceledLogin)
55+
tcs.TrySetException(new MsalClientException(
56+
"authentication_canceled", "User canceled authentication."));
57+
else
58+
tcs.TrySetException(new Exception(
59+
$"ASWebAuthenticationSession error: {error.LocalizedDescription}"));
60+
}
61+
else if (callbackUrl is not null)
62+
{
63+
tcs.TrySetResult(new Uri(callbackUrl.ToString()));
64+
}
65+
else
66+
{
67+
tcs.TrySetException(new Exception(
68+
"No callback URL received from ASWebAuthenticationSession."));
69+
}
70+
});
71+
#pragma warning restore CA1422
72+
73+
session.PresentationContextProvider = new PresentationContextProvider();
74+
session.PrefersEphemeralWebBrowserSession = false;
75+
76+
if (!session.Start())
77+
{
78+
tcs.TrySetException(new Exception("Failed to start ASWebAuthenticationSession."));
79+
}
80+
});
81+
82+
return await tcs.Task;
83+
}
84+
85+
private class PresentationContextProvider : NSObject, IASWebAuthenticationPresentationContextProviding
86+
{
87+
public UIWindow GetPresentationAnchor(ASWebAuthenticationSession session)
88+
{
89+
var scene = UIApplication.SharedApplication.ConnectedScenes
90+
.OfType<UIWindowScene>()
91+
.FirstOrDefault();
92+
93+
return scene?.KeyWindow
94+
?? scene?.Windows.FirstOrDefault()
95+
?? throw new InvalidOperationException("No window found for authentication presentation.");
96+
}
97+
}
98+
}
99+
}

10.0/MauiBlazorWebEntra/MauiBlazorWebEntra/Services/MsalServiceExtensions.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ public static IServiceCollection AddMsalClient(this IServiceCollection services)
2626

2727
var msalClient = msalBuilder.Build();
2828

29-
#if WINDOWS
30-
// Use .NET MAUI secure storage for Windows as Android and iOS automatically
31-
// persist using native platform features.
29+
#if WINDOWS || MACCATALYST
30+
// MSAL persists tokens natively on iOS (Keychain) and Android (SharedPreferences).
31+
// Windows and Mac Catalyst need manual persistence — Windows because it uses a
32+
// generic .NET assembly, Mac Catalyst because MSAL doesn't ship a maccatalyst TFM yet.
33+
// Remove MACCATALYST from this condition once MSAL ships Mac Catalyst support:
34+
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527
3235
msalClient.EnableSecureStorageTokenCachePersistence();
3336
#endif
3437

@@ -78,10 +81,19 @@ public static AcquireTokenInteractiveParameterBuilder WithPlatformOptions(this A
7881

7982
return builder.WithParentActivityOrWindow(activity);
8083

81-
#elif IOS || MACCATALYST
84+
#elif IOS
8285

8386
return builder.WithSystemWebViewOptions(new SystemWebViewOptions());
8487

88+
#elif MACCATALYST
89+
90+
// MSAL doesn't ship a maccatalyst TFM yet, so WithSystemWebViewOptions
91+
// throws PlatformNotSupportedException. Use WithMacCatalystWebView() which
92+
// drives ASWebAuthenticationSession via ICustomWebUi.
93+
// Remove this #elif once MSAL ships Mac Catalyst support:
94+
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527
95+
return builder.WithMacCatalystWebView();
96+
8597
#elif WINDOWS
8698

8799
if (IPlatformApplication.Current?.Application is not IApplication app)

10.0/MauiBlazorWebEntra/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ To remove the Azure app registrations:
7171
pwsh ./scripts/Teardown-Azure.ps1
7272
```
7373

74+
## Known Issues
75+
76+
### Windows: MSAL token cache is not persisted by default
77+
78+
On iOS and Android, MSAL persists tokens automatically using native platform features (Keychain and SharedPreferences respectively). On Windows, the generic .NET assembly does not include built-in token persistence, so tokens would be lost when the app restarts.
79+
80+
This sample calls `EnableSecureStorageTokenCachePersistence()` to persist the MSAL token cache using .NET MAUI [`SecureStorage`](https://github.com/dotnet/maui/blob/main/src/Essentials/src/SecureStorage/SecureStorage.windows.cs), which encrypts data using [`DataProtectionProvider("LOCAL=user")`](https://learn.microsoft.com/uwp/api/windows.security.cryptography.dataprotection.dataprotectionprovider) scoped to the current Windows user.
81+
82+
### Mac Catalyst: MSAL does not ship a `maccatalyst` TFM
83+
84+
MSAL.NET (as of v4.x) does not include a `net*-maccatalyst` target framework. When running on Mac Catalyst, the app loads the generic .NET assembly instead of a platform-specific one. This causes two problems:
85+
86+
1. **System browser auth throws `PlatformNotSupportedException`** — MSAL's browser-based interactive login is not implemented in the generic assembly.
87+
2. **Token cache is not persisted** — the generic assembly has no native token persistence, so users must sign in again every time the app restarts.
88+
89+
#### Workarounds in this sample
90+
91+
**Authentication:** A custom [`ICustomWebUi`](MauiBlazorWebEntra/Platforms/MacCatalyst/MacCatalystWebUi.cs) implementation uses Apple's `ASWebAuthenticationSession` to handle the interactive login flow. It is wired up via a `WithMacCatalystWebView()` extension method in [`MsalServiceExtensions.cs`](MauiBlazorWebEntra/Services/MsalServiceExtensions.cs).
92+
93+
**Token persistence:** The `SecureStorage`-based token cache (normally used only on Windows) is also enabled for Mac Catalyst so that tokens survive app restarts.
94+
95+
Both workarounds include comments referencing [MSAL issue #3527](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527) and can be removed once MSAL ships native Mac Catalyst support.
96+
7497
## Key Differences from MauiBlazorWebIdentity
7598

7699
| Aspect | MauiBlazorWebIdentity | MauiBlazorWebEntra |

0 commit comments

Comments
 (0)