Skip to content

[Breaking change]: Cookie login redirects are disabled for known API endpoints #525

@halter73

Description

@halter73

Description

By default, unauthenticated and unauthorized requests made to known API endpoints protected by cookie authentication now result in 401 and 403 responses rather than redirecting to a login or access denied URI.

Known API Endpoints are identified using the new IDisableCookieRedirectMetadata interface (previously IApiEndpointMetadata), and metadata implementing this interface is added automatically to the following:

  • [ApiController] endpoints
  • Minimal API endpoints that read JSON request bodies or write JSON responses
  • Endpoints using TypedResults return types
  • SignalR endpoints

Update: Additional Workarounds and Metadata

After feedback on the initial change, we've added more flexible ways to work around issues caused by this breaking change:

  • New metadata interface: IDisableCookieRedirectMetadata replaces IApiEndpointMetadata.
  • Opt-in attribute & metadata: You can now use the [AllowCookieRedirect] attribute or implement the IAllowCookieRedirectMetadata interface on your endpoints to explicitly allow cookie authentication redirects, even on endpoints that would otherwise suppress them.
  • Global opt-out AppContext Switch: You can set the AppContext switch Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata to true to revert to the old behavior globally and ignore the new metadata-based suppression. This is particularly useful for large apps or frameworks (like UmbracoCMS) that need a quick mitigation.

Version

.NET 10 Preview 7

Previous behavior

The cookie authentication handler would redirect unauthenticated and unauthorized requests to a login or access denied URI by default for all requests other than XMLHttpRequests (XHRs).

New behavior

Unauthenticated and unauthorized requests made to known API endpoints will result in 401 and 403 responses rather than redirecting to a login or access denied URI. XHRs continue to result in 401 and 403 responses regardless of the target endpoint.

Type of breaking change

  • Binary incompatible: Existing binaries may encounter a breaking change in behavior, such as failure to load or execute, and if so, require recompilation.
  • Source incompatible: When recompiled using the new SDK or component or to target the new runtime, existing source code may require source changes to compile successfully.
  • Behavioral change: Existing binaries may behave differently at run time.

Reason for change

dotnet/aspnetcore#9039 was highly requested, and redirecting unauthenticated requests to a login page doesn't usually make sense for API endpoints which typically rely on 401 and 403 status codes rather than HTML redirects to communicate auth failures.

Recommended action

To always redirect (fully disable new behavior):

Recommended for frameworks/applications needing an immediate opt-out or full legacy compatibility:

Set the following AppContext switch (e.g., in your app's runtimeconfig.json or programmatically at startup):

{
  "runtimeOptions": {
    "configProperties": {
      "Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata": true
    }
  }
}

Or, set programmatically before building the host:

AppContext.SetSwitch("Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata", true);

To allow redirects on specific endpoints:

Apply the [AllowCookieRedirect] attribute to controllers or endpoints that should preserve cookie-based redirects, or implement the IAllowCookieRedirectMetadata interface.

[AllowCookieRedirect]
public class MyLegacyApiController : Controller { ... }

To revert to the exact previous handler logic (redirect except for XHRs):

You can override the events with the following logic:

builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        bool IsXhr(HttpRequest request)
        {
            return string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) ||
                string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal);
        }

        options.Events.OnRedirectToLogin = context =>
        {
            if (IsXhr(context.Request))
            {
                context.Response.Headers.Location = context.RedirectUri;
                context.Response.StatusCode = 401;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }

            return Task.CompletedTask;
        };

        options.Events.OnRedirectToAccessDenied = context =>
        {
            if (IsXhr(context.Request))
            {
                context.Response.Headers.Location = context.RedirectUri;
                context.Response.StatusCode = 403;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }

            return Task.CompletedTask;
        };
    });

To always redirect regardless of endpoint type or XHR:

Override the events to always redirect:

builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        options.Events.OnRedirectToLogin = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };

        options.Events.OnRedirectToAccessDenied = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };
    });

References

Affected APIs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Breaking changeDocumentedThe breaking change has been published to the .NET Core docs

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions