Skip to content

Commit c1cce00

Browse files
author
Benjamin Michaelis
committed
fix: return 401/403 for API endpoints instead of redirecting
Cookie auth's HandleChallengeAsync issues a 302 redirect to the login page by default. For fetch() API calls this causes the request to follow the redirect, eventually hitting MapFallbackToController and returning a user-visible 404. Add OnRedirectToLogin and OnRedirectToAccessDenied handlers in ConfigureApplicationCookie that return 401/403 for /api/* paths, leaving the redirect behavior intact for browser page navigation. This fixes POST /api/chat/stream returning a 404 for unauthenticated users instead of a proper 401.
1 parent 8ee6af2 commit c1cce00

1 file changed

Lines changed: 26 additions & 4 deletions

File tree

EssentialCSharp.Web/Program.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,19 @@ private static void Main(string[] args)
115115
builder.Services.AddDbContext<EssentialCSharpWebContext>(options => options.UseSqlServer(connectionString, sql => sql.EnableRetryOnFailure(5)));
116116

117117
// Data Protection — persist keys across container restarts.
118-
// ACA/production: keys in Azure Blob Storage (Managed Identity, Aspire-provisioned via dp-keys connection string).
118+
// ACA/production: keys in Azure Blob Storage (Managed Identity, Aspire-provisioned via dp-blob connection string).
119+
// dp-blob is an Azure Blob Service URI: "https://account.blob.core.windows.net/"
120+
// Keys are stored as: {serviceUri}dataprotection/keys.xml
121+
// The library auto-creates the container on first write.
119122
// Local dev: keys in SQL Server (persistent via WithDataVolume in AppHost).
120123
// SetApplicationName ensures the discriminator is stable across container hostname changes.
121-
var dpBlobUri = builder.Configuration.GetConnectionString("dp-keys");
124+
var dpBlobServiceUri = builder.Configuration.GetConnectionString("dp-blob");
122125
var dataProtection = builder.Services.AddDataProtection()
123126
.SetApplicationName("EssentialCSharpWeb");
124-
if (!string.IsNullOrEmpty(dpBlobUri))
127+
if (!string.IsNullOrEmpty(dpBlobServiceUri))
125128
{
126129
dataProtection.PersistKeysToAzureBlobStorage(
127-
new Uri($"{dpBlobUri.TrimEnd('/')}/keys.xml"),
130+
new Uri(new Uri(dpBlobServiceUri), "dataprotection/keys.xml"),
128131
new DefaultAzureCredential());
129132
}
130133
else
@@ -166,6 +169,25 @@ private static void Main(string[] args)
166169
options.Cookie.HttpOnly = true;
167170
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
168171
options.SlidingExpiration = true;
172+
// API endpoints must return 401/403 instead of redirecting to the login page.
173+
// Cookie auth's default behavior (302 redirect) causes fetch() to follow the
174+
// redirect, eventually hitting the fallback controller and returning a 404.
175+
options.Events.OnRedirectToLogin = context =>
176+
{
177+
if (context.Request.Path.StartsWithSegments("/api"))
178+
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
179+
else
180+
context.Response.Redirect(context.RedirectUri);
181+
return Task.CompletedTask;
182+
};
183+
options.Events.OnRedirectToAccessDenied = context =>
184+
{
185+
if (context.Request.Path.StartsWithSegments("/api"))
186+
context.Response.StatusCode = StatusCodes.Status403Forbidden;
187+
else
188+
context.Response.Redirect(context.RedirectUri);
189+
return Task.CompletedTask;
190+
};
169191
});
170192

171193
builder.Services.Configure<PasswordHasherOptions>(option =>

0 commit comments

Comments
 (0)