{"id":30,"date":"2024-08-24T11:41:02","date_gmt":"2024-08-24T10:41:02","guid":{"rendered":"https:\/\/contentlabstudy.com\/soft\/?p=30"},"modified":"2026-09-24T21:16:45","modified_gmt":"2026-09-24T20:16:45","slug":"rest-minimal-api-with-authentication","status":"publish","type":"post","link":"https:\/\/contentlabstudy.com\/soft\/rest-minimal-api-with-authentication\/","title":{"rendered":"REST Minimal API with Authentication"},"content":{"rendered":"<p>An HTTP API that can be called without proving who is calling is a public API, even if you \u201cdidn\u2019t document it.\u201d <strong>Authentication<\/strong> answers who; <strong>authorization<\/strong> answers what they may do. In ASP.NET Core Minimal APIs both are middleware + metadata on endpoints, not a custom <code>if (token == \"secret\")<\/code>.<\/p>\n<h2>Pick a scheme and stay boring<\/h2>\n<ul>\n<li><strong>Cookie + session<\/strong> (or cookie + ASP.NET Core Identity) for first-party browsers. <code>HttpOnly<\/code>, <code>Secure<\/code>, <code>SameSite<\/code>. CSRF tokens if the cookie is sent automatically.<\/li>\n<li><strong>Bearer JWT<\/strong> for service-to-service or SPA + API when you accept token theft as a design constraint. Validate issuer, audience, lifetime, signing key. Do not roll your own JWT parser.<\/li>\n<li><strong>Mutual TLS<\/strong> or signed requests for high-value machine clients.<\/li>\n<\/ul>\n<p>Do not put long-lived JWTs in localStorage and call it security. Do not log Authorization headers.<\/p>\n<h2>Wire-up shape<\/h2>\n<p>The HMAC key comes from configuration. Use the asymmetric key you actually issue when that is how the tokens are signed. A missing <code>Jwt:SigningKey<\/code> fails startup. A policy name that was never registered fails the request; it does not mean \u201cany logged-in user.\u201d<\/p>\n<pre><code>builder.Services.AddAuthentication().AddJwtBearer(options =&gt;\n{\n    options.MapInboundClaims = false; \/\/ so \"sub\" stays \"sub\"\n    var key = builder.Configuration[\"Jwt:SigningKey\"];\n    if (string.IsNullOrWhiteSpace(key))\n        throw new InvalidOperationException(\"Jwt:SigningKey is required.\");\n    options.TokenValidationParameters = new TokenValidationParameters\n    {\n        ValidateIssuer = true,\n        ValidIssuer = builder.Configuration[\"Jwt:Issuer\"],\n        ValidateAudience = true,\n        ValidAudience = builder.Configuration[\"Jwt:Audience\"],\n        ValidateLifetime = true,\n        ValidateIssuerSigningKey = true,\n        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))\n    };\n});\nbuilder.Services.AddAuthorization(options =&gt;\n{\n    options.AddPolicy(\"orders.read\", policy =&gt; policy.RequireClaim(\"scope\", \"orders.read\"));\n});\nvar app = builder.Build();\napp.UseAuthentication();\napp.UseAuthorization();\nvar orders = app.MapGroup(\"\/orders\").RequireAuthorization(\"orders.read\");\norders.MapGet(\"\/{id:guid}\", GetOrder);<\/code><\/pre>\n<p><code>RequireAuthorization()<\/code> with no policy still demands an authenticated user. Policies encode roles\/claims (\u201cmust be in tenant X\u201d). Resource-based checks (this order belongs to that user) happen in the handler, not only in a global role.<\/p>\n<h2>What breaks it<\/h2>\n<p><code>GetAnyOrder<\/code> returns any order to any authenticated caller. <code>GetOrder<\/code> compares <code>CustomerId<\/code> to <code>sub<\/code> (claim mapping is off in the wire-up above) and returns 403 when they differ.<\/p>\n<pre><code>static async Task&lt;IResult&gt; GetAnyOrder(Guid id, IOrders orders, CancellationToken ct)\n{\n    var order = await orders.Get(id, ct);\n    return order is null ? Results.NotFound() : Results.Ok(order);\n}<\/code><\/pre>\n<pre><code>static async Task&lt;IResult&gt; GetOrder(HttpContext http, Guid id, IOrders orders, CancellationToken ct)\n{\n    var order = await orders.Get(id, ct);\n    if (order is null)\n        return Results.NotFound();\n    var sub = http.User.FindFirstValue(\"sub\");\n    if (sub is null || order.CustomerId != sub)\n        return Results.Forbid();\n    return Results.Ok(order);\n}<\/code><\/pre>\n<h2>Pitfalls<\/h2>\n<ul>\n<li>Authentication without authorization: any logged-in user can hit <code>\/admin<\/code>.<\/li>\n<li>Failing open when the identity service is down.<\/li>\n<li>Using GET with a token in the query string (logs, Referer).<\/li>\n<li>Confusing 401 (no\/invalid credentials) with 403 (credentials fine, not allowed).<\/li>\n<\/ul>\n<p>See <a href=\"\/soft\/owasp-top-ten\/\">OWASP Top Ten<\/a> A07 Authentication Failures and A01 Broken Access Control.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>An HTTP API that can be called without proving who is calling is a public API, even if you \u201cdidn\u2019t [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[3],"tags":[],"class_list":["post-30","post","type-post","status-publish","format-standard","hentry","category-software-development"],"_links":{"self":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts\/30","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/comments?post=30"}],"version-history":[{"count":4,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts\/30\/revisions"}],"predecessor-version":[{"id":3257,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts\/30\/revisions\/3257"}],"wp:attachment":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/media?parent=30"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/categories?post=30"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/tags?post=30"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}