fix(auth): return 401 instead of 302 for API challenges; persist dev signing key across restarts
Root cause of the 404 on /api/admin/moysklad/test (and /api/me): - AddIdentity<> sets DefaultChallengeScheme = IdentityConstants.ApplicationScheme (cookies), so unauthorized API calls got 302 → /Account/Login → 404 instead of 401. - Ephemeral OpenIddict keys (AddEphemeralSigningKey) regenerated on every API restart, silently invalidating any JWT already stored in the browser. Fixes: - Explicitly set DefaultScheme / DefaultAuthenticateScheme / DefaultChallengeScheme to OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme so [Authorize] challenges now return 401 (axios interceptor can react + retry or redirect). - Replace ephemeral RSA keys with a persistent dev RSA key stored in src/food-market.api/App_Data/openiddict-dev-key.xml (gitignored). Generated on first run, reused on subsequent starts. Dev tokens now survive API restarts. Production must register proper X509 certificates via configuration. - .gitignore: add App_Data/, *.pem, openiddict-dev-key.xml patterns. - Web axios: on hard 401 with failed refresh, redirect to /login rather than leaving the user stuck on a protected screen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cead88b0bc
commit
b07232521b
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -66,10 +66,15 @@ pnpm-debug.log*
|
|||
## Secrets
|
||||
*.pfx
|
||||
*.snk
|
||||
*.pem
|
||||
secrets.json
|
||||
appsettings.Development.local.json
|
||||
appsettings.Production.local.json
|
||||
|
||||
## OpenIddict dev keys (local only, never commit)
|
||||
src/food-market.api/App_Data/
|
||||
**/App_Data/openiddict-dev-key.xml
|
||||
|
||||
## Docker / local
|
||||
.docker-data/
|
||||
postgres-data/
|
||||
|
|
|
|||
|
|
@ -66,9 +66,25 @@
|
|||
opts.AcceptAnonymousClients();
|
||||
opts.RegisterScopes(Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Roles, "api");
|
||||
|
||||
// Persistent dev keys: RSA key stored in src/food-market.api/App_Data/openiddict-dev-key.xml.
|
||||
// Survives API restarts so issued tokens remain valid across rebuilds.
|
||||
// Never commit this file (it's in .gitignore via App_Data/). Production must use real certificates.
|
||||
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "openiddict-dev-key.xml");
|
||||
var rsa = System.Security.Cryptography.RSA.Create(2048);
|
||||
if (File.Exists(keyPath))
|
||||
{
|
||||
rsa.FromXmlString(File.ReadAllText(keyPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(keyPath)!);
|
||||
File.WriteAllText(keyPath, rsa.ToXmlString(includePrivateParameters: true));
|
||||
}
|
||||
var devKey = new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa) { KeyId = "food-market-dev" };
|
||||
opts.AddEncryptionKey(devKey);
|
||||
opts.AddSigningKey(devKey);
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
opts.AddEphemeralEncryptionKey().AddEphemeralSigningKey();
|
||||
opts.DisableAccessTokenEncryption();
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +103,14 @@
|
|||
opts.UseAspNetCore();
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
// Force OpenIddict validation to handle ALL [Authorize] challenges — otherwise AddIdentity's
|
||||
// cookie scheme takes over and 401 becomes a 302 redirect to /Account/Login for API calls.
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ api.interceptors.response.use(
|
|||
return api(original)
|
||||
}
|
||||
clearTokens()
|
||||
// Redirect to login so user isn't stuck on a protected page with stale tokens.
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue