
Why the Self-Service Checkout Left the Door Open
A common question from the teams I mentor is why a well-run engineering shop still gets breached through an API endpoint nobody thought to defend. The 2023 attacks that exfiltrated millions of records through exposed, unauthenticated APIs were not exotic zero-days. They were the same five mistakes: a missing auth header, an endpoint that returned more fields than the app needed, a wildcard CORS policy, a logging gap, and a rate limit nobody enforced. Securing an API is boring work, and boring work is exactly what attackers depend on you skipping. This guide is about the fundamentals that actually stop those attacks, in the order they matter.

We are going to treat API security as a set of operational defaults rather than a list of buzzwords. Authentication tells the system who is calling. Authorization tells it what that caller may do. Input validation stops the payload from lying to you. Rate limiting stops abuse. Logging makes the first four auditable. If you implement these five cleanly, you cover the overwhelming majority of real-world API breaches, and you leave the exotic stuff for a dedicated threat model later.
Authentication: Do Not Roll Your Own
The first decision is which authentication mechanism to use, and here the answer has been stable for years. OAuth 2.0 with the Authorization Code flow is the default for third-party and end-user access, while API keys or machine-to-machine tokens handle server-to-server calls. Do not hand-roll a session system, do not invent your own token format, and do not build a homegrown password store. The mature libraries and the managed identity providers have already solved the cryptography and the storage problems; your job is configuration, not invention.

Where projects go wrong is in the details. A long-lived API key stored in the front-end JavaScript is as good as a publicly posted password. Tokens with no expiry, refresh tokens reused indefinitely, and secrets committed to a repository all count as critical vulnerabilities. If your API talks to a front-end at all, your client secret is exposed by definition, which means you need the PKCE extension and a proper authorization server. A short exploration of API development fundamentals will show why the authentication layer is the first thing to get right before you add more endpoints to the surface.
Authorization: Who Is Allowed to Do What
Authentication only verifies identity. It does not tell you whether that identity may read or write a given resource. The classic failure is an endpoint that authenticates the caller but then returns the full record regardless of ownership, a pattern behind countless "IDOR" (insecure direct object reference) breaches. You must check, on every request, that the authenticated user or service is permitted to perform the requested action on the requested object.

Two common authorization models cover most APIs. Role-based access control (RBAC) assigns permissions to roles, and roles to users, which is simple and sufficient for most internal tools. Attribute-based access control (ABAC) evaluates policies against attributes of the user, resource, and environment, which scales better when you have complex, dynamic rules such as "a user in region A may edit records belonging to team B only during business hours." Whichever you choose, enforce it in your service layer, never only in the front-end, because front-end checks are cosmetic to anyone who can call your endpoint directly.
Designing an API Schema That Does Not Leak
The shape of a REST endpoint leaks information even when it is "working." A response that returns an internal database ID, an error that distinguishes between "user not found" and "password incorrect," or a verbose stack trace all hand useful signals to an attacker. Design your schemas to return the minimum viable payload for each caller type, and keep resource identifiers opaque where they do not need to be human-readable.

The discipline carries over to how you structure the API itself. Batching fields a consumer does not need over a verbose endpoint is a gradual privacy erosion. Lists should paginate and allow field selection. Errors should be generic on the outside and detailed only in server logs. If you are designing the interface from scratch, the naming conventions and nesting patterns you choose early become hard to change, so it is worth reviewing GraphQL API design tradeoffs and whether a typed query language gives you safer, tighter responses than a one-size-fits-all REST endpoint.
| Platform / Tool | Key Features | Pricing |
|---|---|---|
| Auth0 | OAuth/OIDC, MFA, breach password detection, attack protection, rules engine | Free up to 7,500 active users; starts at $23/mo |
| Okta | Workforce and customer identity, SSO, adaptive MFA, lifecycle management | Contact sales; from ~$2/user/mo volume tiers |
| Keycloak | Open-source IAM, OIDC/SAML, user federation, fine-grained authorization policies | Free (open source); paid support optional |
| AWS WAF | Managed web-application firewall, rate-based rules, IP reputation, bot control | From ~$5/mo plus per-request charges |
| Cloudflare WAF | Managed rulesets, rate limiting, bot management, DDoS protection | Free tier; Pro from $20/mo |
| DataDome | Bot protection, AI-driven detection, real-time blocking, analytics | Custom pricing; often $200+/mo |
That table spans the two halves of defense: identity tooling upstream (Auth0, Okta, Keycloak) that decides who is allowed in, and edge protection downstream (AWS WAF, Cloudflare, DataDome) that stops abuse before it reaches your handlers. Teams at different scales pick different combinations, and there is no single best answer, only the one that fits your stack and threat model.
Zero-Trust, Rate Limiting, and Basic Abuse Protection
Even a perfectly designed API can be hammered into submission. Brute-force credential stuffing, scraping, and denial-of-service all follow a similar pattern: too many requests from an attacker who does not need to succeed quickly. Rate limiting is the countermeasure. Set sensible per-key and per-IP limits, and return Retry-After headers so well-behaved clients back off instead of retrying into a storm.

Zero-trust is less a product and more a posture: assume the network is hostile, require authentication and authorization on every call, even between internal services, and never trust the boundary as a substitute for per-request checks. On a zero-trust model, an internal service calling another internal service still receives a token, still goes through the same policy checks, and still appears in the same audit log. This removes the "trusted internal port" backdoor that has undone more than one architecture.
Securing Secrets and the Human Layer
Most credential exposure ultimately traces back to how secrets are stored and shared, not to a novel attack. Hard-coded keys in source, a .env file pushed to a public repo, a credential pasted into a chat that gets indexed, all of it is preventable with discipline. Use a secrets manager, rotate keys on a schedule, and run secret scanning in your CI so a commit with a live credential fails the build before it merges. The patterns that protect credentials generally also protect the endpoints that use them, and the same hygiene questions apply to anyone managing on the team.
The human layer extends past passwords. Phishing that hands over an API key or a session cookie defeats every technical control above, so treat security awareness as part of the deployment, the same way you treat documentation. And once access is granted, it should be revoked automatically on role change or departure; the API access review that never happens is how dormant credentials accumulate quietly for years.
Integrated Systems and the Logging You Cannot Skip
The more APIs you chain together, the bigger the attack surface and the harder it is to reconstruct what happened after an incident. When a webhook from one service triggers three downstream calls, you need correlation IDs threaded through every hop so a single request is traceable from entry to exit. Log the authenticated identity, the source IP (hash it if privacy requires), the action, the status, and the timing, without logging secrets or full payload bodies. The log should let you answer "who did what, when, and from where" for any request in seconds.
This is also where integration hygiene matters. A third-party connector that over-privileges its credentials, or a partner API that ignores your token's expiry, quietly widens the blast radius. Audit each integration for minimum privilege, and when you stand up a new connection, consider what a solid API integration plan requires: documented contracts, validated payloads, and a rollback path you can actually execute.
Storage, Transport, and Encryption Defaults
Encryption in transit should be non-negotiable. Terminate TLS at a modern version, pin nothing in a way that breaks rotation, and treat plain HTTP on production as a release blocker. At rest, the policy differs by data type: hashed and salted credentials, encrypted regulated fields, and never store secrets you do not need. If a database dump leaks, encryption at rest is your last line of defense, and it only helps if the keys are stored separately from the data. Anyone building on cloud infrastructure should read up on how changes how you think about bucket policies and access keys before they start.
Hashing, not encryption, is the correct treatment for passwords. Always use a memory-hard algorithm such as bcrypt, scrypt, or Argon2 with a unique salt per user, and never say "we encrypt passwords" when what you actually need is one-way hashing. The distinction matters because encrypted passwords can be decrypted, defeating the entire point of the protection.
Building Security Into Your Process, Not as an Afterthought
Security reviews that happen after the feature ships become rubber stamps. The reliable way to keep APIs secure over time is to bake checks into the workflow: threat-model new endpoints before they merge, scan dependencies for known CVEs as part of CI, run the OWASP API Security Top 10 as a standing checklist on each release, and treat a failed security test as a build failure, no exceptions. Add a vulnerability disclosure policy so researchers can report issues to you instead of selling them.
Finally, run an incident-response drill before you need one. Define who pages whom, how you cut off a suspected leaked key, and how you restore from backup. The teams that respond fastest to a breach are not the ones with the most exotic tools. They are the ones who already rehearsed the mundane steps of revoking a credential, checking the logs, and posting a status update. Implement the five defaults, document the process, and your API will be in the minority that survives contact with the internet.
For more, check out: and kubernetes security basics.
Frequently Asked Questions
Should I use JWT or opaque session tokens for my API authentication?
Use opaque, server-side session tokens for most end-user APIs because they are instantly revocable and impossible to replay after logout. JWT is better for stateless, distributed systems and machine-to-machine flows where you want no lookup on every call, but a leaked JWT cannot be revoked until it expires, so keep access tokens short-lived (minutes to an hour) and pair them with a refresh flow. Do not store sensitive data in the JWT payload itself; it is visible to anyone who decodes it.
What is the safest way to handle CORS on a public API?
Whitelist only the exact origins your own front-end needs, never use a wildcard on a credentialed API, and remember that CORS is a browser control, not a security boundary. Anyone with curl can call your API regardless of CORS settings, so treat CORS as browser UX, and enforce real authentication and authorization server-side. The risk of a wildcard CORS policy is that a malicious site your user visits can read your API's responses through their authenticated browser session.
How long should API keys and access tokens remain valid?
Short is better. Issue access tokens with expiries of 15 to 60 minutes and refresh tokens that rotate. For API keys used by services, set an explicit rotation schedule of 90 days or less, log the last-used time, and force rotation for any key that has been idle. Companies routinely over-issue long-lived keys and under-rotate them, which is why a single leaked key often grants months of access. Automatic expiry is your cheapest defense against keys you simply forgot to revoke.
How do I protect against attacks on my API once it is already built?
You can retrofit most of the fundamentals without a rewrite. Enforce rate limiting at a WAF or reverse proxy, add a secrets scanner to your CI, review your most sensitive endpoints for over-permissioning and excessive response fields, rotate any credentials you suspect leaked, and turn on audit logging for all authenticated requests. Those five moves close the majority of realistic gaps, and a proper threat model can tackle whatever remains without demanding a from-scratch rebuild.
Is HTTPS alone enough to secure my API from interception?
HTTPS protects data in transit against eavesdropping and tampering, and it is mandatory, but it is far from sufficient. It does nothing about a caller who is legitimately authenticated but over-privileged, a response that leaks internal fields, a brute-force login attempt, or a compromised client. Treat TLS as the floor, not the ceiling, and layer authentication, authorization, validation, rate limiting, and logging on top of it.