Fix passkey RPID mismatch across environments

Derive ServerDomain and Origin from the incoming HTTP request instead of
appsettings.json, so WebAuthn works on localhost, dev, and production
without any environment-specific configuration. Removed IFido2 from DI
and the Fido2 appsettings block — PasskeyController instantiates Fido2
per-request via BuildFido2().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 15:49:45 -04:00
parent edc599a1a2
commit c71332740e
3 changed files with 35 additions and 23 deletions
@@ -15,11 +15,14 @@ namespace PowderCoating.Web.Controllers;
/// Registration requires an authenticated session (user logs in once with password,
/// then enrolls a passkey for future logins). Authentication is anonymous — the
/// browser sends the credential before any session exists.
///
/// Fido2 is constructed per-request from the incoming Host header so the RPID
/// matches automatically on localhost, dev, staging, and production without any
/// environment-specific configuration.
/// </summary>
[Route("[controller]/[action]")]
public class PasskeyController : Controller
{
private readonly IFido2 _fido2;
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ApplicationDbContext _db;
@@ -29,19 +32,41 @@ public class PasskeyController : Controller
private const string AuthChallengeKey = "passkey:auth:challenge";
public PasskeyController(
IFido2 fido2,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ApplicationDbContext db,
ILogger<PasskeyController> logger)
{
_fido2 = fido2;
_userManager = userManager;
_signInManager = signInManager;
_db = db;
_logger = logger;
}
/// <summary>
/// Builds a Fido2 instance whose RPID and origin are derived from the current
/// request, so the same code works on localhost, dev, and production unchanged.
/// </summary>
private IFido2 BuildFido2()
{
var req = HttpContext.Request;
var host = req.Host.Host; // "localhost" or "myapp.azurewebsites.net"
var port = req.Host.Port;
var origin = port.HasValue
? $"{req.Scheme}://{host}:{port}"
: $"{req.Scheme}://{host}";
var config = new Fido2Configuration
{
ServerDomain = host,
ServerName = "Powder Coating Logix",
Origins = new HashSet<string> { origin },
TimestampDriftTolerance = 300
};
return new Fido2(config);
}
// ─── Registration ────────────────────────────────────────────────────────
/// <summary>
@@ -77,7 +102,7 @@ public class PasskeyController : Controller
UserVerification = UserVerificationRequirement.Required
};
var options = _fido2.RequestNewCredential(new RequestNewCredentialParams
var options = BuildFido2().RequestNewCredential(new RequestNewCredentialParams
{
User = fidoUser,
ExcludeCredentials = excludeCredentials,
@@ -112,7 +137,7 @@ public class PasskeyController : Controller
try
{
var options = CredentialCreateOptions.FromJson(optionsJson);
credential = await _fido2.MakeNewCredentialAsync(new MakeNewCredentialParams
credential = await BuildFido2().MakeNewCredentialAsync(new MakeNewCredentialParams
{
AttestationResponse = attestationResponse,
OriginalOptions = options,
@@ -155,7 +180,7 @@ public class PasskeyController : Controller
[AllowAnonymous]
public IActionResult LoginOptions()
{
var options = _fido2.GetAssertionOptions(new GetAssertionOptionsParams
var options = BuildFido2().GetAssertionOptions(new GetAssertionOptionsParams
{
AllowedCredentials = [],
UserVerification = UserVerificationRequirement.Required
@@ -199,7 +224,7 @@ public class PasskeyController : Controller
try
{
var options = AssertionOptions.FromJson(optionsJson);
verifyResult = await _fido2.MakeAssertionAsync(new MakeAssertionParams
verifyResult = await BuildFido2().MakeAssertionAsync(new MakeAssertionParams
{
AssertionResponse = assertionResponse,
OriginalOptions = options,
+3 -10
View File
@@ -290,16 +290,9 @@ builder.Services.AddSession(options =>
// Add memory cache
builder.Services.AddMemoryCache();
// Register Fido2/WebAuthn for passkey (biometric) login
builder.Services.AddFido2(options =>
{
options.ServerDomain = builder.Configuration["Fido2:ServerDomain"] ?? "localhost";
options.ServerName = builder.Configuration["Fido2:ServerName"] ?? "Powder Coating Logix";
var origins = builder.Configuration.GetSection("Fido2:Origins").Get<HashSet<string>>();
if (origins?.Count > 0) options.Origins = origins;
options.TimestampDriftTolerance = int.Parse(
builder.Configuration["Fido2:TimestampDriftTolerance"] ?? "300");
});
// Fido2/WebAuthn: no DI registration needed — PasskeyController builds a
// per-request Fido2 instance from the incoming Host header so the RPID matches
// automatically on every environment without config changes.
// Configure authorization policies for multi-tenancy
builder.Services.AddAuthorization(options =>
-6
View File
@@ -68,12 +68,6 @@
"Enterprise": "price_enterprise_monthly_id_here"
}
},
"Fido2": {
"ServerDomain": "localhost",
"ServerName": "Powder Coating Logix",
"Origins": [ "https://localhost:58461", "http://localhost:58462" ],
"TimestampDriftTolerance": 300
},
"Storage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=powdercoatingappdev;AccountKey=DN3eVfhytXb7aBC0md9h/6jE0Uzg6FJ+PK6MFc772qyqpf0kgTeXH0C2VCBBun9PiuItPd9CDKTP+ASthFCuCg==;EndpointSuffix=core.windows.net",
"Containers": {