[AllowAnonymous]

Open specific endpoints such as health checks and login to anonymous callers under a fallback policy, and audit exactly what [AllowAnonymous] exposes.

Signature
[AllowAnonymous]  |  app.MapGet("/health", ...).AllowAnonymous()
  • Added in .NET ASP.NET Core 1.0

Return value

n/aAdds IAllowAnonymous metadata. When the authorization middleware finds it on an endpoint, it skips authorization entirely, including the fallback policy and every [Authorize] or RequireAuthorization on the endpoint, its controller or its group.

Errors

Nothing. It never fails; its risk is exposing more than intended.

Changelog

  • PHP ASP.NET Core 3.0Honoured by the authorization middleware as endpoint metadata, which made it win over [Authorize] wherever either is declared.
  • PHP ASP.NET Core 6.0Minimal APIs: attribute on the handler or .AllowAnonymous() on the endpoint or group.

[AllowAnonymous] is the opt-out. The authorization middleware asks one question first: does the endpoint carry IAllowAnonymous metadata? If so, it skips evaluation completely and the request proceeds as anonymous or authenticated, whatever it is. There is no precedence order to reason about: wherever the marker is attached (action, controller, base class, route group, endpoint), it wins over every [Authorize] on that endpoint.

Use it with a fallback policy

The safe design is secure by default: set a FallbackPolicy that requires an authenticated user, so every endpoint without its own authorization metadata is protected, including those added next month. Then mark the few public endpoints, such as health probes, a login or token endpoint, a public product catalogue or webhook receivers with their own signature check, with [AllowAnonymous]. The fallback policy applies only to endpoints without any authorization metadata; a DefaultPolicy only shapes what bare [Authorize] means.

What it does not do

  • It does not skip authentication. If the request carries a valid token, HttpContext.User is populated, so an anonymous endpoint can still personalise its answer.
  • It does not bypass other middleware: rate limiting, CORS and antiforgery still apply.
  • It does not let an invalid token through as an error: an expired token on an anonymous endpoint just means an unauthenticated user.

Production failure modes

MistakeResult
[AllowAnonymous] on a controller class to open one actionEvery action, including [Authorize(Roles = "admin")] ones, is public.
On a shared base controllerEvery derived controller is public.
.AllowAnonymous() on a route groupEvery endpoint in the group, including ones added later.

Put the attribute on individual actions or endpoints only, and keep a test that lists every endpoint carrying IAllowAnonymous and compares it with an approved list. A new anonymous endpoint then needs a deliberate change to the test, which shows up in code review.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Let Kubernetes liveness and readiness probes reach /health while a fallback policy protects every other endpoint
  • Serve a public product list from an otherwise authenticated catalogue controller
  • Fail the build when a pull request adds an endpoint that allows anonymous access without updating the approved list

Frequently asked questions

Does [AllowAnonymous] beat [Authorize] on the same action?

Yes. The middleware skips authorization whenever the endpoint has IAllowAnonymous metadata, regardless of where either attribute is declared.

Is the user still available on an anonymous endpoint?

Yes, if the request carries valid credentials. Authentication still runs; only the authorization check is skipped, so HttpContext.User may be authenticated.

Should health checks be anonymous?

Liveness and readiness probes usually must be, because the orchestrator sends no token. Keep them free of sensitive detail, and put a detailed health report on a separate, protected endpoint.

More examples

Public endpoints in a protected group

<code>.AllowAnonymous()</code> on one endpoint overrides the group's <code>RequireAuthorization()</code> for that endpoint only. Health endpoints need it whenever a fallback policy exists, or Kubernetes probes get 401 and the pod is restarted in a loop.

Example · csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddHealthChecks();

var app = builder.Build();

var api = app.MapGroup("/api").RequireAuthorization();

api.MapGet("/orders", () => TypedResults.Ok(Array.Empty<object>()));

// Only this endpoint is public; the group stays protected.
api.MapGet("/products", () => TypedResults.Ok(new[] { new { sku = "MUG-WHITE", price = 12.50m } }))
   .AllowAnonymous();

// Probes from the orchestrator carry no token.
app.MapHealthChecks("/health").AllowAnonymous();

app.Run();

A test that pins the list of anonymous endpoints

The test reads the real endpoint metadata from the running app, so it sees attributes, group conventions and inheritance exactly as the authorization middleware does. Adding a public endpoint now requires editing <code>Approved</code>, which makes the change visible in review. The test project references the API project, which exposes its <code>Program</code> with a partial declaration in its own <code>Program.cs</code>.

Example · csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Routing;
using Xunit;

public class AnonymousEndpointTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
    private static readonly string[] Approved = ["api/catalog/products", "health", "auth/token"];

    [Fact]
    public void Only_approved_endpoints_allow_anonymous_access()
    {
        var anonymous = factory.Services.GetRequiredService<EndpointDataSource>().Endpoints
            .OfType<RouteEndpoint>()
            .Where(e => e.Metadata.GetMetadata<IAllowAnonymous>() is not null)
            .Select(e => e.RoutePattern.RawText!.TrimStart('/'))
            .Order()
            .ToArray();

        Assert.Equal(Approved.Order(), anonymous);
    }
}

// Stand-in so this file compiles alone. In a real test project delete it: the API's
// Program.cs ends with "public partial class Program;" in the global namespace, this
// project references the API project, and WebApplicationFactory<Program> then boots
// the real API. Declared here inside a namespace, it would be a different, empty class.
public partial class Program;

Webhook receiver: anonymous, but verified

The payment provider cannot obtain a token from your identity provider, so the endpoint must be anonymous to the authorization middleware. Anonymous does not mean unauthenticated in practice: the HMAC signature over timestamp and body, the five-minute freshness window and event-id de-duplication together are the authentication, since a signature alone lets a captured delivery be replayed forever. The size limit and <code>RequestAborted</code> stop an unauthenticated caller from making the server buffer a large body, and <code>FixedTimeEquals</code> avoids leaking the signature through timing.

Example · csharp
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
var app = builder.Build();

// Processed event ids; use a shared store with a TTL when running several replicas.
var processed = new ConcurrentDictionary<string, DateTimeOffset>();

app.MapPost("/webhooks/payments", [AllowAnonymous] [RequestSizeLimit(64 * 1024)] async (HttpRequest request, IConfiguration config) =>
{
    // Freshness: the provider signs "timestamp.body"; old deliveries are replays.
    if (!long.TryParse(request.Headers["X-Timestamp"], out var unixSeconds)
        || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - unixSeconds) > 300)
    {
        return Results.Unauthorized();
    }

    using var reader = new StreamReader(request.Body);
    var payload = await reader.ReadToEndAsync(request.HttpContext.RequestAborted);
    var secret = Encoding.UTF8.GetBytes(config["Payments:WebhookSecret"]!);
    var signed = Encoding.UTF8.GetBytes($"{unixSeconds}.{payload}");
    var expected = Convert.ToHexStringLower(HMACSHA256.HashData(secret, signed));
    var received = request.Headers["X-Signature"].ToString();

    if (!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(received)))
    {
        return Results.Unauthorized();
    }

    // De-duplication: a verified delivery with a known event id is acknowledged, not reprocessed.
    var eventId = request.Headers["X-Event-Id"].ToString();
    if (eventId.Length == 0) return Results.BadRequest();
    if (!processed.TryAdd(eventId, DateTimeOffset.UtcNow)) return Results.Ok();

    // Process the payment event once here.
    return Results.Ok();
});

app.Run();

Discussion

  • Be the first to comment on this lesson.