Compare commits
2 Commits
edc599a1a2
...
92f71f62d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 92f71f62d0 | |||
| c71332740e |
@@ -15,11 +15,14 @@ namespace PowderCoating.Web.Controllers;
|
|||||||
/// Registration requires an authenticated session (user logs in once with password,
|
/// Registration requires an authenticated session (user logs in once with password,
|
||||||
/// then enrolls a passkey for future logins). Authentication is anonymous — the
|
/// then enrolls a passkey for future logins). Authentication is anonymous — the
|
||||||
/// browser sends the credential before any session exists.
|
/// 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>
|
/// </summary>
|
||||||
[Route("[controller]/[action]")]
|
[Route("[controller]/[action]")]
|
||||||
public class PasskeyController : Controller
|
public class PasskeyController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFido2 _fido2;
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
private readonly ApplicationDbContext _db;
|
private readonly ApplicationDbContext _db;
|
||||||
@@ -29,19 +32,41 @@ public class PasskeyController : Controller
|
|||||||
private const string AuthChallengeKey = "passkey:auth:challenge";
|
private const string AuthChallengeKey = "passkey:auth:challenge";
|
||||||
|
|
||||||
public PasskeyController(
|
public PasskeyController(
|
||||||
IFido2 fido2,
|
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
SignInManager<ApplicationUser> signInManager,
|
SignInManager<ApplicationUser> signInManager,
|
||||||
ApplicationDbContext db,
|
ApplicationDbContext db,
|
||||||
ILogger<PasskeyController> logger)
|
ILogger<PasskeyController> logger)
|
||||||
{
|
{
|
||||||
_fido2 = fido2;
|
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
_db = db;
|
_db = db;
|
||||||
_logger = logger;
|
_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 ────────────────────────────────────────────────────────
|
// ─── Registration ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -77,7 +102,7 @@ public class PasskeyController : Controller
|
|||||||
UserVerification = UserVerificationRequirement.Required
|
UserVerification = UserVerificationRequirement.Required
|
||||||
};
|
};
|
||||||
|
|
||||||
var options = _fido2.RequestNewCredential(new RequestNewCredentialParams
|
var options = BuildFido2().RequestNewCredential(new RequestNewCredentialParams
|
||||||
{
|
{
|
||||||
User = fidoUser,
|
User = fidoUser,
|
||||||
ExcludeCredentials = excludeCredentials,
|
ExcludeCredentials = excludeCredentials,
|
||||||
@@ -112,7 +137,7 @@ public class PasskeyController : Controller
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var options = CredentialCreateOptions.FromJson(optionsJson);
|
var options = CredentialCreateOptions.FromJson(optionsJson);
|
||||||
credential = await _fido2.MakeNewCredentialAsync(new MakeNewCredentialParams
|
credential = await BuildFido2().MakeNewCredentialAsync(new MakeNewCredentialParams
|
||||||
{
|
{
|
||||||
AttestationResponse = attestationResponse,
|
AttestationResponse = attestationResponse,
|
||||||
OriginalOptions = options,
|
OriginalOptions = options,
|
||||||
@@ -155,7 +180,7 @@ public class PasskeyController : Controller
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public IActionResult LoginOptions()
|
public IActionResult LoginOptions()
|
||||||
{
|
{
|
||||||
var options = _fido2.GetAssertionOptions(new GetAssertionOptionsParams
|
var options = BuildFido2().GetAssertionOptions(new GetAssertionOptionsParams
|
||||||
{
|
{
|
||||||
AllowedCredentials = [],
|
AllowedCredentials = [],
|
||||||
UserVerification = UserVerificationRequirement.Required
|
UserVerification = UserVerificationRequirement.Required
|
||||||
@@ -199,7 +224,7 @@ public class PasskeyController : Controller
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var options = AssertionOptions.FromJson(optionsJson);
|
var options = AssertionOptions.FromJson(optionsJson);
|
||||||
verifyResult = await _fido2.MakeAssertionAsync(new MakeAssertionParams
|
verifyResult = await BuildFido2().MakeAssertionAsync(new MakeAssertionParams
|
||||||
{
|
{
|
||||||
AssertionResponse = assertionResponse,
|
AssertionResponse = assertionResponse,
|
||||||
OriginalOptions = options,
|
OriginalOptions = options,
|
||||||
|
|||||||
@@ -290,16 +290,9 @@ builder.Services.AddSession(options =>
|
|||||||
// Add memory cache
|
// Add memory cache
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
// Register Fido2/WebAuthn for passkey (biometric) login
|
// Fido2/WebAuthn: no DI registration needed — PasskeyController builds a
|
||||||
builder.Services.AddFido2(options =>
|
// per-request Fido2 instance from the incoming Host header so the RPID matches
|
||||||
{
|
// automatically on every environment without config changes.
|
||||||
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");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure authorization policies for multi-tenancy
|
// Configure authorization policies for multi-tenancy
|
||||||
builder.Services.AddAuthorization(options =>
|
builder.Services.AddAuthorization(options =>
|
||||||
|
|||||||
@@ -68,12 +68,6 @@
|
|||||||
"Enterprise": "price_enterprise_monthly_id_here"
|
"Enterprise": "price_enterprise_monthly_id_here"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Fido2": {
|
|
||||||
"ServerDomain": "localhost",
|
|
||||||
"ServerName": "Powder Coating Logix",
|
|
||||||
"Origins": [ "https://localhost:58461", "http://localhost:58462" ],
|
|
||||||
"TimestampDriftTolerance": 300
|
|
||||||
},
|
|
||||||
"Storage": {
|
"Storage": {
|
||||||
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=powdercoatingappdev;AccountKey=DN3eVfhytXb7aBC0md9h/6jE0Uzg6FJ+PK6MFc772qyqpf0kgTeXH0C2VCBBun9PiuItPd9CDKTP+ASthFCuCg==;EndpointSuffix=core.windows.net",
|
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=powdercoatingappdev;AccountKey=DN3eVfhytXb7aBC0md9h/6jE0Uzg6FJ+PK6MFc772qyqpf0kgTeXH0C2VCBBun9PiuItPd9CDKTP+ASthFCuCg==;EndpointSuffix=core.windows.net",
|
||||||
"Containers": {
|
"Containers": {
|
||||||
|
|||||||
@@ -186,11 +186,21 @@ async function loginWithPasskey() {
|
|||||||
|
|
||||||
// ─── Feature detection ────────────────────────────────────────────────────────
|
// ─── Feature detection ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** True if this browser + platform support WebAuthn conditional UI (passkeys). */
|
/**
|
||||||
|
* True if the device has a user-verifying platform authenticator (Face ID,
|
||||||
|
* fingerprint, Windows Hello, etc.) that can handle our modal passkey flow.
|
||||||
|
*
|
||||||
|
* Deliberately uses isUserVerifyingPlatformAuthenticatorAvailable() rather than
|
||||||
|
* isConditionalMediationAvailable(). The conditional API signals to iOS Safari
|
||||||
|
* that the page wants autofill-style passkey interception, which causes iOS 17+
|
||||||
|
* to show its own native passkey enrollment sheet when the password form is
|
||||||
|
* submitted — not what we want. The platform authenticator check simply asks
|
||||||
|
* "can this device do biometrics?" with no side-effects.
|
||||||
|
*/
|
||||||
async function passkeySupported() {
|
async function passkeySupported() {
|
||||||
if (!window.PublicKeyCredential) return false;
|
if (!window.PublicKeyCredential) return false;
|
||||||
try {
|
try {
|
||||||
return await PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;
|
return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user