-
Notifications
You must be signed in to change notification settings - Fork 2k
[Windows] WebAuthenticator: add OAuth support via app activation #34887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
3d29a2e
c387415
e6f9f98
271e3ab
a9f9e01
56847bd
aa50699
34217e7
9dcdd3c
f41f0f3
d94e1c7
eb9a840
06d61d2
1b66e12
0cb668a
00275e4
a6966a1
86c21b1
13372a7
dc62109
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,18 +1,115 @@ | ||||||
| #nullable enable | ||||||
| using System; | ||||||
| using System.Linq; | ||||||
| using System.Threading; | ||||||
| using System.Threading.Tasks; | ||||||
| using System.Xml; | ||||||
| using System.Xml.Linq; | ||||||
| using System.Xml.XPath; | ||||||
| using Microsoft.Maui.ApplicationModel; | ||||||
| using Microsoft.Maui.Storage; | ||||||
| using Microsoft.Security.Authentication.OAuth; | ||||||
| using Microsoft.Windows.AppLifecycle; | ||||||
| using Windows.ApplicationModel.Activation; | ||||||
|
|
||||||
| namespace Microsoft.Maui.Authentication | ||||||
| { | ||||||
| partial class WebAuthenticatorImplementation : IWebAuthenticator | ||||||
| { | ||||||
| public Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions) | ||||||
| internal static bool OnAppActivation(UI.Xaml.Application application, AppActivationArguments args) | ||||||
| { | ||||||
| throw new PlatformNotSupportedException("This implementation of WebAuthenticator does not support Windows. See https://github.com/microsoft/WindowsAppSDK/issues/441 for more details."); | ||||||
| if (args is null || args.Kind != ExtendedActivationKind.Protocol) | ||||||
| return false; | ||||||
|
|
||||||
| if (args.Data is not IProtocolActivatedEventArgs protocolArgs) | ||||||
| return false; | ||||||
|
|
||||||
| if (!OAuth2Manager.CompleteAuthRequest(protocolArgs.Uri)) | ||||||
| return false; | ||||||
|
|
||||||
| // When the protocol callback launches a transient helper instance, complete the auth request | ||||||
| // and immediately exit before the app finishes booting into a headless background process. | ||||||
| if (WindowStateManager.Default.GetActiveWindow() is null) | ||||||
| System.Diagnostics.Process.GetCurrentProcess().Kill(); | ||||||
|
|
||||||
| return true; | ||||||
| } | ||||||
| public Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions, CancellationToken cancellationToken) | ||||||
|
|
||||||
| public async Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions) | ||||||
| => await AuthenticateAsync(webAuthenticatorOptions, CancellationToken.None).ConfigureAwait(false); | ||||||
|
|
||||||
| public async Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions, CancellationToken cancellationToken) | ||||||
| { | ||||||
| throw new PlatformNotSupportedException("This implementation of WebAuthenticator does not support Windows. See https://github.com/microsoft/WindowsAppSDK/issues/441 for more details."); | ||||||
| cancellationToken.ThrowIfCancellationRequested(); | ||||||
|
|
||||||
| ArgumentNullException.ThrowIfNull(webAuthenticatorOptions); | ||||||
|
|
||||||
| var url = webAuthenticatorOptions.Url ?? throw new ArgumentNullException(nameof(webAuthenticatorOptions.Url)); | ||||||
| var callbackUrl = webAuthenticatorOptions.CallbackUrl ?? throw new ArgumentNullException(nameof(webAuthenticatorOptions.CallbackUrl)); | ||||||
|
|
||||||
| bool isPackaged = AppInfoUtils.IsPackagedApp; | ||||||
|
|
||||||
| if (isPackaged) | ||||||
| { | ||||||
| if (!IsUriProtocolDeclared(callbackUrl.Scheme)) | ||||||
| throw new InvalidOperationException($"You need to declare the windows.protocol usage of the protocol/scheme `{callbackUrl.Scheme}` in your AppxManifest.xml file"); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| if (callbackUrl.Scheme == "http" || callbackUrl.Scheme == "https") | ||||||
| throw new InvalidOperationException($"{callbackUrl.Scheme}:// schemes are not allowed for callbackUri. Use a custom scheme like 'myapp' instead."); | ||||||
|
|
||||||
| var value = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(callbackUrl.Scheme); | ||||||
| if (value is null || value.GetValue("URL Protocol") is null) | ||||||
| { | ||||||
| throw new InvalidOperationException($"The URI Scheme '{callbackUrl.Scheme}' is not registered. Call ActivationRegistrationManager.RegisterForProtocolActivation to register protocol activation."); | ||||||
| } | ||||||
| } | ||||||
| AuthRequestParams authRequestParams = AuthRequestParams.CreateForAuthorizationCodeRequest("", callbackUrl); | ||||||
|
|
||||||
| var windowId = WindowStateManager.Default.GetActiveAppWindow(false)?.Id; | ||||||
| if (!windowId.HasValue) | ||||||
| throw new InvalidOperationException("No active window found for authentication."); | ||||||
|
|
||||||
| AuthRequestResult authRequestResult = await OAuth2Manager | ||||||
| .RequestAuthWithParamsAsync(windowId.Value, url, authRequestParams) | ||||||
| .AsTask(cancellationToken) | ||||||
| .ConfigureAwait(false); | ||||||
| if (authRequestResult.Failure is not null) | ||||||
| { | ||||||
| var message = string.IsNullOrEmpty(authRequestResult.Failure.ErrorDescription) | ||||||
| ? authRequestResult.Failure.Error | ||||||
| : $"{authRequestResult.Failure.Error}: {authRequestResult.Failure.ErrorDescription}"; | ||||||
|
|
||||||
| if (IsUserCancellation(authRequestResult.Failure.Error, authRequestResult.Failure.ErrorDescription)) | ||||||
| throw new TaskCanceledException(message); | ||||||
|
|
||||||
| throw new InvalidOperationException(message); | ||||||
| } | ||||||
|
|
||||||
| return new WebAuthenticatorResult(authRequestResult.ResponseUri, webAuthenticatorOptions.ResponseDecoder); | ||||||
| } | ||||||
|
|
||||||
| static bool IsUserCancellation(string? error, string? errorDescription) => | ||||||
| string.Equals(error, "access_denied", StringComparison.OrdinalIgnoreCase) || | ||||||
| (error?.IndexOf("cancel", StringComparison.OrdinalIgnoreCase) >= 0) || | ||||||
| (errorDescription?.IndexOf("cancel", StringComparison.OrdinalIgnoreCase) >= 0); | ||||||
|
|
||||||
| static bool IsUriProtocolDeclared(string scheme) | ||||||
| { | ||||||
| var docPath = FileSystemUtils.PlatformGetFullAppPackageFilePath(PlatformUtils.AppManifestFilename); | ||||||
| var doc = XDocument.Load(docPath, LoadOptions.None); | ||||||
| var reader = doc.CreateReader(); | ||||||
|
||||||
| var reader = doc.CreateReader(); | |
| using var reader = doc.CreateReader(); |
Copilot
AI
Apr 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IsUriProtocolDeclared hard-codes the UAP XML namespace string even though PlatformUtils.AppManifestUapXmlns already defines it (and is used elsewhere, e.g., Permissions). Using the shared constant would reduce duplication and avoid drift if the namespace ever changes.
| namespaceManager.AddNamespace("uap", "http://schemas.microsoft.com/appx/manifest/uap/windows10"); | |
| namespaceManager.AddNamespace("uap", PlatformUtils.AppManifestUapXmlns); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registry.ClassesRoot.OpenSubKey(...)returns aRegistryKeythat should be disposed/closed. Consider usingusing var value = ...(or a try/finally) before readingURL Protocol, to avoid leaking registry handles in unpackaged scenarios.