A Deep Dive into JWT: How to Secure HTTP Endpoints and Refine Authentication
Web developers must ensure that only authorized individuals or systems can access specific HTTP endpoints, such as API URLs. This involves verifying both the identity of the request's originator and whether they have the necessary permissions. In this beginner-friendly guide, we will explore the common methods for securing HTTP endpoints—including API keys, session cookies, and OAuth2—and explain their pros and cons in simple terms. Then, we will take a deep dive into JSON Web Tokens (JWTs) , explaining what they are, how they work, and how they can be used to protect your endpoints. We will also cover the difference between access tokens and refresh tokens, how they fit into the authentication flow, best practices for using JWTs, and common security pitfalls to avoid.
Common Methods for Securing HTTP Endpoints
There are several widely used methods to authenticate the client or user calling your HTTP API. Each has its advantages and disadvantages:
API Keys
An API key is a simple secret token (often a long string) that a client includes in each request (e.g., in a query parameter or header) to identify itself. The server checks this key to decide whether to allow the request. It's like a single, secret password for an API client.
- Pros: API keys are simple to implement. There's no complex handshake process—the client just sends the key with every request, and the server validates it. This simplicity makes API keys popular for internal APIs or services where ease of use is important. Additionally, API keys can be issued to third-party developers to track usage of your API on a per-client basis.
- Cons: Security can be a concern. API keys are often long-lived credentials, so if a key is compromised (e.g., pushed to a public repository or intercepted in transit), an attacker can use it until it's manually revoked. Rotating (changing) keys without breaking clients is challenging. API keys often grant broad access (all or nothing), making it difficult to implement fine-grained permissions. They also don't represent a user on their own—all requests with the same key look the same, making it hard to audit individual user actions. Finally, if an API key is passed in a URL, it can appear in logs or browser history, increasing the risk of exposure.
Session Cookies
Session cookies are the traditional method used by websites. When a user logs in with a username/password, the server creates a session (storing user info in memory or a database) and sends the client a cookie containing a session ID. With each request, the browser automatically sends this cookie, and the server looks up the session to know who the user is.
- Pros: Session cookies are convenient for traditional web applications. The browser handles sending the cookie with every request to a domain, so the client (developer) doesn't need to manually attach a token. Sessions can store complex user state on the server, and you can easily revoke a session (e.g., log out) by clearing it on the server. Cookies can be set to
HttpOnly
(inaccessible to JavaScript), which mitigates some attacks like XSS. They are also scoped to a domain, which can simplify using the same login across subdomains. - Cons: This approach is stateful—the server must store and keep session data in sync, which can complicate scaling. In a load-balanced environment, a user might hit a different server that doesn't have their session, requiring a shared session store (a database or cache). Cookies are vulnerable to Cross-Site Request Forgery (CSRF) attacks if not configured properly, as the browser sends them automatically. Mitigations like the
SameSite
cookie attribute help, but strict settings can impact user experience (e.g., breaking cross-site logins). Furthermore, cookies are only effective in a browser context; they are less suitable for securing pure backend API-to-API communication. In fact, cookies are not ideal for stateless RESTful APIs—using tokens is preferable in that case. Finally, session cookies (like any authentication token) must be sent over HTTPS to prevent eavesdropping.
OAuth2 Tokens
OAuth2 is an industry-standard authorization protocol. It's a more complex but flexible way to secure endpoints. In OAuth2, a client doesn't hold a simple password; instead, it obtains an access token (and sometimes a refresh token, which we'll discuss later) from an authorization server. This token is then presented to the API (the resource server) as proof of authorization, usually via an HTTP header like Authorization: Bearer <token>
. OAuth2 is often used for third-party integrations and "Sign in with X" scenarios, but it's also used to secure first-party APIs in a robust way.
- Pros: OAuth2 is designed to solve many of the shortcomings of API keys and traditional sessions. Access tokens issued by an OAuth2 server are typically short-lived and can have specific scopes (permissions) attached. For example, a token might grant read-only access to an API, not full control. This limits the damage if a token is stolen and allows for the principle of least privilege. OAuth2 also supports token rotation and revocation strategies out of the box. Because the OAuth2 flow involves an authorization server, you can implement centralized authentication (e.g., your users can log in to a single auth service that issues tokens for various APIs). In short, OAuth2 tokens are superior in terms of authorization and security granularity. Many large platforms and identity providers implement OAuth2, so you can integrate with Google, Facebook, etc., without handling passwords directly.
- Cons: The main downside of OAuth2 is complexity. It's a framework with multiple flows (Authorization Code, Implicit, Client Credentials, etc.) that can be daunting for beginners. You need to deploy or use an authorization server and understand redirection-based logins or token endpoint calls. Implementing OAuth2 securely requires careful adherence to the specification. In short, it can be "harder to get right" than simpler methods. Additionally, debugging token issues or understanding the multi-step handshake can be more difficult than dealing with a single API key or session cookie. However, many modern libraries and services (Auth0, Okta, etc.) can handle the heavy lifting of OAuth for you.
JSON Web Tokens (JWT): What and Why?
Modern endpoint security often relies on JSON Web Tokens (JWTs) as the credential format, especially in token-based authentication schemes like OAuth2. A JWT is essentially a string that asserts some information ("claims") about a user or client and is digitally signed so that a server can verify it. Let's break this down further:
What is a JWT? A JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. It's "self-contained" because the token itself carries the data needed to identify the user and determine their permissions (the claims), and it's signed (with a secret or a key), so the server can verify that it hasn't been tampered with. A JWT consists of three parts: a header, a payload, and a signature, separated by dots (.
). For example, a JWT might look like xxxxx.yyyyy.zzzzz
, where the first part is the header, the second is the payload (both are Base64-encoded JSON), and the third is the signature.
- Header: Contains metadata, such as the token type (
JWT
) and the signing algorithm being used (e.g., HS256 or RS256). - Payload: Contains the claims—the information you actually care about (e.g., user ID, username, roles, and expiration time). There are standard claim fields (like
iss
for issuer,exp
for expiration time,sub
for subject, etc.), and you can include custom claims as needed. - Signature: This is the result of taking the header and payload and signing them (often with HMAC and a secret, or with RSA/ECDSA and a private key). This signature allows the server to later verify that the token was indeed issued by a trusted source (the party holding the key/secret) and that the payload has not been altered in transit.
Essentially, a JWT is like a tamper-proof, sealed envelope with user information inside: if someone changes the data inside, the signature check will fail, and the token will be rejected. And since the payload is JSON, you can put whatever information the server might need inside (like user roles or an expiration timestamp).
How are JWTs used to protect endpoints? Once a user logs in and obtains a JWT, the client will include that token in every protected request (usually in the HTTP Authorization
header, in the format Bearer <token>
). The server, on each request, will parse the JWT, verify the signature (using a secret or public key it trusts), and, if valid, use the information inside the token to identify the user and authorize the action. This is a stateless authentication method: the server doesn't need to keep session data, because the token itself carries the user's identity and roles/permissions. For example, an API endpoint could require a JWT to contain a claim "role":"admin"
; if the token doesn't have this claim (or the token is missing/invalid), the request will be rejected.
Pros of JWTs: JWTs combine some of the best of API keys and sessions while avoiding their downsides. They are stateless and self-contained, so a server can validate a request without having to do a database lookup every time. This makes JWTs great for scalable, microservice architectures and APIs (where each call is ideally independent and sends its credentials every time). JWTs can also embed user roles or scopes, allowing for fine-grained authorization without another lookup. Since JWTs are just strings, they can be used easily across different domains and on mobile or IoT devices (unlike cookies, which are tied to browsers). And because they are signed, a client cannot change its contents (e.g., escalate its privileges) without being detected by the server.
Cons of JWTs: JWTs are not a silver bullet. One major issue is security after theft—a JWT is a bearer token, meaning anyone who has it can use it. If an attacker gets your JWT, they can impersonate you until the token expires. Unlike server-side sessions, there's no easy way to immediately revoke an issued JWT (since the server has no record of it). A JWT is valid until its expiration—it cannot be invalidated or changed mid-flight by the issuer. (A workaround is to maintain a blacklist of compromised tokens on the server, but this reintroduces state and complexity.) Also, because JWTs can store data, there's a risk of trusting that data for too long. A token is issued at a point in time and can become stale—for example, if a user's role is changed or their account is revoked, an existing token may still have the old claims, allowing access when it shouldn't. For this reason, tokens should have a short lifetime. Another con: JWTs are larger than simple session IDs or API keys—they carry more data (often ~300 bytes or more), which is minor in most cases but still an overhead on every request. Finally, while JWTs are signed to prevent tampering, their payload is not encrypted by default. Anyone who intercepts or inspects the token (there are even websites dedicated to decoding them) can read its contents. This means sensitive data (like passwords or personal information) should not be put in JWT claims, unless you are encrypting the JWT payload.
Using JWTs: Access Tokens and Refresh Tokens
When securing an API with JWTs (especially via OAuth2 or similar), you'll typically deal with access tokens and refresh tokens. It's important to understand the difference between them and how they work together to balance security and usability.
- Access Token: This is the JWT that is typically used to access a protected resource. It's a short-lived token that proves the bearer has been authorized. For example, an access token might be valid for 5 minutes or 1 hour. The idea is that even if this token is compromised, an attacker can only use it for a short window of time. Access tokens are meant to be presented to the resource server (your API) and are usually included in the
Authorization
header, as discussed. The API validates the JWT, and if it's valid, processes the request. Access tokens should expire relatively quickly (minutes or hours) to limit risk and to allow the system to periodically require updates or re-verification of user permissions. - Refresh Token: As its name implies, a refresh token is used to "refresh" an access token. It is a separate credential that typically has a much longer lifetime (it might be valid for days, weeks, or months). A refresh token is never sent directly to a resource API. Instead, the client stores it securely, and when the access token expires, the client sends the refresh token to the authorization server (or auth service) to get a new access token (and sometimes a new refresh token). In OAuth2 terms, the client makes a request to the token endpoint with
grant_type=refresh_token
, along with the refresh token and its own credentials, and gets back a new access token. Refresh tokens allow a client to get new access tokens without prompting the user to log in again, providing a smoother user experience for long-lived sessions.
Why not just make the access token long-lived and skip the refresh token? The reason is security: if an access token is valid for, say, 30 days and it's compromised, an attacker has 30 days of access. By using a short-lived access token and a refresh token, you limit the attack window. If an access token is compromised, it expires quickly. If the refresh token is stored more securely (e.g., an HttpOnly cookie or secure storage) and is only ever sent to the auth server, it's less likely to be intercepted by an attacker. And even if a refresh token is stolen, some systems implement protections (like rotating refresh tokens and invalidating the old one upon use, IP/device checks, etc.) to mitigate abuse.
The Authentication Flow with Access and Refresh Tokens: When a user logs in (or authenticates via OAuth2), the server issues both an access token and a refresh token to the client. The client typically stores the access token in memory or short-term storage and the refresh token in more secure, long-term storage (since it's valid for longer).
The access token will expire after a short period. Let's say its lifetime is 15 minutes. The user continues to use the application, and after 15 minutes, the next API call fails (the server responds "unauthorized" or an error code indicating the token has expired). At this point, the client can automatically use the refresh token to get a new access token without bothering the user. It makes a background request to the auth server (e.g., a POST /auth/refresh
endpoint) with the refresh token. The auth server validates the refresh token (checking that it's valid and not expired or revoked). If everything is okay, it responds with a new access token (and often a new refresh token). The client then uses this new access token for future requests, and the cycle continues. If the refresh token itself has expired or is invalid, the client will need to have the user log in again.
A few points on refresh tokens: because they are powerful (anyone with a valid refresh token can keep getting new access tokens, potentially forever), effort must be made to protect them. Often, refresh tokens are stored in an HttpOnly cookie or are kept in a server-side session store, especially in browser-based applications, so that malicious JavaScript cannot steal them. Some frameworks employ refresh token rotation—each time a refresh token is used, the server issues a new one and invalidates the old one. That way, if an attacker steals a refresh token and tries to reuse it, the server will notice it's already been used and reject it, effectively logging out the thief. Additionally, refresh tokens often have a fixed lifetime or usage limit (e.g., a refresh token might expire after 30 days or a certain number of uses). All of these measures help mitigate the risk of a long-lived credential being abused.
Best Practices for Using JWTs
When implementing JWT-based security, keep the following best practices in mind to maintain a high level of security:
- Use Short Expiration Times: Always set an expiration (
exp
) claim on your JWT access tokens. A token should be valid for the minimum duration your application requires—usually minutes or hours, not days. Short-lived tokens limit the window of time an attacker has to abuse a stolen token. If you need longer sessions, use refresh tokens (which have their own expiration), not a single long-lived token. For example, an access token might expire in 15 minutes, and then you use a refresh token to extend the session as needed, rather than issuing a 24-hour token. This also forces periodic re-validation of user permissions. - Always Use HTTPS: Never send JWTs (or any sensitive token) over an unencrypted connection. Without HTTPS, an attacker can sniff network traffic and steal the token in transit. Ensure your application only transmits tokens over TLS/SSL. In fact, many JWT libraries will refuse to transmit tokens in an insecure context for this reason. Similarly, if using cookies to store or send tokens, mark them as
Secure
so they are only transmitted over HTTPS. - Secure Client-Side Storage: How you store tokens in your client-side application is critical. If you can, avoid storing JWTs in plain localStorage or other JavaScript-accessible locations, as this makes them vulnerable to XSS (Cross-Site Scripting) attacks—a malicious script on the page could read the token and exfiltrate it. For web applications, a better approach is to store the token in an HttpOnly cookie, which is not accessible to JavaScript. This prevents XSS from directly getting your token (though you'll have to defend against CSRF in that case). If you must store in localStorage or memory, be aware of the risks and mitigate XSS by sanitizing inputs, etc. For mobile apps, use a secure keystore/keychain. The key principle is to minimize the token's exposure to the client-side runtime. Additionally, never log tokens or expose them in error messages—treat them like passwords.
- Keep JWT Payloads Small and Non-Sensitive: Only include what's necessary for authorization in the token. Remember that while a JWT's signature prevents tampering, the token's payload is easily decoded and read. Do not put sensitive personal data or secrets in the token. For example, store a user ID, not their password. If you need to pass more user info to the client, fetch it from your API after authentication, rather than bloating the JWT. Smaller tokens are also more performant (remember it's sent with every request). If you have very sensitive info that must be transported in a token, consider using an encrypted JWT (JWE)—but this is a more advanced topic.
- Strong Signatures and Validation: Sign your JWTs with a strong algorithm. Common choices are HS256 (HMAC using SHA-256), which uses one secret key; or RS256 (RSA using SHA-256), which uses a private/public key pair. If using HS256, ensure the secret is sufficiently long and random (treat it like a password). If using RS256, keep your private key safe. On the server, always validate the signature and the standard claims of the token. Verify that
exp
(expiration) is in the future, check thatiss
(issuer) is your service, and, if applicable, checkaud
(audience) to ensure the token was intended for your API. Using a well-tested JWT library will handle most of these checks for you, but you must configure it with the right key/secret and expected values. Be careful with algorithms—avoid weak ones or no algorithm at all. (There was a known JWT attack where an attacker could bypass validation entirely if the server incorrectly allowedalg: "none"
.) - Implement Revocation (If You Need It): By design, stateless JWTs are not easily revokable. However, consider how you would handle it if you needed to revoke a token immediately (e.g., a user reports a stolen device). One strategy is to keep token lifetimes very short so that revocation is less critical (the token will expire on its own soon). Another approach is to maintain a blocklist of tokens or token IDs on the server side that are no longer valid. For example, you could include a token identifier (
jti
claim) and have a cache of revoked IDs. The API would check the signature and ensure thejti
is not in the revoked list. This introduces state and overhead, so it's a trade-off. If you use refresh tokens, you can effectively revoke a session by revoking the refresh token (since they can't get a new access token once theirs expires). Some identity systems also support a "log out everywhere" feature by tracking token validity in a central store (e.g., an OAuth server can revoke a token). In summary, plan for how you'll handle token revocation or abuse in your system design. - Use Refresh Tokens Strategically: If you use refresh tokens, guard them even more strictly than access tokens. A common best practice is to issue refresh tokens that are bound to a user/device and to rotate them on each use. Monitor refresh token usage on the server—if a refresh token is used multiple times (which may indicate it was copied), revoke it to prevent an attacker from using a stolen token. Also, set a reasonable absolute expiration for refresh tokens (e.g., a refresh token might be valid for 30 days, after which a re-login is required, even if access tokens were constantly refreshed during that time). This limits the period a stolen refresh token could be abused.
- Consider Secondary Validation: In high-security scenarios, you may want to add extra checks beyond the JWT itself. For example, some systems pass a one-time nonce or use a fingerprint of the client (IP address, user agent) when issuing a token and embed that info in the token claims. The server can then verify that the token is being used from the same client context. Another approach is to pair a JWT with a secure cookie as a form of confirmation. These are advanced techniques, but the point is: consider your threat model. For most applications, a standard JWT with HTTPS and short expirations is sufficient. But if you're protecting very sensitive data, layering in extra validation (device binding, rotating keys, etc.) can provide defense-in-depth.
Common JWT Security Pitfalls to Avoid
Finally, let's highlight some common mistakes or pitfalls when using JWTs to secure HTTP endpoints (and how to avoid them):
- Token Leakage: The worst thing that can happen to a token is for it to fall into unauthorized hands. As discussed, avoid practices that can lead to token leakage. This includes sending tokens over insecure channels (always use HTTPS), not storing them in plain text where scripts can read them (use HttpOnly cookies or secure storage), and not exposing them in URLs. Be mindful of browser developer tools and logs—do not log JWTs, and if you must pass them between application layers, treat them sensitively. A leaked JWT is like a leaked password; it can be used until it expires. Assume any token can be leaked and build in mitigations (short lifetimes, refresh tokens, etc.) to limit the damage.
- No Revocation Plan: Many novice developers switch to JWTs and remove server-side session tracking, only to realize later that they have no way to force a logout or revoke a compromised token before it expires. This is a design decision you should make early on. If you go fully stateless, you accept that you cannot immediately revoke a token—so you must rely on short expiration times. If that's unacceptable for your use case (e.g., you need to be able to disable a user's access instantly), then you need a token revocation strategy (like maintaining a blacklist or using a centralized auth service that can invalidate tokens). Not planning for this is a pitfall. In short, decide how you will invalidate a credential if you need to. For many apps, a short-lived token + refresh on login is sufficient (if revoked, the user is logged out after a short period). For others, consider a hybrid approach (most calls use a stateless JWT, but there's a revokable refresh token or a session record that can be killed in a database).
- Using JWTs for the Wrong Thing (JWTs for Everything): JWTs are powerful, but they are not always the simplest solution for every scenario. Sometimes, developers use JWTs where a simple session cookie would have been sufficient, adding unnecessary complexity. If your application is a traditional, server-side rendered website, maintaining a session might be easier and perfectly secure. JWTs shine in APIs, microservices, and when you need stateless, portable tokens (or third-party delegation via OAuth). But avoid forcing JWTs in a way that ignores their constraints. For example, don't use extremely long-lived JWTs as a replacement for a session—this combines the worst of both worlds (inflexible and insecure). Also, be careful not to confuse authentication with authorization: a JWT authenticates the token holder, but you still must enforce authorization (e.g., check the user's role in the JWT before allowing an action). A common mistake is assuming that the mere presence of a valid JWT means the user can do anything—you should still validate their permissions based on the claims.
- Ignoring Signature/Validation Basics: Another pitfall is misconfiguring a JWT library—for example, not actually validating the token signature or trusting a token signed with the wrong algorithm. Always ensure your server checks the signature against the expected key or public key. Do not accept a token signed with an unknown algorithm. Also, if you have multiple issuers (e.g., tokens from Google and your own system), validate the issuer field so you don't accept a token someone else issued to a different audience. Use a well-tested library and follow its documentation for secure usage. Never disable validation for "convenience" in development and then forget to re-enable it in production.
- CSRF from Cookies: If you choose to store JWTs in a cookie (a common and relatively secure approach, especially for web apps), remember that cookies are subject to CSRF attacks, as the browser sends them automatically. Your API (if it accepts cookie authentication) should implement CSRF protection (e.g., by requiring a CSRF token/header for state-changing requests, or by setting the
SameSite
attribute on the cookie). Alternatively, some architectures use cookies only for the refresh token and require the client to explicitly send the access token in a header, which splits the risk: the access token (JWT) is not sent automatically by the browser, avoiding CSRF on protected endpoints, while the refresh cookie is HttpOnly and is only used on a dedicated refresh endpoint (which can be CSRF-protected or set to SameSite). In short, be aware of the interplay between JWTs and web security mechanisms.
With JWTs and a sound authentication design, our APIs can be both secure and user-friendly, enabling authorized users to interact with your services while keeping bad actors out.
你可能也感兴趣
- Baidu URL Auto-Push Guide: A Deep Dive into SEO for Normal Indexing
- VS Code Automated Deployment to Linux: Non-Root User, SSH Keys, and Systemd in Practice — Boosting Efficiency & Security
- A Beginner's Guide to Multilingual SEO: Best Practices for Global Reach
- 深入浅出 JWT:如何保护 HTTP 端点并完善身份验证
- Five-Minute Guide to WinPE Network Boot: Tiny PXE Server + Wimboot