Understanding API Authentication - OAuth, API Keys, and JWTs Explained
Authentication is the front door to every API. Get it right and your system is secure, scalable, and pleasant to integrate with. Get it wrong and you are one leaked credential away from a data breach that makes the news.
Despite this, authentication remains one of the most misunderstood aspects of API development. Teams pick the wrong method for their use case, implement the right method incorrectly, or mix up authentication (who are you?) with authorisation (what are you allowed to do?) - and end up with systems that are either insecure, unnecessarily complex, or both.
This guide breaks down the three most common API authentication methods - API keys, OAuth 2.0, and JSON Web Tokens - explains when each one is the right choice, and highlights the real-world pitfalls that catch developers out in production.
API Keys - Simple, but not as safe as you think
API keys are the simplest form of API authentication. A static string, usually passed in a request header or query parameter, that identifies the calling application. Most developers encounter API keys early in their careers, and for good reason - they are straightforward to implement and easy to understand.
A typical API key flow works like this: the server generates a unique key for each client application, the client includes that key with every request, and the server validates it before processing the request. In Laravel, you might implement this as middleware that checks for a valid key in the X-API-Key header against a hashed value stored in your database.
API keys work well for server-to-server communication where you control both ends of the connection. Internal microservices talking to each other, third-party services calling your webhook endpoints, or backend systems pulling data from an external provider - these are all reasonable use cases for API keys.
However, API keys have significant limitations that developers frequently underestimate. First, they identify applications, not users. An API key tells you which system is making the request, but it cannot distinguish between different users of that system. If you need per-user access control, API keys alone will not get you there.
Second, API keys are bearer tokens in the truest sense - anyone who possesses the key has full access. There is no additional verification step. If the key is leaked through a Git commit, a client-side JavaScript bundle, server logs, or a Slack message, an attacker has immediate access to everything that key permits.
Third, most API key implementations lack fine-grained scope control. The key either works or it does not. More sophisticated implementations allow you to assign permissions to individual keys, but this adds complexity that starts to approach what OAuth provides out of the box.
Common API key mistakes
The most frequent mistake we see is API keys exposed in frontend code. If your React or Next.js application includes an API key in client-side JavaScript, every user of your application can see that key in their browser's developer tools. This applies to environment variables prefixed with NEXT_PUBLIC_ in Next.js or REACT_APP_ in Create React App - these are bundled into the client-side build and are not secret.
The second most common issue is committing API keys to version control. Even if you remove the key in a subsequent commit, it remains in the Git history indefinitely. Tools like truffleHog and git-secrets can help catch this before it happens, but the safest approach is to never put keys in code files at all. Use environment variables and a .env file that is listed in your .gitignore.
Finally, many teams fail to implement key rotation. API keys should be rotatable without downtime. This means your system needs to support multiple active keys per client, with the ability to deprecate old keys gracefully. In Laravel, this is straightforward to implement using a api_keys table with an expires_at column and a scheduled command that notifies clients before expiry.
OAuth 2.0 - The industry standard for delegated authorisation
OAuth 2.0 is the protocol that powers "Sign in with Google", "Connect your GitHub account", and virtually every third-party integration you have ever used. It solves a specific problem: allowing a user to grant a third-party application limited access to their resources without sharing their credentials.
The core concept is delegation. Rather than giving an application your username and password, you authenticate directly with the resource provider (Google, GitHub, your own auth server) and then grant the application a token with specific permissions. The application never sees your password, and you can revoke its access at any time without changing your credentials.
OAuth 2.0 defines several grant types for different scenarios. The Authorization Code grant is the most common and most secure for web applications. It involves redirecting the user to the auth server, the user authenticating and granting consent, the auth server redirecting back with a short-lived code, and your server exchanging that code for an access token. The critical security feature here is that the access token is never exposed to the user's browser - the exchange happens server-to-server.
The Authorization Code with PKCE (Proof Key for Code Exchange) grant extends the standard flow for single-page applications and mobile apps where you cannot securely store a client secret. PKCE adds a cryptographic challenge that prevents an attacker from intercepting the authorization code and exchanging it for a token. This is now the recommended approach for all public clients.
The Client Credentials grant is designed for machine-to-machine communication where there is no user involved. Your backend service authenticates with the auth server using its client ID and secret, and receives an access token. This is conceptually similar to API keys but with automatic token expiry and rotation built into the protocol.
For Laravel applications, Laravel Passport provides a full OAuth 2.0 server implementation. If you only need first-party token authentication without the full OAuth ceremony, Laravel Sanctum is a lighter-weight alternative that handles SPA authentication and simple API token management without the overhead of a complete OAuth server.
When OAuth is overkill
OAuth 2.0 is powerful, but it is also complex. If you are building an API that will only be consumed by your own frontend application - a React or Next.js client talking to your Laravel API - you probably do not need a full OAuth implementation. Laravel Sanctum's SPA authentication, which uses cookie-based session authentication behind the scenes, is simpler, more secure for this use case, and avoids the token management complexity entirely.
Similarly, if your API is purely internal - microservices communicating within a private network - the overhead of OAuth token negotiation may not be justified. API keys with IP allowlisting and mutual TLS can provide equivalent security with less complexity.
The sweet spot for OAuth is when third parties need access to user data, when you are building a platform that other developers will integrate with, or when you need standardised, revocable, scoped access to resources across multiple services.
JSON Web Tokens - Stateless authentication with trade-offs
JSON Web Tokens (JWTs) are not an authentication protocol - they are a token format. This distinction matters because JWTs are frequently used as part of OAuth 2.0 flows, but they can also be used independently. A JWT is a self-contained, digitally signed token that carries a set of claims (key-value pairs) about the user or client.
A JWT consists of three parts separated by dots: a header (specifying the signing algorithm), a payload (containing the claims), and a signature (proving the token has not been tampered with). The header and payload are Base64URL-encoded JSON objects - they are encoded, not encrypted. Anyone who intercepts a JWT can read its contents. The signature ensures integrity, not confidentiality.
The main advantage of JWTs is statelessness. Because the token itself contains all the information the server needs to authenticate and authorise the request, there is no need to query a database or session store on every request. This is particularly valuable in microservice architectures where multiple services need to verify authentication independently. Each service can validate the JWT's signature using a shared secret or public key without making a network call to a central auth service.
In Laravel, the tymon/jwt-auth package provides straightforward JWT authentication. You issue a token on login, the client includes it in the Authorization: Bearer header on subsequent requests, and the middleware validates the signature and extracts the user claims.
The JWT pitfalls that catch everyone
JWTs have a reputation for being misused, and that reputation is largely deserved. Here are the issues we see most frequently.
Storing sensitive data in the payload. Because the JWT payload is only Base64-encoded, not encrypted, any data in the payload is readable by anyone who intercepts the token. Do not put email addresses, permissions lists, or any personal data in the payload unless you are also encrypting the token (JWE). Stick to a user ID and minimal claims.
No easy revocation. Once a JWT is issued, it is valid until it expires. If a user logs out, changes their password, or has their account compromised, you cannot invalidate existing tokens without additional infrastructure. Common workarounds include short expiry times (15 minutes) combined with refresh tokens, or maintaining a token blocklist - but the blocklist approach negates the stateless advantage that made JWTs attractive in the first place.
Using the none algorithm. The JWT specification allows a none algorithm, meaning the token has no signature. Some poorly implemented libraries accept unsigned tokens if the algorithm header says none, allowing an attacker to forge arbitrary tokens. Always validate that your library rejects the none algorithm and explicitly specify the expected algorithm when verifying tokens.
Oversized tokens. Because JWTs are sent with every request, large payloads add overhead. We have seen tokens exceeding 4KB because developers packed entire permission trees into the claims. This creates performance problems and can even exceed cookie size limits if you are storing tokens in cookies. Keep your JWT payload small and look up additional permissions server-side when needed.
Choosing the right approach for your project
In practice, most production APIs use a combination of these methods rather than relying on a single approach. Here is a practical decision framework:
Server-to-server integrations with trusted partners: API keys are appropriate here. The communication happens entirely on the backend, you control who receives the keys, and the simplicity outweighs the limitations. Add IP allowlisting and key rotation for additional security.
User-facing applications with a first-party frontend: If your React or Next.js frontend only talks to your own Laravel API, use Laravel Sanctum with SPA authentication. It handles CSRF protection, cookie-based sessions, and does not require you to manage tokens in JavaScript at all.
Third-party integrations or platform APIs: OAuth 2.0 with the Authorization Code grant (and PKCE for public clients) is the standard choice. It gives third parties scoped, revocable access without exposing user credentials.
Microservice architectures: JWTs work well here as the token format, typically issued by an OAuth 2.0 auth server. Each microservice can validate the token independently without a central session store. Use short-lived access tokens with refresh tokens to balance security with usability.
Mobile applications: OAuth 2.0 with PKCE is the recommended approach. The mobile app redirects to the system browser for authentication (not an embedded web view), receives an authorization code, and exchanges it for tokens. Avoid storing long-lived tokens on the device.
Security fundamentals that apply to every method
Regardless of which authentication method you choose, certain security practices are non-negotiable.
Always use HTTPS. Every authentication method relies on the secrecy of tokens or credentials in transit. Without TLS, any of these approaches can be defeated by a network-level attacker.
Implement rate limiting on authentication endpoints. Brute force attacks against login endpoints and token exchange flows are common. Laravel's built-in ThrottleRequests middleware provides a solid starting point, but consider more sophisticated approaches like exponential backoff for repeated failures from the same IP.
Log authentication events. Every successful login, failed attempt, token refresh, and key rotation should be logged with enough context to investigate incidents. But do not log the tokens or credentials themselves - log identifiers and metadata only.
Rotate credentials regularly. API keys should have expiry dates. OAuth client secrets should be rotatable without downtime. JWT signing keys should be rotated on a schedule, with both old and new keys accepted during the transition window.
Getting authentication right matters
Authentication is not a feature you bolt on at the end of development. It is a foundational architectural decision that affects your security posture, your developer experience, and your ability to integrate with the wider ecosystem. Choosing the right method for your specific use case - and implementing it correctly - is worth the upfront investment.
At The API Guys, we build APIs with Laravel every day, and authentication is always one of the first conversations we have with clients. Whether you are securing a new API, migrating from an insecure legacy system, or building a platform that third-party developers will integrate with, we can help you get it right from the start.
