Sign in with Apple, Google, and Microsoft in .NET MAUI 10: OAuth for iOS and Android (2026)
A practical .NET MAUI 10 OAuth walkthrough for Sign in with Apple, Google, and Microsoft, covering WebAuthenticator, MSAL.NET, PKCE, secure token storage, and the App Store review rules that block first submissions.
To add Sign in with Apple, Google, and Microsoft to a .NET MAUI 10 app, wire each provider through WebAuthenticator (or MSAL.NET for Microsoft), register platform-specific redirect URIs in your app manifests, exchange the authorization code for tokens using PKCE, and persist the refresh token in SecureStorage. On iOS you must also offer Sign in with Apple if you ship any third-party social login, or App Store review will reject the build. Honestly, that last point catches more first-time MAUI shippers than any other. This guide walks through the full 2026 OAuth flow for all three providers, including the platform gotchas that eat a sprint if you don't know them upfront.
Use WebAuthenticator.AuthenticateAsync for Apple and Google; use MSAL.NET's PublicClientApplication for Microsoft Entra ID and Microsoft consumer accounts.
PKCE is mandatory for public mobile clients as of 2026, and you should never ship a client secret in your app.
Sign in with Apple is required by App Store Review Guideline 4.8 if you offer any other social login, and must be visually equivalent.
Google's GoogleSignIn SDK is deprecated on Android in favor of Credential Manager; on iOS it still ships via CocoaPods.
Store refresh tokens in SecureStorage (Keychain / EncryptedSharedPreferences), never plain Preferences.
Custom URL scheme redirect URIs must match what you register in Info.plist, AndroidManifest.xml, and the provider console. Typos here cause 90% of "authentication cancelled" bugs.
Why OAuth changed for mobile in 2026
So, if you last touched mobile OAuth in the Xamarin.Auth era, almost everything has moved. On my team we've re-integrated social sign-in three times in eighteen months: once to migrate off Google's deprecated GoogleSignIn Android SDK, once to add PKCE after Azure AD B2C started rejecting non-PKCE flows for public clients, and once to add Sign in with Apple after the App Store rejected our v2 build. None of those changes were optional, so it pays to understand the landscape before you write a line of code.
Here's the 2026 baseline. Every public mobile client uses the RFC 7636 PKCE extension to the authorization code flow. Implicit flow is deprecated. Client secrets in mobile apps are treated as leaked by default. If you see one in a tutorial, close the tab. .NET MAUI's WebAuthenticator handles the browser round-trip and returns query parameters back to your app via a custom URL scheme. For Microsoft, MSAL.NET wraps the same PKCE flow with broker support (via the Microsoft Authenticator app) so users don't retype their password on every device.
The second big shift is Sign in with Apple. Guideline 4.8 of the App Store Review Guidelines requires it if your app offers third-party or social login, and reviewers now check for visual parity, so you can't hide it three taps deep. Play Store has no equivalent requirement, but users increasingly expect passwordless sign-in on Android too, which is where Credential Manager comes in. I'll cover all three providers with production-ready code.
Setting up WebAuthenticator in .NET MAUI 10
WebAuthenticator is the .NET MAUI abstraction over the platform's system browser (ASWebAuthenticationSession on iOS and Chrome Custom Tabs on Android). Both give you a sandboxed browser that shares cookies with the system browser, so users stay signed into Google or Apple, but returns control to your app via a redirect URI. The official WebAuthenticator docs cover the basic setup; here's what actually works for a shipping app.
First, add the browser callback intent filter on Android. In Platforms/Android/AndroidManifest.xml:
The scheme (myapp) must be globally unique on the device; I usually reverse the bundle identifier (so com.acme.checkout becomes the scheme). Never use a generic scheme like app://. Another installed app could hijack the callback, and you won't know until users start complaining.
Finally, register a wrapper service in MauiProgram.cs:
I use a single IAuthService facade with a method per provider so ViewModels don't care which OAuth server they're talking to. That pays off the day a product manager asks you to add a fourth provider.
How do I implement Sign in with Apple in .NET MAUI?
Sign in with Apple has two flavors: the native AuthenticationServices framework on iOS (which I strongly recommend on iPhone/iPad) and a web-based OAuth flow via WebAuthenticator on Android. Apple requires you to use the native flow on Apple platforms — using the web flow on iOS invites App Store review to reject the build for a substandard experience.
Native Sign in with Apple on iOS
Add a partial class handler under Platforms/iOS/:
using AuthenticationServices;
using Foundation;
using UIKit;
public partial class AppleAuthService : NSObject, IASAuthorizationControllerDelegate,
IASAuthorizationControllerPresentationContextProviding
{
private TaskCompletionSource<AppleCredential>? _tcs;
public Task<AppleCredential> SignInAsync(string nonce)
{
_tcs = new TaskCompletionSource<AppleCredential>();
var provider = new ASAuthorizationAppleIdProvider();
var request = provider.CreateRequest();
request.RequestedScopes = new[] { ASAuthorizationScope.FullName, ASAuthorizationScope.Email };
request.Nonce = Sha256(nonce); // Apple wants the SHA-256 hex of the raw nonce
var controller = new ASAuthorizationController(new[] { request });
controller.Delegate = this;
controller.PresentationContextProvider = this;
controller.PerformRequests();
return _tcs.Task;
}
[Export("authorizationController:didCompleteWithAuthorization:")]
public void DidComplete(ASAuthorizationController c, ASAuthorization auth)
{
var cred = (ASAuthorizationAppleIdCredential)auth.GetCredential();
_tcs?.TrySetResult(new AppleCredential(
UserId: cred.User,
IdentityToken: cred.IdentityToken?.ToString(NSStringEncoding.UTF8) ?? "",
AuthorizationCode: cred.AuthorizationCode?.ToString(NSStringEncoding.UTF8) ?? "",
Email: cred.Email,
FullName: cred.FullName?.GivenName));
}
}
The nonce parameter is critical. Generate a cryptographically random string, hash it with SHA-256, send the hash to Apple, and later verify the un-hashed value against your backend's returned ID token. This prevents replay attacks and is one of the checks Apple's server-side validation performs.
Web-based Sign in with Apple on Android
On Android use the Sign in with Apple JS web flow through WebAuthenticator:
var authUrl = new Uri(
"https://appleid.apple.com/auth/authorize" +
"?client_id=com.acme.checkout.web" +
"&redirect_uri=https%3A%2F%2Fapi.acme.com%2Fauth%2Fapple%2Fcallback" +
"&response_type=code%20id_token" +
"&scope=name%20email" +
"&response_mode=form_post" +
"&state=" + Uri.EscapeDataString(state) +
"&nonce=" + Uri.EscapeDataString(nonce));
var result = await WebAuthenticator.Default.AuthenticateAsync(new WebAuthenticatorOptions
{
Url = authUrl,
CallbackUrl = new Uri("myapp://auth/apple"),
PrefersEphemeralWebBrowserSession = true
});
var idToken = result.Properties["id_token"];
Note the client_id for Android is your Services ID (not your bundle ID), and Apple posts the response to your server, which then redirects to your app scheme with the ID token. That backend hop is unavoidable because Apple doesn't support custom URL schemes as redirect URIs.
How do I add Google Sign-In to .NET MAUI 10?
Google's story fragmented in 2024 when they deprecated the standalone GoogleSignIn Android SDK in favor of Credential Manager, which unifies passwords, passkeys, and federated sign-in behind one bottom sheet. As of 2026, the recommended approach on Android is Credential Manager via a binding library, and on iOS the plain WebAuthenticator flow works fine for most apps that don't need iCloud Keychain passkey integration.
Cross-platform Google OAuth with WebAuthenticator
The pragmatic option is to skip Credential Manager and use the standard OAuth authorization code flow with PKCE on both platforms. It's a bit less "native" but avoids two separate integrations. Register an OAuth 2.0 Client ID in Google Cloud Console: one iOS entry, one Android entry (with SHA-1 of your signing cert), and one Web entry.
public async Task<GoogleTokens> SignInWithGoogleAsync()
{
var verifier = PkceHelper.GenerateCodeVerifier();
var challenge = PkceHelper.Sha256Base64Url(verifier);
var state = Guid.NewGuid().ToString("N");
var authUrl = new Uri(
"https://accounts.google.com/o/oauth2/v2/auth" +
$"?client_id={_iosClientId}" + // pick per-platform
"&redirect_uri=" + Uri.EscapeDataString("com.googleusercontent.apps.1234567890:/oauth2redirect") +
"&response_type=code" +
"&scope=" + Uri.EscapeDataString("openid email profile") +
"&code_challenge=" + challenge +
"&code_challenge_method=S256" +
"&state=" + state);
var result = await WebAuthenticator.Default.AuthenticateAsync(new WebAuthenticatorOptions
{
Url = authUrl,
CallbackUrl = new Uri("com.googleusercontent.apps.1234567890:/oauth2redirect"),
PrefersEphemeralWebBrowserSession = false // let users stay signed in across sessions
});
if (result.Properties["state"] != state)
throw new SecurityException("OAuth state mismatch");
var code = result.Properties["code"];
return await ExchangeCodeAsync(code, verifier);
}
private async Task<GoogleTokens> ExchangeCodeAsync(string code, string verifier)
{
using var http = new HttpClient();
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["client_id"] = _iosClientId,
["code"] = code,
["code_verifier"] = verifier,
["grant_type"] = "authorization_code",
["redirect_uri"] = "com.googleusercontent.apps.1234567890:/oauth2redirect"
});
var resp = await http.PostAsync("https://oauth2.googleapis.com/token", form);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadFromJsonAsync<GoogleTokens>()
?? throw new AuthenticationException("Empty token response");
}
The reversed-domain redirect URI (com.googleusercontent.apps.NNNN:/oauth2redirect) is what Google issues automatically for iOS clients. Register it as a URL scheme in Info.plist and as an intent filter on Android. Also worth knowing: Google is one of very few providers that still exchanges the code without a client secret for mobile clients, a small consolation for a slightly weirder redirect format.
Microsoft Entra ID with MSAL.NET
For Microsoft accounts (both consumer and Entra ID work), skip WebAuthenticator entirely and use MSAL.NET. The library handles PKCE, silent token refresh, and the Microsoft Authenticator broker. That last piece really matters in enterprises with Conditional Access policies. See the MSAL.NET documentation for the full API surface.
using Microsoft.Identity.Client;
public sealed class MicrosoftAuthService
{
private readonly IPublicClientApplication _pca;
private static readonly string[] Scopes = { "User.Read", "openid", "profile", "offline_access" };
public MicrosoftAuthService()
{
var builder = PublicClientApplicationBuilder
.Create("<your-app-registration-client-id>")
.WithAuthority(AzureCloudInstance.AzurePublic, "common")
.WithRedirectUri("msauth.com.acme.checkout://auth");
#if IOS
builder = builder.WithIosKeychainSecurityGroup("com.acme.shared");
#endif
_pca = builder.Build();
}
public async Task<AuthenticationResult> SignInAsync()
{
var accounts = await _pca.GetAccountsAsync();
try
{
return await _pca.AcquireTokenSilent(Scopes, accounts.FirstOrDefault()).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
return await _pca.AcquireTokenInteractive(Scopes)
.WithParentActivityOrWindow(Platform.CurrentActivity) // Android only
.ExecuteAsync();
}
}
}
The msauth.<bundle-id>://auth redirect URI is MSAL's convention; add it to Info.plist and AndroidManifest.xml exactly as above. On Android, MSAL also requires an activity filter for msauth and a signature-hash-based intent filter for the Microsoft Authenticator broker. The MSAL setup guide has the exact XML.
The silent-first pattern (AcquireTokenSilent then fall back to interactive) is what keeps users from re-authenticating on every launch. Under the hood MSAL uses the refresh token to mint a new access token, and if that fails throws MsalUiRequiredException, which you handle by prompting the user again. For deeper token lifecycle patterns, see our securing .NET MAUI apps guide.
How to handle OAuth redirect URIs on iOS and Android
Ninety percent of "authentication cancelled" bugs come down to redirect URI mismatches. The exact string you send in the authorization request must match one of the URIs registered in the provider console and match a URL scheme or intent filter your app declares. There's no error message; the browser just closes and your WebAuthenticator call throws TaskCanceledException. Debugging this by logs alone is miserable (I've done it).
My team keeps a small checklist for each provider:
Register the redirect URI in the provider's console (Google Cloud, Azure AD, Apple Developer).
Add the scheme to Info.plist under CFBundleURLSchemes.
Add the intent filter to AndroidManifest.xml on the WebAuthenticatorCallbackActivity.
Pass the exact same string as redirect_uri in the authorization request AND as CallbackUrl in WebAuthenticatorOptions.
Test cold-start (kill the app, click sign-in, complete the flow, and confirm the app resumes).
Related reading: our deep linking guide for .NET MAUI 10 covers the intent filter and Info.plist patterns in more depth, since OAuth callbacks are functionally a special case of deep links.
Storing tokens securely and handling refresh
Access tokens are typically short-lived (5–60 minutes); refresh tokens last days to weeks. Neither belongs in Preferences. On Android that's plain SharedPreferences, and on iOS it's NSUserDefaults. Both are readable by any app with device access or by anyone who lifts a device backup. Use SecureStorage, which wraps Keychain on iOS and EncryptedSharedPreferences on Android.
public sealed class SecureStorageTokenStore : ITokenStore
{
public Task SaveAsync(string provider, TokenBundle tokens)
{
var json = JsonSerializer.Serialize(tokens);
return SecureStorage.Default.SetAsync($"tokens:{provider}", json);
}
public async Task<TokenBundle?> LoadAsync(string provider)
{
var json = await SecureStorage.Default.GetAsync($"tokens:{provider}");
return json is null ? null : JsonSerializer.Deserialize<TokenBundle>(json);
}
public void Clear(string provider) => SecureStorage.Default.Remove($"tokens:{provider}");
}
public record TokenBundle(
string AccessToken,
string? RefreshToken,
string IdToken,
DateTimeOffset AccessTokenExpiresAt);
Wrap HttpClient with a DelegatingHandler that checks expiry and refreshes on 401s. For teams already using Refit or Polly-based resilience, this fits neatly into an AuthenticationHandler:
public sealed class AuthHandler : DelegatingHandler
{
private readonly ITokenStore _store;
private readonly IAuthService _auth;
private static readonly SemaphoreSlim _refreshLock = new(1, 1);
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage req, CancellationToken ct)
{
var bundle = await _store.LoadAsync("primary");
if (bundle is null) return await base.SendAsync(req, ct);
if (bundle.AccessTokenExpiresAt < DateTimeOffset.UtcNow.AddMinutes(1))
bundle = await RefreshAsync(bundle);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bundle.AccessToken);
var resp = await base.SendAsync(req, ct);
if (resp.StatusCode == HttpStatusCode.Unauthorized)
{
bundle = await RefreshAsync(bundle);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bundle.AccessToken);
resp = await base.SendAsync(req, ct);
}
return resp;
}
}
The SemaphoreSlim prevents refresh stampedes when several parallel requests get 401 at the same time — a real problem the first time we shipped this.
Server-side ID token validation
Never trust an ID token that came out of the OAuth flow directly. Validate it on your backend. Each provider ships a JWKS endpoint you can use to verify the signature, and each has provider-specific claims worth checking:
On the .NET backend, Microsoft.IdentityModel.Tokens plus System.IdentityModel.Tokens.Jwt handles the signature verification. Cache the JWKS response for at most an hour and honor the Cache-Control header; providers rotate keys.
App Store and Play Store review checklist
Sign-in flows are one of the highest-touch surfaces for store reviewers, so a few pre-submission checks save a rejection cycle.
iOS App Store checks
Guideline 4.8: If you offer Google, Facebook, Microsoft, or any third-party login, you must also offer Sign in with Apple, and it must be visually equivalent (same prominence, same position group). Reviewers screenshot this.
Data collection: Declare authentication data types in your PrivacyInfo.xcprivacy manifest and in the App Store Connect data-collection questionnaire.
Delete account flow: Guideline 5.1.1(v) requires apps with account creation to offer in-app account deletion. Social sign-in counts as account creation.
Test with a demo account: Provide reviewers with a working test account in App Review Information, including which social provider it authenticates against.
Google Play checks
Data Safety declaration: List each token type stored and whether it's encrypted at rest and in transit.
Sensitive permissions: If you request GET_ACCOUNTS for legacy Google Sign-In, expect additional review; Credential Manager avoids this.
Deep link verification: If you use App Links (https redirects), Play Console verifies the Digital Asset Links file at review time.
Frequently Asked Questions
Do I need Sign in with Apple if I use Google Sign-In in my MAUI app?
Yes, on iOS. App Store Review Guideline 4.8 requires that any app offering third-party or social login (Google, Facebook, Microsoft, etc.) also offer Sign in with Apple with equivalent visual prominence. On Android there is no such requirement, so you can offer Google-only there. If you ship a build to the App Store without Sign in with Apple alongside Google or Microsoft login, expect a rejection.
What is WebAuthenticator in .NET MAUI 10?
WebAuthenticator is the .NET MAUI cross-platform API that opens a system browser (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android), lets the user complete an OAuth flow, and returns query-string parameters back to your app via a custom URL scheme callback. It's the recommended way to implement OAuth for any provider that doesn't ship a dedicated .NET SDK.
Can I use MSAL.NET for Google or Apple sign-in?
No. MSAL.NET is specifically for Microsoft identity providers (Entra ID, Microsoft consumer accounts, Azure AD B2C). For Google and Apple, use WebAuthenticator or provider-specific SDKs. Some teams standardize on WebAuthenticator for all three providers to avoid the extra dependency, but you lose the Microsoft Authenticator broker integration MSAL provides for enterprise scenarios.
How do I refresh an OAuth access token in .NET MAUI?
Store the refresh token in SecureStorage after the initial sign-in, then POST to the provider's token endpoint with grant_type=refresh_token when your access token expires. Wrap this in a DelegatingHandler so refresh happens transparently before requests and on 401 responses. Use a SemaphoreSlim to prevent multiple concurrent requests from all trying to refresh at once.
Why does my OAuth callback fail with "authentication cancelled"?
The most common cause is a redirect URI mismatch. Verify that the exact string you send as redirect_uri in the authorization request matches one registered in the provider's console AND matches a URL scheme in your Info.plist (iOS) plus an intent filter in AndroidManifest.xml (Android). A second common cause on iOS is missing PrefersEphemeralWebBrowserSession tuning when Safari is set as the system browser.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.
A working developer guide to CommunityToolkit MediaElement in .NET MAUI 10: install, XAML setup, HLS/DASH streaming, background audio, PiP, and fixes for the errors you will actually hit.
Add passkeys to .NET MAUI 10 using iOS AuthenticationServices and Android Credential Manager. Working C# handlers, association files, and a Fido2NetLib backend.
Wire in-app updates into .NET MAUI 10 with the Google Play In-App Update API on Android and iTunes Lookup on iOS. Includes flexible vs immediate flows, force-update via Firebase Remote Config, and Play internal-sharing tests.