Ask an LLM to write a post for you and you get the same generic opener, the same three points, the same six emojis. Models are stateless β they start from zero every time, so they fall back on the average voice of the internet.
This tutorial shows how to fix that by giving an agent memory: past posts stored as vectors, a JSON profile of how you actually write, and a reflection loop that updates that profile as your style changes. All three live in one Oracle AI Database 26ai instance, so there is no pile of separate services to stitch together. The schema is 20 lines, the code is TypeScript end to end, and the full project is on GitHub.
AI agents are ideal for tackling well-scoped tasks so you can focus on larger projects, but they can also slow down your workflow. Most run on vendor servers, which means specialized software, API keys, and agent harnesses. Security and governance are outside your control.
Coder Agents is a chat interface and API for delegating research and development to agents inside your Coder deployment, so you can connect models and oversee agents in a single control plane you already trust.
The process is simple:
Describe what you want done in the chat
Coder Agents plans, provisions, and executes in the background
You review results, steer execution, and refine along the way
It does not matter how clean your architecture is or how fast your queries run - if an attacker can read another user's orders or forge a token, none of that matters.
Most API security problems come from skipping the basics:
Not updated NuGet package with security vulnerability
Missing validation check
Weak authentication setup
An over-permissive CORS policy
A secret committed to source control.
The good news is that ASP .NET Core gives you almost everything you need built in.
Over the years, I have shipped and reviewed many .NET APIs, and the same set of practices keeps them safe.
Here are 18 of them, each with the code to apply it.
In this post, we will explore:
Enforce HTTPS everywhere
Authenticate with tokens, not sessions
Validate the JWT signature, issuer, audience, and lifetime
Every request to your API should travel over an encrypted connection.
Without HTTPS, tokens, passwords, and personal data move in plain text, where anyone on the network path can read them.
Plain HTTP also opens the door to downgrade attacks, in which an attacker forces a client to use an insecure connection.
ASP .NET Core gives you two tools. UseHttpsRedirection redirects HTTP requests to HTTPS, and HSTS (HTTP Strict Transport Security) tells browsers to only ever connect over HTTPS:
UseHttpsRedirection upgrades any HTTP request to HTTPS.
UseHsts adds the Strict-Transport-Security header, so compliant browsers refuse to talk to your API over plain HTTP at all - which blocks downgrade attacks.
HSTS is skipped during development because you often use http://localhost.
Prefer stateless token authentication over server-side sessions.
A session stores authentication state in server memory or a shared store, which ties each user to a server and makes horizontal scaling harder.
A token carries its own proof of identity, so any instance of your API can validate it without a lookup.
For most APIs, that means JWT bearer tokens, often issued by an identity provider through OAuth 2.0 or OpenID Connect:
The client sends the token in the Authorization: Bearer <token> header on every request.
Your API validates it and reads the user's identity from the token's claims - no session store, no sticky sessions, no server-side state to scale.
This keeps your API stateless and easy to scale horizontally.
3. Validate the JWT Signature, Issuer, Audience, and Lifetime
Before accepting a token, you should trust it.
A token is only safe once you verify who issued it, who it is for, that it has not expired, and that its signature is valid.
Configure these checks explicitly through TokenValidationParameters rather than trusting framework defaults:
ValidateIssuerSigningKey confirms the signature - never turn this off, or anyone can forge a token.
ValidateIssuer and ValidateAudience ensure the token came from your identity provider and was meant for your API, not some other service.
ValidateLifetime rejects expired tokens.
Pay attention to ClockSkew.
It exists to tolerate small clock differences between servers, but its default is a generous 5 minutes, so an expired token can still be accepted for up to 5 extra minutes.
Reducing it to about 30 seconds, or even zero, tightens token expiry enforcement, as long as your servers' clocks are synchronized (with NTP).
Authentication tells you who the user is.
Authorization decides what they are allowed to do.
A bare [Authorize] only checks that the user is logged in.
Real rules - "only managers can delete orders", "users can only see their own data" - need policy-based and resource-based authorization.
When the rule depends on the specific resource - like "only the owner can edit this order" - use resource-based authorization with IAuthorizationService.AuthorizeAsync(user, order, "OrderOwner"), which evaluates the policy against the actual entity.
Policies keep authorization logic in one place, out of your endpoint bodies.
Give every client the minimum access it needs, and nothing more.
A token, a claim set, or an API key should grant exactly the permissions required for its job.
A mobile app that only reads the product catalog should not hold a token capable of deleting orders.
Scope this at the token level.
When your identity provider issues a token, it includes only the scopes that the client was granted, and your policies check for them:
The same idea applies to API keys and database accounts - scope them down too.
If a credential leaks, least privilege limits the blast radius to only what that one credential could do.
Treat every incoming payload as dangerous until proven valid.
Malformed or malicious input should be rejected at the edge, before it reaches your business logic or your database.
ASP .NET Core helps here: with the [ApiController] attribute, a request that fails model validation automatically returns a 400 with a problem-details body.
Add data annotations or, for richer rules, FluentValidation:
csharp
1publicclassCreateProductRequest2{3[Required]4[StringLength(200, MinimumLength =1)]5publicstring Name {get;set;}=string.Empty;67[Range(0.01,1_000_000)]8publicdecimal Price {get;set;}9}1011[ApiController]12[Route("api/products")]13publicclassProductsController:ControllerBase14{15[HttpPost]16publicIActionResultCreate(CreateProductRequest request)17{18// If we get here, the model is already valid19var product = _productService.Create(request);20returnCreatedAtAction(nameof(Get),new{ id = product.Id }, product);21}22}
The minimal API equivalent runs validation through a filter or explicitly:
Validating early turns a class of attacks - oversized strings, out-of-range numbers, missing fields - into a clean 400 instead of an exception deep in your code.
Never bind incoming JSON straight onto your database entity.
If clients post to your EF Core entity directly, they can set fields they should never control - IsAdmin, Balance, Status, or another user's Id.
This is called over-posting, or mass assignment.
Bind to a dedicated request DTO that exposes only the fields a client is allowed to set, then map it onto the entity yourself:
csharp
1// Request DTO - only what the client may set2publicrecordUpdateProductRequest(string Name,decimal Price);34[HttpPut("{id:int}")]5publicasyncTask<IActionResult>Update(int id,UpdateProductRequest request)6{7var product =await _dbContext.Products.FindAsync(id);8if(product isnull)9{10returnNotFound();11}1213// Map only the allowed fields14 product.Name = request.Name;15 product.Price = request.Price;1617await _dbContext.SaveChangesAsync();18returnNoContent();19}
The Product entity might also have CreatedAt, OwnerId, or IsFeatured fields, but because the client can only send Name and Price, those fields are inaccessible from the outside.
Separate request models require a small amount of typing and close a whole category of privilege-escalation bugs.
An endpoint that accepts JSON should reject other media types, and no endpoint should accept an unbounded request body - a multi-gigabyte upload is an easy way to exhaust server memory and take your API down.
Restrict the content type with [Consumes] and cap the body size with [RequestSizeLimit]:
The same discipline applies to the amount of data a client can pull back.
Always enforce pagination with a maximum page size on collection endpoints, so a client cannot request an unbounded result set:
csharp
1[HttpGet]2publicIActionResultGetProducts(int page =1,int pageSize =20)3{4// Clamp the page size so no one can ask for everything at once5 pageSize = Math.Min(pageSize,100);67var products = _productService.GetPage(page, pageSize);8returnOk(products);9}
Capping request size, content type, and page size all defend against the same thing: a single request that tries to consume more resources than your server can spare.
Never build SQL by concatenating strings together from user input.
String concatenation is how SQL injection happens - a crafted input like '; DROP TABLE Products; -- becomes part of your query.
Parameterized queries keep user input as data, never as executable SQL.
EF Core parameterizes everything by default, so a LINQ query is always safe:
When you do need raw SQL, use FromSql orFromSqlInterpolated, which turn the interpolated values into parameters rather than literal text:
csharp
1// 2. Safe - 'category' becomes a SQL parameter, not concatenated text2var products =await _dbContext.Products
3.FromSqlInterpolated($"SELECT * FROM Products WHERE Category = {category}")4.ToListAsync();56// 3. Dangerous - never do this7var sql ="SELECT * FROM Products WHERE Category = '"+ category +"'";89// 4. And never use FromSqlRaw with string interpolation10var products =await _dbContext.Products
11.FromSqlRaw($"SELECT * FROM Products WHERE Category = {category}")12.ToListAsync();
The first two are safe; 3 and 4 are an attacker's door.
Limit how often a single client can call your API.
Without limits, one client - or one attacker - can hammer your login endpoint with brute-force attempts, or flood your API with requests until it falls over.
ASP .NET Core has a built-in rate limiter you wire up in Program.cs:
Allow only the origins you trust to call your API from a browser.
Cross-Origin Resource Sharing (CORS) controls which web origins can make requests to your API.
The dangerous shortcut is AllowAnyOrigin(), which lets any website on the internet call your API on behalf of a logged-in user.
Define a named policy that lists exactly the origins, methods, and headers you allow:
An error response should help the caller, not the attacker.
A raw exception page leaks stack traces, framework versions, file paths, and SQL - a map of your internals.
Return a clean, standard error instead, using Problem Details (RFC 9457):
csharp
1builder.Services.AddProblemDetails();23var app = builder.Build();45if(app.Environment.IsDevelopment())6{7 app.UseDeveloperExceptionPage();8}9else10{11// Production: a clean ProblemDetails response, no stack trace12 app.UseExceptionHandler();13}
In production, an unhandled exception returns a structured Problem Details body with a status code and a generic message - and nothing about your internals.
Keep detailed diagnostics in your logs, where only you can see them, not in the HTTP response, where a client can.
A few response headers harden your API against common browser-side attacks.
Headers like Content-Security-Policy, X-Content-Type-Options, and Referrer-Policy tell the browser how to treat your responses - which scripts may run, whether to guess content types, how much referrer information to leak.
Add them with a small piece of middleware that runs on every response:
X-Content-Type-Options: nosniff stops the browser from guessing (and mis-running) content types.
Content-Security-Policy restricts where scripts, styles, and other resources may load from.
Referrer-Policy controls how much of your URL is sent to other sites.
X-Frame-Options: DENY prevents your responses from being embedded in a frame (clickjacking).
For a pure JSON API, these matter most when responses are rendered in a browser, but they are cheap insurance for any API.
Note: in production, consider using a maintained library or a reverse proxy to manage these headers and a stricter Content-Security-Policy tailored to your app.
Keys, connection strings, and tokens must never live in source control.
A secret committed to appsettings.json is a secret leaked - it stays in your Git history forever, visible to everyone with repository access.
Also, when using AI Agents, they can directly read your secrets from your configuration, unless you deny access for them. Be aware of this!
Cross-Site Request Forgery protection matters when you authenticate with cookies.
CSRF tricks a logged-in user's browser into sending an unwanted request, using the cookie the browser attaches automatically.
If your API authenticates with cookies, you need anti-forgery tokens:
Here is the important nuance: pure token-based APIs are largely immune to CSRF.
If your client sends a JWT in the Authorization header (not a cookie), the browser does not automatically attach it, so a forged cross-site request carries no credentials.
You mainly need antiforgery for cookie-authenticated endpoints - server-rendered apps and APIs that use cookie auth.
Match the protection to your authentication model: cookies need CSRF defense, bearer tokens generally do not.
16. Version Your API and Deprecate Insecure Endpoints
Versioning lets you retire a bad design without breaking every client at once.
When an endpoint turns out to be insecure or poorly shaped, you need a way to introduce a fixed version and phase out the old one on a schedule.
API versioning gives you that path.
Add the Asp.Versioning.Mvc package and mark the old version deprecated:
1[ApiController]2[ApiVersion("1.0", Deprecated =true)]3[ApiVersion("2.0")]4[Route("api/v{version:apiVersion}/orders")]5publicclassOrdersController:ControllerBase6{7// v2 fixes the insecure contract; v1 is marked deprecated8}
ReportApiVersions adds api-supported-versions and api-deprecated-versions headers to your responses, so clients see the deprecation coming and can migrate before you remove v1.
This turns "we cannot fix it because clients depend on it" into a managed migration.
Security events - failed logins, authorization denials, suspicious request patterns - need to be logged with enough context to reconstruct what happened.
Capture them as structured logs:
csharp
1[HttpPost("login")]2publicasyncTask<IActionResult>Login(LoginRequest request)3{4var result =await _authService.AuthenticateAsync(request);5if(!result.Succeeded)6{7 _logger.LogWarning(8"Failed login for {Email} from {IpAddress}",9 request.Email, HttpContext.Connection.RemoteIpAddress);1011returnUnauthorized();12}1314returnOk(result.Token);15}
Log authentication failures, authorization denials, and anything that appears to be probing.
Record enough context to support a real forensic investigation - which can include sensitive details where your threat model requires them, such as the account, the source IP, and the action attempted.
Because those logs can contain sensitive data, treat them as sensitive: store them where only authorized people can read them, and apply retention and access controls.
Most breaches exploit known vulnerabilities, not new ones.
Your API depends on dozens of NuGet packages and the .NET runtime, and each is a potential entry point when a security fix ships and you have not applied it.
Staying current is one of the highest-value things you can do.
.NET can list vulnerable packages for you:
bash
1dotnet list package --vulnerable
2dotnet list package --outdated
Run these regularly, and wire the scan into your CI pipeline so a known-vulnerable dependency fails the build:
bash
1# Include transitive dependencies - a vulnerability three packages deep is still yours2dotnet list package --vulnerable --include-transitive
Update your NuGet packages and the .NET runtime on a schedule, not only when something breaks.
Combined with a tool like Dependabot or GitHub security alerts, this closes the gap attackers rely on most.
Security is not a single feature you add at the end. It is a set of habits applied across the whole API.
Let's recap the key takeaways:
Secure the connection. Enforce HTTPS with redirection and HSTS, and add security headers so responses are hard to abuse.
Get authentication and authorization right. Use stateless tokens, validate every part of the JWT (including a tight ClockSkew), and authorize with policies and least privilege - not just [Authorize].
Treat all input as hostile. Validate and sanitize payloads, bind to request DTOs to stop over-posting, cap request and page size, and let EF Core parameterize your queries.
Defend against abuse. Rate-limit clients, restrict CORS to trusted origins, and apply CSRF protection wherever you use cookie authentication.
Do not leak information. Return minimal error detail with Problem Details, and keep secrets in a vault or environment, never in source control.
Operate securely over time. Version and deprecate insecure endpoints, log and audit security events into access-controlled storage, and keep your dependencies patched.
No single practice makes an API secure, and you do not have to apply all 18 at once.
Start with the ones that close your biggest gaps - usually HTTPS, token validation, authorization, and input validation - then work down the list.
I am building a deep-dive System Design course.
It will be better than anything you have seen so far:
Hope you find this newsletter useful. See you next time.
Whenever you're ready, here's how I can help you:
The .NET Senior Playbook is built to:
Fast-track you from junior or mid-level to senior
Keep you growing as a senior
Help you beat any .NET interview
Covers everything: C#, ASP.NET Core, EF Core, system design β answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.
Not sure where you stand? Take the free .NET Interview Run:
Find out your real level β Junior to Senior+
A realistic mock .NET interview β across 13 areas of C#, .NET, ASP.NET Core and System Design
No credit card required. When you finish, you get a personalized report: your level, your strongest and weakest areas, and where to focus next β the perfect way to benchmark yourself before diving into the Playbook.
Comments