Skip to content

Passkeys (WebAuthn/FIDO2) Essentials API — spec + implementation - #36617

Closed
mattleibow wants to merge 50 commits into
net11.0from
mattleibow-spec-passkeys-essentials
Closed

Passkeys (WebAuthn/FIDO2) Essentials API — spec + implementation#36617
mattleibow wants to merge 50 commits into
net11.0from
mattleibow-spec-passkeys-essentials

Conversation

@mattleibow

@mattleibow mattleibow commented Jul 16, 2026

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

Adds a cross-platform Passkeys (WebAuthn / FIDO2 public-key credentials) Essentials API — the design spec, the implementation across all platforms, a runnable sample, and a reference relying-party server.

  • Spec: docs/specs/Passkeys.md
  • Code: src/Essentials/src/Passkeys/, namespace Microsoft.Maui.Authentication, in Microsoft.Maui.Essentials.

The API brokers the standard WebAuthn JSON between the app's relying-party (RP) server and the OS authenticator (Face ID / Touch ID / Windows Hello / Android biometric). It does not do server-side verification, challenge generation, or attestation validation — that stays on the RP server.

Note

This PR is stacked on the Blazor Identity template baseline (#36768) — the reference server's non-passkey code is that stock template. The base is currently set to net11.0 so CI runs, which means the GitHub "Files changed" tab temporarily includes the baseline template files. Once #36768 merges into net11.0, this diff collapses back to just the passkey-specific changes.

👉 To review only what this PR actually changes (passkey code, on top of the stock template), use this compare:
mattleibow-blazor-identity-template-baseline...mattleibow-spec-passkeys-essentials

Public API (Microsoft.Maui.Authentication)

namespace Microsoft.Maui.Authentication;

// Static entry point for passkeys, mirroring WebAuthenticator.
public static class Passkeys
{
    // True when this OS/device can create and use passkeys.
    public static bool IsSupported { get; }
    // The platform implementation the static methods delegate to.
    public static IPasskeys Default { get; }
    // Register a passkey from the RP server's WebAuthn creation-options JSON.
    public static Task<PasskeyCreationResponse> CreateAsync(string creationOptionsJson, CancellationToken cancellationToken = default);
    // Register a passkey from a parsed options object.
    public static Task<PasskeyCreationResponse> CreateAsync(PasskeyCreationOptions options, CancellationToken cancellationToken = default);
    // Sign in with a passkey from the RP server's WebAuthn request-options JSON.
    public static Task<PasskeyAssertionResponse> AssertAsync(string requestOptionsJson, CancellationToken cancellationToken = default);
    // Sign in with a passkey from a parsed options object.
    public static Task<PasskeyAssertionResponse> AssertAsync(PasskeyRequestOptions options, CancellationToken cancellationToken = default);
}

// Platform abstraction behind Passkeys.Default (injectable/mockable).
public interface IPasskeys
{
    // True when this OS/device can create and use passkeys.
    bool IsSupported { get; }
    // Register a new passkey with the OS authenticator.
    Task<PasskeyCreationResponse> CreateAsync(PasskeyCreationOptions options, CancellationToken cancellationToken = default);
    // Authenticate with an existing passkey.
    Task<PasskeyAssertionResponse> AssertAsync(PasskeyRequestOptions options, CancellationToken cancellationToken = default);
}

// Wraps the RP server's WebAuthn creation-options JSON for CreateAsync.
public sealed class PasskeyCreationOptions
{
    // Build from the server's creation-options JSON.
    public PasskeyCreationOptions(string creationOptionsJson);
    // Prefer credentials already on the device (skip the large picker) where supported.
    public bool PreferImmediatelyAvailable { get; set; }
    // Returns the underlying creation-options JSON.
    public override string ToString();
}

// Wraps the RP server's WebAuthn request-options JSON for AssertAsync.
public sealed class PasskeyRequestOptions
{
    // Build from the server's request-options JSON.
    public PasskeyRequestOptions(string requestOptionsJson);
    // Prefer credentials already on the device (skip the large picker) where supported.
    public bool PreferImmediatelyAvailable { get; set; }
    // Returns the underlying request-options JSON.
    public override string ToString();
}

// Result of a successful CreateAsync (post back to the RP server to finish registration).
public sealed class PasskeyCreationResponse
{
    // Base64Url credential id of the new passkey.
    public string Id { get; }
    // Returns the full WebAuthn attestation response JSON.
    public override string ToString();
}

// Result of a successful AssertAsync (post back to the RP server to finish sign-in).
public sealed class PasskeyAssertionResponse
{
    // Base64Url credential id of the passkey that was used.
    public string Id { get; }
    // Base64Url user handle the passkey is bound to, if the authenticator returned one.
    public string? UserHandle { get; }
    // Returns the full WebAuthn assertion response JSON.
    public override string ToString();
}

// Thrown when a passkey operation fails, is cancelled, or is unsupported.
public class PasskeyException : Exception
{
    // Create with a message.
    public PasskeyException(string message);
    // Create with a message and an inner exception.
    public PasskeyException(string message, Exception? innerException);
}

PublicAPI.Unshipped.txt is updated for all 7 TFM folders (net, netstandard, net-android, net-ios, net-maccatalyst, net-windows, net-tizen).

Platform status

Platform Status
Android (API 34+) ✅ Implemented — Jetpack Credential Manager (androidx.credentials), callback API bridged to a Task; PreferImmediatelyAvailable, cancellation, Activity, error mapping. Exercised end-to-end on-device (register, list, name, delete, password + passkey sign-in against the reference server).
iOS / iPadOS / Mac Catalyst (iOS 16+) ✅ Implemented — AuthenticationServices (ASAuthorizationPlatformPublicKeyCredentialProvider + ASAuthorizationController). Builds clean.
Windows (Windows 11) ✅ Implemented — Win32 webauthn.dll P/Invoke (WebAuthNAuthenticatorMakeCredential/GetAssertion), GUID-based cancellation. Compiles cleanly; on-device behavior still to be verified on Windows 11 hardware.
netstandard / tvOS / Tizen ✅ Not-supported stub (IsSupported == false).
Standalone macOS Not enabled — Essentials has no net-macos target today; enabling it is an assembly-wide change out of scope here.

Tests

  • src/Essentials/test/UnitTests/Passkeys_Tests.cs — 16 unit tests covering the options/response JSON round-tripping and the not-supported stub behaviour.

Sample & reference server

A full, runnable demo lives under src/Essentials/samples/:

  • MAUI sample — a "Passkeys" page (View/PasskeysPage.xaml + ViewModel/PasskeysViewModel.cs) that behaves like a real login screen: create an account / sign in with a password, get offered a passkey, name it on creation, list your passkeys, remove one, and sign in username-less with a passkey. The server URL is editable from a toolbar button.
  • Reference relying-party serverSamples.WebServer, an ASP.NET Core Identity app that does the WebAuthn server half. Native-app-facing JSON endpoints in Components/Account/PasskeyApiEndpoints.cs:
    • POST /passkeys/register/begin · POST /passkeys/register/finish?name= (enroll a passkey for the signed-in user)
    • POST /passkeys/login/begin · POST /passkeys/login/finish (username-less assertion)
    • GET /passkeys/list · DELETE /passkeys/delete?credentialId= (manage passkeys — [Authorize]-guarded; the cookie challenge is translated to a clean 401 for the native client)
    • plus MapIdentityApi under /account (register/login/refresh) and /account/logout.
    • Platform domain-association documents (WellKnownEndpoints.cs): /.well-known/assetlinks.json (Android) and /.well-known/apple-app-site-association (Apple AASA), and IdentityPasskeyOptions.ValidateOrigin accepting the platform native origins.
  • Configure.ps1 provisions a dev tunnel and writes the server user-secrets (RP domain, allowed origins, and the Android debug-key SHA-256 fingerprint + apk-key-hash origin). Docs: Samples.WebServer/README.md and samples/README.md.

⚠️ Before merge

Still to do (follow-ups)

  • Windows on-device verification on Windows 11 hardware.

Stacked follow-up

Related

Targets the net11.0 feature branch (this PR adds public API).

Copilot AI review requested due to automatic review settings July 16, 2026 16:23
@mattleibow
mattleibow temporarily deployed to copilot-pat-pool July 16, 2026 16:23 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36617

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36617"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@mattleibow
mattleibow temporarily deployed to copilot-pat-pool July 16, 2026 16:23 — with GitHub Actions Inactive
@mattleibow
mattleibow temporarily deployed to copilot-pat-pool July 16, 2026 16:24 — with GitHub Actions Inactive
@mattleibow
mattleibow temporarily deployed to copilot-pat-pool July 16, 2026 16:26 — with GitHub Actions Inactive
@mattleibow
mattleibow temporarily deployed to copilot-pat-pool July 16, 2026 16:27 — with GitHub Actions Inactive
@github-actions github-actions Bot added the area-essentials Essentials: Device, Display, Connectivity, Secure Storage, Sensors, App Info label Jul 16, 2026
@mattleibow
mattleibow temporarily deployed to copilot-pat-pool July 16, 2026 16:27 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces a spec-first (docs-only) proposal for adding a new cross-platform Passkeys (WebAuthn/FIDO2) API to MAUI Essentials under Microsoft.Maui.Authentication, focusing on a small JSON-in/JSON-out contract that brokers WebAuthn options/responses between an app’s RP server and native platform authenticators.

Changes:

  • Adds a new specification document describing the proposed public API surface (Passkeys, IPasskeys, options/response types) and conventions (Default/SetDefault, IsSupported).
  • Documents per-platform implementation strategy for Android Credential Manager, Apple AuthenticationServices, and Windows webauthn.dll, including capability/version gating.
  • Captures error handling, packaging/dependency impact, security considerations, and testing strategy, plus a list of open questions.

Comment thread docs/specs/Passkeys.md
Comment thread docs/specs/Passkeys.md Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 16:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread docs/specs/Passkeys.md
Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 16:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.

Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Comment thread docs/specs/Passkeys.md Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread docs/specs/Passkeys.md
Comment thread docs/specs/Passkeys.md
Comment thread docs/specs/Passkeys.md Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 17:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread docs/specs/Passkeys.md
Comment thread docs/specs/Passkeys.md Outdated
@mattleibow
mattleibow force-pushed the mattleibow-spec-passkeys-essentials branch from e0372ba to 54b1172 Compare July 16, 2026 18:14
Copilot AI review requested due to automatic review settings July 16, 2026 18:14
@mattleibow
mattleibow changed the base branch from main to net11.0 July 16, 2026 18:14
mattleibow and others added 3 commits July 24, 2026 21:27
Rework the Essentials Passkeys sample into a single, state-driven page that
mirrors a real app's account flow instead of three always-visible step lists.

Server (Samples.WebServer):
- Add GET /passkeys/list returning { username, passkeyCount } for the
  cookie-authenticated user so a native client can, after signing in, detect
  whether it already has a passkey (WebAuthn offers no pre-login probe by
  design — that would be account enumeration).
- Guard /passkeys/list and /passkeys/register/begin with declarative
  .RequireAuthorization() instead of hand-rolled GetUserAsync checks.
- Translate the Identity application-cookie challenge into a clean 401 for
  /passkeys/* via ConfigureApplicationCookie, so the native JSON client gets a
  status code instead of a 302 redirect to an HTML login page.

Client (Essentials.Sample):
- Add real view-model state (IsSignedIn / CurrentUsername / HasPasskey /
  PasskeyCount) and drive the whole page off it.
- Sign up now auto-signs-in (mirrors "create account -> logged in"); password
  sign-in and sign-up detect passkeys via /passkeys/list and pop a one-time
  "Set up a passkey?" prompt when the account has none.
- Single page with logged-out (email/password + username-less passkey sign-in)
  and logged-in (account summary + create/sign-out) containers.
- Move the server URL to a "Server" toolbar button (input prompt) and reduce
  the log to a single last-message strip at the bottom.
- Extend BaseViewModel/BasePage with confirm (Yes/No) and text-prompt plumbing
  reusing the existing event pattern.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d
Make the signed-in passkey experience match a real account-management
screen: name a passkey when you create it, see all your passkeys, and
remove one you no longer want.

Server (Samples.WebServer, PasskeyApiEndpoints.cs):
- GET /passkeys/list now also returns a passkeys[] array — each entry has
  the Base64Url credential id, a display name (explicit name, else the
  AAGUID-inferred authenticator, else "Unnamed passkey", via the template's
  PasskeyAuthenticators.GetDisplayName), and createdAt.
- POST /passkeys/register/finish accepts an optional ?name= and sets
  passkey.Name, falling back to the AAGUID-inferred authenticator name when
  omitted (mirrors the browser template's AddPasskey flow).
- Add DELETE /passkeys/delete?credentialId= (RequireAuthorization) backed by
  UserManager.RemovePasskeyAsync so a user can remove their own passkeys.

Client (Essentials.Sample):
- Prompt for a passkey name (defaulting to the device name) when creating,
  and pass it to register/finish.
- Show the user's passkeys in the signed-in view as cards (name + date added)
  each with a Remove button that confirms, calls the delete endpoint, and
  refreshes the list.
- Bump the status strip from 2 to 5 word-wrapped lines so longer server
  messages are readable.
- Alias Microsoft.Maui.Authentication.Passkeys as PasskeysApi to avoid a name
  clash with the new Passkeys collection property.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d
Implement Flow 1 — the officially recommended Backend-for-Frontend pattern
for native external login — alongside the existing password and passkey
sign-in, so the MAUI app and the reference server demonstrate "sign in with
an external account -> local ASP.NET Core Identity account", with the
provider token kept server-side.

Server (Samples.WebServer):
- Register a self-contained "Development Test Login" OAuth provider (generic
  AddOAuth pointed at built-in /dev-oauth/* mock authorize/token/userinfo
  endpoints) so the whole flow works end to end without real Google keys.
  SaveTokens=true keeps the provider token on the server. Swap for AddGoogle
  with real credentials later; the Identity + native plumbing is unchanged.
- ExternalAuthEndpoints.cs: the mock provider, the native BFF handshake
  (/native-auth/external/start -> /complete -> one-time code -> /exchange for a
  cookie session), and GET /me/external which uses the SERVER-stored provider
  token to fetch and relay the external profile (the "do something" step).
  The browser-facing authorize endpoint uses the public host; the token and
  userinfo backchannels use localhost so they don't depend on tunnel DNS.
- Extend the cookie->401 translation to /me so native calls get a clean 401.

Client (Essentials.Sample):
- "Sign in with an external account (OAuth)" drives the flow via
  WebAuthenticator: the server brokers the OAuth exchange and returns a
  one-time code to the custom scheme; the app exchanges it over its own
  HttpClient so the Identity cookie lands in its CookieContainer. The result
  is the same account as password/passkey, so passkeys can then be added.
- "Show my external profile (server relay)" calls /me/external and shows the
  server-relayed profile, demonstrating that the client never holds the
  provider token.

Verified end to end on Android: external sign-in creates a local account,
the post-login passkey offer + enrollment work on that account, and the
profile relay returns the provider data.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 101 out of 159 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (4)

src/Essentials/src/Passkeys/Passkeys.android.cs:39

  • CreateAsync null-forgives RegistrationResponseJson. If the credential provider returns a response object but the JSON payload is null/empty, this will throw later with a less actionable exception. Validate the JSON and throw a PasskeyException with a clear message.
    src/Essentials/src/Passkeys/Passkeys.android.cs:68
  • AssertAsync null-forgives AuthenticationResponseJson. If the provider returns a PublicKeyCredential but omits the JSON payload, the code will throw with a null argument. Validate the JSON and throw a PasskeyException with a clearer error.
    src/Essentials/src/Passkeys/Passkeys.android.cs:116
  • MapException duplicates the same "Cancellation" string check logic that's already factored into IsCancellation. Consolidating this reduces duplication and keeps cancellation detection consistent between sync and callback error paths.
    NuGet.config:34
  • NuGet.config adds nuget.org as a package source. This bypasses the repo's controlled feeds and can break CI restores in restricted-network environments; it also increases supply-chain risk. This source should be removed before merge once Xamarin.AndroidX.Credentials is mirrored into the dotnet-public feed.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

Comment on lines +44 to +47
public Task<string> DisplayPromptAsync(string title, string message, string initialValue)
{
return DoDisplayPrompt?.Invoke(title, message, initialValue) ?? Task.FromResult<string>(null);
}
Rework the external-login sample to use only real ASP.NET Core providers and a
fully provider-agnostic flow, replacing the temporary "Development Test Login"
mock. No hand-rolled auth: the framework's OAuth handlers, SignInManager, and
Data Protection do the security-relevant work.

Server (Samples.WebServer):
- Remove the mock /dev-oauth/* OAuth provider and its in-memory token stores.
- ExternalProviders.cs: register Google, Microsoft, Facebook, and Apple from
  configuration (Authentication section), each only when its credentials are
  present — so the sample runs with none, some, or all. Add the matching
  provider packages (Microsoft.AspNetCore.Authentication.Google/MicrosoftAccount/
  Facebook + AspNet.Security.OAuth.Apple), mirroring the Web Authenticator sample.
- Add GET /native-auth/external/providers so clients discover the configured
  providers instead of hard-coding them.
- Replace the in-memory one-time code with an ITimeLimitedDataProtector token
  (framework-signed + expiring) — no custom code store.
- /me/external relay is now generic (Google/Microsoft Graph/Facebook userinfo;
  providers without a userinfo endpoint fall back to the linked local account).
- appsettings.json documents the Authentication provider config.

Client (Essentials.Sample):
- Discover providers from the server and render one "Continue with <provider>"
  button per configured provider (BindableLayout) — no provider is hard-coded.
  ExternalSignInAsync takes the provider name; providers reload when the server
  URL changes. Shows a hint when none are configured.

Verified: with no credentials the app shows the "no providers configured" hint
and discovery returns []; configuring providers makes them appear automatically
in both the web login page and the native app (dynamic buttons).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 101 out of 160 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (5)

NuGet.config:34

  • NuGet.config adds a nuget.org package source. This is explicitly marked “DO NOT MERGE” and can also break builds that rely on the repo’s isolated/approved feeds. Please remove this source before merging (mirror Xamarin.AndroidX.Credentials into the dotnet-public feed instead).
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/samples/Samples/ViewModel/BaseViewModel.cs:47

  • DisplayPromptAsync returns a Task but falls back to Task.FromResult(null) when the handler isn’t wired up. That makes the “no UI binding” path return null unexpectedly and can cause null-reference bugs in callers that treat the result as a usable string.
    src/Essentials/samples/Samples.WebServer/Program.cs:107
  • ForwardedHeadersOptions clears KnownIPNetworks/KnownProxies, which makes the server trust forwarded headers from any source. Even for a sample, that’s a security footgun (host/proto spoofing) if someone runs it outside a dev-tunnel scenario. Consider scoping this to Development only, or reading an explicit allow-list from configuration.
    src/Essentials/src/Passkeys/Passkeys.android.cs:83
  • InvokeAsync uses TaskCompletionSource without RunContinuationsAsynchronously. Since the TCS is completed from Java callbacks, continuations may run inline on the callback thread, which can lead to reentrancy or unexpected blocking. Using RunContinuationsAsynchronously is the safer pattern for bridging callbacks to Tasks.
    src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:36
  • requestCredential interpolates the email value directly into the query string. If the form field is empty, FormData.get(...) yields null and the URL becomes username=null; and if it contains characters like +, &, or ?, the query will be malformed. Encode the value and treat null/empty as “no username” so the server can do discoverable (username-less) requests.

…nfig

- Rename the per-provider button text from "Continue with X" to "Sign in with X"
  so the sample shows one clear "Sign in with Google" / "Sign in with Microsoft"
  / "Sign in with Facebook" button per registered provider.
- Only register the Apple provider when a private key path is also configured.
  Apple derives its client secret from the private key, so a partial Apple config
  (ClientId but no key) previously failed options validation on every request,
  breaking authentication for all providers. Requiring the key avoids that.

Verified on Android with three providers configured: the app renders three
distinct "Sign in with …" buttons, one per provider.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 101 out of 160 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (5)

src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:36

  • requestCredential interpolates email directly into the query string (...username=${email}), which will break for common values like user+tag@example.com and can produce an invalid URL. Encode the value (and coerce to string) before building the URL.
    src/Essentials/samples/Samples.WebServer/Program.cs:106
  • ForwardedHeadersOptions is configured to accept forwarded headers from any source by clearing KnownIPNetworks and KnownProxies. That makes it easy for a direct client to spoof scheme/host via X-Forwarded-* headers (host header injection), which is risky even for a sample since it can affect origin validation and redirects.
    src/Essentials/src/Passkeys/Passkeys.android.cs:80
  • TaskCompletionSource is created without RunContinuationsAsynchronously, so continuations can run inline on the Credential Manager callback thread. That can cause surprising re-entrancy and hangs (especially if a continuation blocks).
    src/Essentials/src/Passkeys/Passkeys.android.cs:103
  • The IExecutorService created via Executors.NewSingleThreadExecutor() is only Shutdown()'d; it should also be disposed to avoid leaking the underlying Java resources/thread in long-running apps (multiple passkey operations).
    NuGet.config:34
  • This adds nuget.org as a package source. The repo normally relies on the Azure Artifacts feeds, and leaving nuget.org enabled can bypass feed isolation in CI (and was already called out in the PR description as “DO NOT MERGE”). This needs to be removed before merging, after the required package is mirrored into the internal feed.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

Shorten the passkey and external-auth comments across the sample and reference
server: remove internal shorthand ("Flow 1", "BFF") and narrative about how the
code came to be, and keep only concise notes on what each part does now. No
behavior change. Also reword the one user-facing "(BFF)" hint on the passkeys
page.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 103 out of 160 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (4)

NuGet.config:34

  • The repo-wide NuGet.config adds a nuget.org package source. dotnet/maui CI runs under CFSClean/network isolation and expects dependencies to resolve from the dotnet-public feeds; leaving nuget.org enabled is a merge blocker (even if temporary).
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/samples/Samples.WebServer/Program.cs:98

  • Forwarded headers are accepted from any remote by clearing KnownIPNetworks/KnownProxies, then applied unconditionally. If this sample is ever run outside a trusted dev-tunnel scenario, that enables host/scheme spoofing via X-Forwarded-* headers.
    src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:33
  • The username is interpolated into a query string without URL-encoding. This breaks for valid emails containing '+' and can allow query-string injection (e.g. adding extra parameters).
    src/Essentials/src/Passkeys/Passkeys.android.cs:84
  • InvokeAsync spins up a new single-thread executor per call and doesn’t dispose the Java objects it creates. This is relatively expensive and can leak JNI resources; also, TaskCompletionSource should use RunContinuationsAsynchronously (see other Essentials Android async bridges) to avoid running continuations inline on the callback thread.

The passkeys sample now covers only passwords and passkeys. Server-brokered
external OAuth sign-in (Google/Microsoft/Facebook/Apple via the BFF pattern)
moves to a follow-up PR stacked on this branch so this PR stays focused on the
Passkeys Essentials API.

Removes the two OAuth-only server files (ExternalAuthEndpoints, ExternalProviders)
and the OAuth hunks in Program.cs, appsettings.json, the server csproj, the
sample view model and the sample page.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 102 out of 158 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (4)

NuGet.config:34

  • NuGet.config currently adds a nuget.org package source, which breaks the repo’s mirrored-feed/deterministic restore assumptions and can also fail in CI environments that block nuget.org. This needs to be removed before merge after Xamarin.AndroidX.Credentials is mirrored into the appropriate internal feed.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/src/Passkeys/Passkeys.android.cs:80

  • TaskCompletionSource is created without RunContinuationsAsynchronously. Because the AndroidX callback may complete the TCS on its executor thread, continuations (including the awaiting code) can run inline on that thread, which is a common source of reentrancy/deadlocks. Use TaskCreationOptions.RunContinuationsAsynchronously for safer callback-to-Task bridging.
    src/Essentials/samples/Samples/ViewModel/BaseViewModel.cs:47
  • DisplayPromptAsync falls back to Task.FromResult(null) when no UI handler is wired. Returning null from a method typed as Task can easily lead to NullReferenceExceptions in callers that assume a string. Prefer returning an empty string (or change the method/event contract to allow null explicitly).
    src/Essentials/samples/Samples.WebServer/Program.cs:91
  • ForwardedHeadersOptions clears KnownIPNetworks/KnownProxies, which makes ForwardedHeadersMiddleware trust X-Forwarded-* from any remote client. Even in a sample, this is an easy footgun if someone runs it outside a local dev-tunnel scenario. Safer default is to keep the built-in loopback-only trust unless explicitly configured.

Extends Configure.ps1 to configure iOS / Mac Catalyst passkey testing: with
-AppleTeamId it writes the App Site Association app-id (<TeamID>.<BundleID>) into
the server's user-secrets and adds the webcredentials associated-domains
entitlement to the sample app's iOS Entitlements.plist (a local edit, not
committed).

Turns the samples testing guide into src/Essentials/samples/README.md so it
auto-renders when browsing the folder. It opens with a short orientation (the
sample app, the reference server, Configure.ps1) and then a single testing flow —
Prerequisites, Server (shared), and per-platform Apple / Android / Windows
sections (with the Apple App ID + signing + AASA walkthrough and troubleshooting).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 102 out of 158 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (3)

NuGet.config:34

  • This adds a nuget.org package source. MAUI CI runs under network isolation and restores must come from the repo feeds (dotnet-public), so this source needs to be removed/disabled before merge.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/src/Passkeys/Passkeys.android.cs:83

  • When bridging callback-based APIs to Task, use TaskCreationOptions.RunContinuationsAsynchronously so await continuations don’t run inline on the credential-manager executor thread (this pattern is used elsewhere in Essentials).
    src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:33
  • username is interpolated directly into the query string without URL encoding. If it contains &, ?, or non-ASCII characters the request can break (and it’s generally unsafe to put unencoded user input in URLs).

mattleibow and others added 2 commits July 27, 2026 17:26
The .NET iOS SDK applies Platforms/iOS/Entitlements.plist by convention, so drop
the redundant CodesignEntitlements guidance: simulator and Mac Catalyst need no
signing changes, and only a real iOS device needs Team + provisioning. Add a
troubleshooting note that the web UI must be opened via the tunnel URL, not
localhost (passkeys are bound to the RP ID domain).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d
Mac Catalyst runs as a real signed macOS app, so — like a physical iOS device —
its associated-domains entitlement needs an explicit provisioning profile; only
the iOS Simulator is exempt. Fix the earlier note that grouped Mac Catalyst with
the Simulator, and add an MT7139 troubleshooting row.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 102 out of 158 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (3)

NuGet.config:34

  • nuget.org is added as a package source. This is flagged in-file as temporary and will bypass the repo's feed isolation; it should be removed before merge once Xamarin.AndroidX.Credentials is mirrored into dotnet-public.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/samples/Samples.WebServer/Program.cs:90

  • This config clears KnownIPNetworks/KnownProxies, which makes UseForwardedHeaders() trust forwarded host/scheme from any client. That’s unsafe outside a controlled dev-tunnel scenario; it should be gated (e.g., Development-only and/or only when Passkeys:ServerDomain is set).
    src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:35
  • The username query parameter is built via string interpolation without URL-encoding. Email addresses commonly include + and other reserved characters, which can be mangled in query parsing (e.g., + becoming a space). Encode the username before sending.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 102 out of 158 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (3)

src/Essentials/samples/Samples/ViewModel/BaseViewModel.cs:47

  • DisplayPromptAsync declares a non-nullable Task<string> result but falls back to Task.FromResult<string>(null) when no UI handler is attached. This violates the method contract and can leak nulls into call sites.
    src/Essentials/src/Passkeys/Passkeys.android.cs:83
  • TaskCompletionSource is created without RunContinuationsAsynchronously, so continuations may run inline on the CredentialManager callback/executor thread. Using RunContinuationsAsynchronously avoids reentrancy surprises and is the typical pattern when bridging callback APIs to Task.
    NuGet.config:34
  • NuGet.config adds nuget.org as a package source. This changes restore behavior repo-wide and should not be merged; the referenced package needs to be mirrored into the appropriate internal feed (dotnet-public) and the external source removed.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

…mple

Configure.ps1 -AppleTeamId now also auto-detects the Apple Development signing
identity from the keychain and the installed provisioning profile that matches
the app id + Associated Domains, and writes them to a git-ignored
Samples/Signing.local.props that the sample csproj imports. Mac Catalyst is then
ready to build and run signed with no extra flags (the props also set
MtouchLink=None, since this sample's runtime XAML inflation trips the default
Mac Catalyst linker). -AppleSigningIdentity / -AppleProvisioningProfile override
auto-detection.

Also skip the dev-tunnel sign-in when already authenticated (check
'devtunnel user show' first) so re-runs don't force a re-auth.

README: document the one-stop flow, that an explicit App ID is globally unique to
one team (contributors must use their own bundle id), and per-target build/run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 103 out of 160 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (2)

NuGet.config:34

  • nuget.org is enabled as a package source. This will break repo/CI assumptions (CFSClean/offline restore) and is explicitly called out as “DO NOT MERGE” in the file. Please mirror Xamarin.AndroidX.Credentials into dotnet-public and remove this source before merging.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:36

  • requestCredential interpolates email directly into the query string. When conditional mediation runs and the email field is empty, FormData.get(...) can be null, producing username=null (string) and changing server behavior (it will look up a user named "null"). It should omit the query parameter when no username is provided, and URL-encode the value when present.

Comment on lines +80 to +83
var tcs = new TaskCompletionSource<Java.Lang.Object>();
var signal = new CancellationSignal();
var executor = Executors.NewSingleThreadExecutor()!;
var callback = new CredentialManagerCallback(tcs);
Configure.ps1 no longer edits any committed file. Instead it writes everything
developer-specific into git-ignored files (plus the server user-secrets):

- Samples/Passkeys.Local.props (imported by the app csproj): the default server
  URL — baked into the app via AssemblyMetadata and read by PasskeysViewModel,
  replacing the hard-coded URL that had to be kept out of commits — plus the
  auto-detected Mac Catalyst signing identity + provisioning profile and
  MtouchLink=None.
- Samples/Platforms/iOS/Entitlements.Local.plist: a copy of the committed
  Entitlements.plist plus the webcredentials associated-domains entry, so the
  committed plist is never modified. The props point CodesignEntitlements at it
  for iOS and Mac Catalyst.

A committed Passkeys.Local.in.props template documents the shape for anyone who
prefers to set it up by hand. Both generated files are git-ignored.

Web server config stays in user-secrets (the official, leak-proof mechanism);
trim the // doc-comment keys from appsettings.json to empty safe defaults and
document the keys in the server README instead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 161 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (5)

NuGet.config:34

  • nuget.org is added as a package source, which breaks the repo’s usual isolated/approved feeds and is explicitly marked “DO NOT MERGE”. This needs to be removed before merging (the AndroidX package should be mirrored into the dotnet-public feed instead).
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/samples/Samples/ViewModel/BaseViewModel.cs:47

  • DisplayPromptAsync falls back to returning a Task whose result is null when DoDisplayPrompt is not wired up. That violates the method’s declared non-null return type and can lead to NullReferenceExceptions downstream (and nullable warnings if enabled).
    src/Essentials/samples/Samples.WebServer/Components/Account/Shared/PasskeySubmit.razor.js:34
  • The username/email value is interpolated directly into the query string and can be null/non-string (FormData.get returns FormDataEntryValue). This can produce URLs like “?username=null” or “[object File]”, and it isn’t URL-encoded.
    src/Essentials/src/Passkeys/Passkeys.android.cs:84
  • TaskCompletionSource is created without RunContinuationsAsynchronously. Since completions happen from a Java callback/executor thread, running continuations inline can lead to unexpected reentrancy and thread-affinity issues. Use RunContinuationsAsynchronously to decouple continuations from the callback thread.
    src/Essentials/samples/Samples.WebServer/Program.cs:91
  • Forwarded headers are enabled while clearing KnownProxies/KnownNetworks, which makes the app trust X-Forwarded-* from any client. That’s unsafe outside a controlled dev-tunnel scenario (scheme/host spoofing). Scope this behavior to Development (or gate it behind explicit configuration of trusted proxies).

…emplate-driven props

- Rename -AndroidPackage to -ApplicationId and drop -AppleBundleId: it's the one
  app id shared by every platform (read from <ApplicationId>), used for both the
  Android package and the Apple <TeamID>.<ApplicationId>.
- Fail fast instead of limping on: hard-error when <ApplicationId> can't be read
  from the project (no silent default), and when the dev tunnel URL can't be
  resolved (a public HTTPS domain is required — there is no localhost fallback for
  Android/Apple passkeys), instead of a soft warning that continues.
- Generate Entitlements.Local.plist with pure XmlDocument (cross-platform, no
  PlistBuddy/external process) and a fixed Apple header so the output is byte-clean.
- Make Passkeys.Local.props template-driven: load the committed Passkeys.Local.in.props
  and fill values via XML (strip the Apple-only groups when Apple isn't set up), so
  the .in template is the single source of truth for the file's shape and comments.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 161 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (4)

src/Essentials/samples/Samples/ViewModel/BaseViewModel.cs:47

  • DisplayPromptAsync is declared as Task<string> but the fallback path returns null (Task.FromResult<string>(null)), so callers can receive a null string while the API advertises non-null. This also conflicts with the typical DisplayPromptAsync behavior where cancel returns null. Make the return type nullable (Task<string?>) and update the event signature + fallback accordingly.
    src/Essentials/src/Passkeys/Passkeys.android.cs:80
  • TaskCompletionSource is created without RunContinuationsAsynchronously, so continuations may run inline on the CredentialManager callback/executor thread. This can lead to unexpected re-entrancy and makes cancellation/exception paths harder to reason about. Use TaskCreationOptions.RunContinuationsAsynchronously for the TCS.
    src/Essentials/samples/Samples.WebServer/Program.cs:91
  • The forwarded headers configuration clears KnownIPNetworks/KnownProxies and UseForwardedHeaders() is enabled unconditionally. This makes the app trust X-Forwarded-* from any client by default (host/scheme spoofing), which is risky even for a sample. Consider enabling this only in Development (or behind an explicit config switch) and leaving the default proxy restrictions intact for other environments.
    NuGet.config:34
  • nuget.org is added as a package source. Even though it’s marked temporary, leaving it in the repo config changes restore behavior for all contributors/CI and is called out in the PR description as “DO NOT MERGE”. This needs to be removed before merge once Xamarin.AndroidX.Credentials is mirrored into the official feeds.
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

The committed csproj carried both the AssemblyMetadata plumbing and a placeholder
default, while the git-ignored Passkeys.Local.props overrode it — so the
placeholder lived in two committed places (csproj + PasskeysViewModel). Move the
whole mechanism into the local props / template: the AssemblyMetadata item (with
the URL) now lives in Passkeys.Local.props alongside the value, and the committed
csproj just imports it. When the props is absent (default checkout / CI), no
metadata is emitted and PasskeysViewModel falls back to its built-in placeholder —
now the single committed default.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 161 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (3)

NuGet.config:34

  • Repository-level NuGet.config should not include nuget.org, as it breaks isolated/offline builds and violates the repo’s internal-feed-only assumption. This temporary source needs to be removed before merge (and the package mirrored into dotnet-public instead).
    <!-- TEMPORARY: nuget.org added to restore Xamarin.AndroidX.Credentials for the Passkeys Android
         implementation. This package must be mirrored into the dotnet-public feed and this source
         REMOVED before merging. DO NOT MERGE with nuget.org present. -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

src/Essentials/samples/Samples/ViewModel/BaseViewModel.cs:47

  • DisplayPromptAsync returns Task but falls back to a null string when there is no UI handler. That violates the non-nullable return contract and can surface as null reference issues for callers.
    src/Essentials/samples/Samples.WebServer/Program.cs:91
  • Forwarded headers are trusted from any source by clearing KnownIPNetworks/KnownProxies. That enables X-Forwarded-* spoofing if this sample is deployed directly, and it can weaken origin/scheme/host-based logic. Consider enabling this only in Development (dev-tunnel scenario).

Xamarin.AndroidX.Credentials 1.6.0.1 (used by the Passkeys Android implementation)
is now mirrored into the dotnet-public feed, so the temporary nuget.org source
added to restore it is no longer needed. Verified a clean restore (cache cleared)
resolves the package from dotnet-public with nuget.org removed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8fa25a04-ac6e-44ad-ab51-96d594e4107d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 103 out of 160 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/Essentials/samples/Samples.WebServer/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: Generated file
Comments suppressed due to low confidence (2)

src/Essentials/src/Passkeys/Passkeys.android.cs:83

  • InvokeAsync creates a TaskCompletionSource without TaskCreationOptions.RunContinuationsAsynchronously. In this repo, other Android callback-to-Task bridges use RunContinuationsAsynchronously (e.g., ActivityForResultRequest.android.cs) to avoid running continuations inline on the callback/executor thread, which can cause reentrancy and hard-to-debug deadlocks.
    src/Essentials/samples/Samples.WebServer/Program.cs:88
  • ForwardedHeadersOptions is configured by clearing KnownIPNetworks/KnownProxies, which disables source validation and makes X-Forwarded-* spoofable. Even for a sample, it’s safer to only trust forwarded headers in Development (or behind an explicitly trusted reverse proxy) so this code isn’t accidentally deployed as-is.

Comment on lines +36 to +39
protected override void OnInitialized()
{
tokens = Services.GetService<IAntiforgery>()?.GetTokens(HttpContext);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-essentials Essentials: Device, Display, Connectivity, Secure Storage, Sensors, App Info

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants