Most API security failures we've seen in the wild aren't the result of some exotic attack. They're the result of ordinary, well-understood mistakes shipped under deadline pressure: an admin endpoint left reachable without a role check, a webhook handler that trusts whatever JSON arrives on the wire, a login route with no rate limit at all. None of this requires cutting-edge cryptography to fix. It requires treating a handful of boring practices as non-negotiable rather than optional polish added "later."
Token-Based Authentication: Bearer Tokens vs. Session Cookies
Session cookies work well when the browser and the server are the only two parties involved — the browser stores a cookie, the server maintains session state, and CSRF protections handle the main risk that model introduces. But a SaaS API is rarely talked to only by a browser. Mobile apps, third-party integrations, server-to-server clients, and CLI tools all need a way to authenticate that doesn't depend on cookie jars or a shared browser context.
That's what bearer tokens are for. In a Laravel API, Sanctum-style personal access tokens are a practical middle ground: a client authenticates once, receives an opaque token, and sends it as an Authorization: Bearer header on every subsequent request. The server looks the token up, confirms it's valid and unexpired, and identifies the user or application it belongs to. There's no session state to synchronize across servers, which also makes this model naturally friendly to horizontally scaled, stateless API deployments.
The practical rule of thumb: use session-based auth for your own first-party web frontend talking to its own backend on the same domain, and use tokens for anything else — mobile clients, public API consumers, integrations, and server-to-server calls. Trying to force one model to cover both cases usually ends up compromising the security properties of both.
Why Short-Lived, Rotatable Tokens Beat Static API Keys
A static API key that never expires is convenient right up until it leaks — and it will leak, eventually, through a committed .env file, a client-side JavaScript bundle, a support ticket screenshot, or a log line that captured a request header it shouldn't have. Once that happens, a key with no expiry and no rotation mechanism is valid indefinitely, and you may have no reliable way to know it's been compromised until you see the abuse.
The fix isn't complicated, just disciplined:
- Give tokens an expiry. Short-lived access tokens paired with a separate refresh flow limit how long a leaked token remains useful.
- Make revocation instant. A token should be checkable against a database or cache the moment you need to kill it — not something baked into a self-contained value you can't invalidate early.
- Rotate on suspicion, not just on schedule. Any sign of anomalous use should be enough to revoke and reissue immediately, without waiting for a scheduled rotation window.
- Never log full tokens. Log a token's ID or a truncated, non-reversible reference, never the credential itself.
This connects directly to the licensing model we cover in how software licensing works — the same "server holds the decision, client holds a short-lived, revocable answer" principle applies to API tokens just as much as it does to license validation.
Rate Limiting and Throttling
Rate limits are frequently treated as a performance safeguard and left off the endpoints that need them most: authentication. A login route with no throttle is an open invitation to credential stuffing — attackers running lists of leaked username/password pairs from other breaches against your login form, hoping for reuse. Password reset and OTP endpoints deserve the same treatment for the same reason.
Laravel's throttle middleware makes this cheap to apply, and it's worth layering more than one dimension of limiting rather than relying on a single global number:
- Per-IP limits to slow down single-source abuse
- Per-account limits to stop targeted attacks against one user regardless of source IP
- Per-token limits on authenticated endpoints, tuned to a legitimate client's real usage pattern
- Tighter, separate limits specifically on auth, password-reset, and OTP routes rather than lumping them in with general API traffic
Return a proper 429 Too Many Requests with a Retry-After header rather than a generic error, so well-behaved clients can back off correctly instead of retrying immediately and making the problem worse.
Verifying Webhook Signatures
If your SaaS integrates with Stripe, PayPal, or any other third-party service that pushes events to you via webhook, that endpoint is a public URL accepting POST requests from the internet. Nothing stops anyone from sending a request to it that looks like a legitimate "payment succeeded" event. The only thing standing between that and a forged event silently marking someone's account as paid is signature verification.
Both Stripe and PayPal sign their webhook payloads with a secret only you and they know, and both provide SDK helpers to verify that signature against the raw request body — not the parsed JSON, the raw bytes, since re-serializing JSON can change byte-for-byte content and break the signature check. A minimal correct flow:
- Read the raw request body without letting a framework middleware parse and re-encode it first.
- Compute or verify the signature using the provider's SDK and your webhook secret.
- Reject the request outright if verification fails — don't process it "just in case."
- Check the event timestamp against a reasonable window to guard against replay of an old, previously valid signed payload.
- Make handlers idempotent, since providers retry webhook delivery and the same event ID can arrive more than once.
Input Validation at the Boundary
Frontend validation is a user-experience feature, not a security control — anyone can bypass it entirely and send whatever they want directly to your API. Every field, type, and range needs to be validated again on the server, at the point where the request first enters your application. In Laravel this is what Form Requests are for: explicit rules for every field, applied before a controller ever sees the data, with a whitelist approach — only the fields you explicitly expect should be accepted and mass-assigned, never whatever the client happened to send. This matters for correctness as much as security; it's the same discipline we recommend when designing a REST API that scales, since APIs that skip boundary validation tend to accumulate exactly the kind of inconsistent data that makes scaling harder later.
Principle of Least Privilege for API Scopes
Not every token needs to be able to do everything the API can do. Sanctum's token abilities (and equivalent scope systems elsewhere) let you issue a token that can only read order data, for instance, without also granting it the power to issue refunds or change account settings. This matters most for third-party integrations and machine-to-machine tokens, where the blast radius of a leaked credential should be limited to exactly what that integration needs — nothing more.
The same principle applies to admin capability. A support tool that only needs to view customer records should authenticate with a token scoped to read-only access, not with the same full-privilege credential your billing system uses internally. Scoping tokens this tightly takes a bit more upfront design, but it directly limits how much damage any single compromised credential can do.
Logging and Audit Trails
Sensitive actions — permission changes, plan upgrades or downgrades, payment method changes, refunds, account deletions, admin impersonation — should leave a durable, append-only record of who did what and when, separate from your general application logs. When something goes wrong, the difference between "we can reconstruct exactly what happened" and "we're guessing" usually comes down entirely to whether this logging existed before the incident, not whether it gets added afterward. It doesn't need to be elaborate: a table recording actor, action, target, timestamp, and relevant before/after state covers most needs, as long as it's populated consistently for every sensitive action rather than added selectively.
CORS and Public vs. Authenticated Surfaces
A related, often overlooked piece: CORS configuration is a security boundary, not just a browser-compatibility setting to get out of the way with a wildcard. An API that sets Access-Control-Allow-Origin: * on authenticated, cookie-based endpoints effectively invites any website to make credentialed requests on behalf of a logged-in user's browser session. For token-authenticated endpoints this matters less, since the token itself has to be attached explicitly by the calling code rather than sent automatically by the browser the way a cookie is — but it's still worth being deliberate about which origins are allowed, rather than defaulting to permissive because it made a frontend integration easier to get working during development and nobody revisited it before shipping.
Common Real Mistakes We See
- Admin or internal endpoints reachable on the public API surface. Internal tooling routes should live behind separate middleware, a separate domain, or network-level restrictions — not just an assumption that nobody will find the URL.
- Weak or reused tokens. Tokens generated with predictable patterns, or the same token shared across multiple integrations "to keep things simple," turn one leak into many.
- No rate limits on authentication endpoints specifically. General API throttling doesn't help if login, password reset, and OTP verification are left unthrottled.
- Trusting client-supplied IDs without an ownership check. An endpoint like /invoices/{id} that returns any invoice matching that ID, rather than checking it belongs to the authenticated user, is a textbook insecure direct object reference.
- Verbose error responses in production. Stack traces and internal exception messages returned to the client hand an attacker a map of your internals for free.
None of this is exotic, and that's the point — API security for a SaaS platform is mostly about consistently applying a short list of well-understood practices rather than finding some clever trick. It's the same set of principles we build into every Laravel API we ship, whether that's for our own products or client platforms; if you're weighing your stack for a new build, our comparison of Laravel vs. Next.js is a reasonable starting point, and we're happy to talk through the specifics of an existing API you're trying to harden.