dotnet / dotnet/aspnetcore

Conditionally mediated passkey creation

Open
#68,192 0 comments 0 reactions 0 assignees View on GitHub
api-proposal api-suggestion area-identity
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

Tracked by #67298, implemented in #68194.

## Background and Motivation

Getting people onto passkeys is mostly a UI problem. Today the only way an Identity app can create one is if the user goes looking for an "add a passkey" button and clicks it, and most never do, so they stay on passwords.

WebAuthn has a feature for this called [conditional create](https://developer.chrome.com/docs/identity/webauthn-conditional-create). If the user has a saved password for the site and just used it, the site can create a passkey silently, with no prompt and no button, right after sign-in. Chrome on desktop and Android and Safari on macOS and iOS support it.

Identity cannot do this today, because the server has to be told up front that a creation is conditional. It changes both what the server asks for and what it accepts:

- The authenticator reports user-present and user-verified as false, and the current code rejects a registration where user-present is false.
- `IdentityPasskeyOptions.UserVerificationRequirement` defaults to `"required"`, and the spec has the browser refuse the ceremony outright when verification is required and mediation is conditional, so no credential reaches the server at all.

The flag cannot be inferred: mediation is not part of `clientDataJSON`. It also cannot be taken from the client at attestation time, because that would let anyone switch off the user-presence check on an ordinary registration. So it has to be supplied when the options are made and carried in the server's own data-protected state.

## Proposed API

```diff
namespace Microsoft.AspNetCore.Identity;

public interface IPasskeyHandler
where TUser : class
{
+ bool SupportsConditionalCreation => false;

Task MakeCreationOptionsAsync(PasskeyUserEntity userEntity, HttpContext httpContext);
+ Task MakeCreationOptionsAsync(PasskeyUserEntity userEntity, bool isConditionallyMediated, HttpContext httpContext) { }

Task MakeRequestOptionsAsync(TUser? user, HttpContext httpContext);
Task PerformAttestationAsync(PasskeyAttestationContext context);
Task> PerformAssertionAsync(PasskeyAssertionContext context);
}

public sealed class PasskeyHandler : IPasskeyHandler
where TUser : class
{
+ public bool SupportsConditionalCreation { get; }

public Task MakeCreationOptionsAsync(PasskeyUserEntity userEntity, HttpContext httpContext);
+ public Task MakeCreationOptionsAsync(PasskeyUserEntity userEntity, bool isConditionallyMediated, HttpContext httpContext);
}

public class SignInManager
where TUser : class
{
+ public virtual bool SupportsPasskeyConditionalCreation { get; }

public virtual Task MakePasskeyCreationOptionsAsync(PasskeyUserEntity userEntity);
+ public virtual Task MakePasskeyCreationOptionsAsync(PasskeyUserEntity userEntity, bool isConditionallyMediated);
}
```

Both new interface members are default implementations. `SupportsConditionalCreation` returns `false`, and the new overload throws `NotSupportedException` when asked for a conditional creation and otherwise forwards to the existing method.

## Usage Examples

Server side, immediately after a successful password sign-in:

```csharp
if (SignInManager.SupportsPasskeyConditionalCreation)
{
creationOptionsJson = await SignInManager.MakePasskeyCreationOptionsAsync(
userEntity,
isConditionallyMediated: true);
}
```

Browser side, with the feature detection the spec asks for:

```js
const capabilities = await PublicKeyCredential.getClientCapabilities?.();
if (capabilities?.conditionalCreate) {
const credential = await navigator.credentials.create({
publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(creationOptionsJson)),
mediation: 'conditional',
});
}
```

## Alternative Designs

**A `PasskeyCreationArgs` options object instead of the bool.** It absorbs future WebAuthn options without another overload each time. It is source-breaking. Added beside the shipped `MakeCreationOptionsAsync(PasskeyUserEntity, HttpContext)`, any existing call passing a target-typed `new() { ... }` becomes `CS0121` ambiguous, because overload resolution picks the candidate before it looks at the object initializer. I compiled this to confirm it, and the repo's own Blazor template calls the method in exactly that shape. A differently named method avoids the ambiguity but adds more surface for one bool.

**A flag on `IdentityPasskeyOptions`.** Rejected: this is a per-call decision, not a per-app one. The same app makes ordinary creations from its "add a passkey" page and conditional ones after a password sign-in.

**No capability property, let the new method throw.** Rejected: an app with a custom `IPasskeyHandler` that has not implemented the overload would then throw on every password sign-in with no way to ask first.

## Risks

**No binary or source break.** Everything is additive. The new overload differs in arity from the shipped one, so no existing call site changes meaning. This is the specific thing that ruled out the options-object design, so I checked it by compiling rather than by reasoning.

**Two new members on a shipped interface**, both default implementations, so existing implementers keep compiling and keep their current behaviour: `SupportsConditionalCreation` is `false` and the new overload throws only when asked for a conditional creation.

**Issuing conditional options is an authorization decision the caller owns.** Once options are issued the client cannot change the mediation mode: the flag lives in the server's data-protected attestation state, so verification uses the rules chosen at issuance. But nothing stops an app from issuing conditional options to any request carrying a session cookie, which would turn stolen-cookie access into a registered passkey. The remarks on both new methods say the caller must only request conditional mediation after a recent successful password authentication, and that an existing session is not sufficient on its own. The framework cannot enforce this: what counts as recent is app-specific, and Identity has no reliable signal for it, since the application cookie's issue time moves with sliding expiration and the security stamp check runs on a 30 minute interval by default. The Blazor template follows the documented rule by creating the options inside the sign-in handler and carrying them to the upgrade page in protected TempData.

**A conditionally created passkey has `IsUserVerified` false**, which is inherent to the feature rather than a defect. An app that treats passkeys as a second factor, or otherwise relies on verification, should not use this.

Contributor guide

Open the contributing guide

Research direction

The issue is tracked by #67298 and implemented in #68194; start by reviewing those linked issues and the IPasskeyHandler, PasskeyHandler, and SignInManager entry points listed here. The proposed work is already represented by the linked pull request, so verify its API, conditional WebAuthn behavior, and compatibility against the usage examples and stated risks.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, javascript
Domain
api, authentication, backend-api-design
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.