Recommended Free Tools
Use JWTs as one part of an API security design, not as the whole design. In Flask, a safe baseline is: verify credentials at login, issue a short-lived access token with Flask-JWT-Extended, require Authorization: Bearer ... on private routes, validate the token before reading its claims, enforce authorization for the requested resource, and serve every endpoint over HTTPS. Add refresh and revocation controls when your application needs sessions that can be ended before token expiry.
What a secure JWT flow looks like
A JSON Web Token (JWT) is a signed claim set. The client presents it; the API verifies its signature and registered claims, then uses the verified subject to make an authorization decision. A valid signature proves that the token was issued by a holder of your signing key. It does not prove that the caller may read a particular invoice, change another user’s profile, or perform an administrator action.
- The client sends credentials to a private login endpoint over HTTPS.
- The server looks up the account and verifies the submitted password with its password-hashing implementation.
- After successful authentication, the server creates an access token whose identity is a stable user identifier.
- The client sends that token in an
Authorization: Bearerheader. - Flask-JWT-Extended verifies the token before the view runs. The view obtains the verified identity and performs resource-level authorization.
- The API returns an appropriate success or error status without exposing secrets or raw token material.
Flask-JWT-Extended’s stable documentation is version 4.7.4; confirm current defaults when upgrading.
Install and configure Flask-JWT-Extended safely
python -m venv .venv
. .venv/bin/activate
pip install Flask Flask-JWT-Extended Werkzeug
export JWT_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
Keep the secret in a secret manager or environment-specific configuration, never in committed source. Anyone who obtains it can mint tokens your API accepts. Changing the key invalidates outstanding tokens, so plan key rotation and the resulting sign-in requirement.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errors#1 Best Overall
import os
from datetime import timedelta
from flask import Flask, jsonify, request
from flask_jwt_extended import (
JWTManager, create_access_token, get_jwt, get_jwt_identity,
jwt_required
)
from werkzeug.security import check_password_hash
app = Flask(__name__)
app.config.update(
JWT_SECRET_KEY=os.environ["JWT_SECRET_KEY"],
JWT_ACCESS_TOKEN_EXPIRES=timedelta(minutes=15),
JWT_TOKEN_LOCATION=["headers"],
JWT_ALGORITHM="HS256",
)
jwt = JWTManager(app)
# Replace this demonstration record with a database lookup.
USERS = {
"42": {
"password_hash": "",
"role": "user",
}
}
@app.post("/login")
def login():
data = request.get_json(silent=True) or {}
user_id = str(data.get("user_id", ""))
password = data.get("password", "")
user = USERS.get(user_id)
if not user or not check_password_hash(user["password_hash"], password):
return jsonify(error="invalid credentials"), 401
token = create_access_token(identity=user_id)
return jsonify(access_token=token, token_type="Bearer")
@app.get("/api/me")
@jwt_required()
def me():
user_id = get_jwt_identity()
return jsonify(user_id=user_id)
@app.get("/api/admin-report")
@jwt_required()
def admin_report():
user_id = get_jwt_identity()
user = USERS.get(str(user_id))
if not user or user.get("role") != "admin":
return jsonify(error="forbidden"), 403
return jsonify(report="restricted data")
if __name__ == "__main__":
app.run()
The hard-coded user map is only a runnable shape. In production, fetch the account from your database and use a password hash created by your chosen password-hashing system. Do not copy a documentation example that compares a literal username and password.
Issue tokens only after real authentication
Use a stable, non-sensitive subject such as an internal user ID. Do not use an email address if it can change, and do not put passwords, API keys, or private profile data in readable claims. JWT payloads are encoded, not encrypted; anyone holding a token can decode its claims.
Return the same generic login failure for an unknown account and a wrong password. That avoids making account enumeration easier. Apply rate limiting and monitoring at the login boundary, and never log the password or complete bearer token.
Protect routes and separate authentication from authorization
@jwt_required() makes token verification a prerequisite for the view. get_jwt_identity() returns the verified identity configured when the token was created. Keep public endpoints un-decorated intentionally; do not apply a broad catch-all that accidentally changes their contract.
Authorization belongs after authentication and must use the requested resource. For a route such as /api/orders/<order_id>, load that order and check its owner (or an explicit permission) before returning it. Never treat possession of a valid token as permission to access every record. OWASP’s REST guidance states that non-public REST services must perform access control at each API endpoint.
@app.get("/api/orders/<int:order_id>")
@jwt_required()
def get_order(order_id):
user_id = get_jwt_identity()
order = find_order(order_id) # database query
if order is None:
return jsonify(error="not found"), 404
if order.owner_id != int(user_id):
return jsonify(error="forbidden"), 403
return jsonify(order=order.to_dict())
Verify claims and algorithms before trusting token data
Do not decode an unverified token and use its sub, role, or email to make a decision. Let the configured JWT verifier check the signature, expiration (exp), not-before (nbf) and token type, then apply your policy. Configure an expected algorithm rather than accepting an algorithm named by an untrusted token header; reject unsecured tokens.
For deployments serving multiple issuers or audiences, configure and check the expected issuer and audience as well. Keep clock settings consistent across servers so a small time difference does not make valid tokens appear early or late.
Choose a transport: header by default, cookies conditionally
| Transport | Good fit | Required care |
|---|---|---|
Authorization: Bearer |
Mobile apps, command-line clients and services that explicitly attach credentials. | Use HTTPS, protect client storage, and never place the token in a URL. |
| Secure cookie | Browser applications that benefit from automatic cookie handling. | Use HTTPS cookie settings and retain CSRF protection for state-changing requests. Flask-JWT-Extended documents a double-submit CSRF pattern. |
| Query string | Not appropriate for ordinary access tokens. | URLs are commonly saved in browser history, proxy records and server logs, so tokens can leak. |
There is no universal answer for browser, mobile, service-to-service, or mixed clients. Choose storage and identity-provider architecture for the client threat model. With the header default, a request looks like:
curl https://api.example.test/api/me
-H "Authorization: Bearer ACCESS_TOKEN"
HTTPS is mandatory
Secure REST services must only provide HTTPS endpoints. TLS protects credentials and bearer tokens while in transit; it does not repair a leaked secret, an overly broad authorization rule, or unsafe client storage. Redirect HTTP to HTTPS at the edge, mark production cookies Secure, and ensure reverse proxies pass the original scheme correctly.
Expiry, refresh and revocation
Short-lived access tokens
Set an access-token lifetime appropriate to the application’s risk; the example uses 15 minutes. A shorter lifetime limits the useful window after theft, while increasing refresh traffic and user reauthentication complexity. Do not promise that expiry is a logout mechanism: a token remains usable until it expires unless the API checks a revocation state.
Refresh behavior
If you add refresh tokens, keep their endpoint and token type policy explicit, rotate or otherwise protect refresh credentials, and avoid accepting a refresh token where an access token is expected. Flask-JWT-Extended supports fresh and refresh-token requirements; preserve the default token-type verification unless a reviewed design requires a change.
Early invalidation with a blocklist
For logout, password reset, account suspension, or suspected theft, store the token’s unique jti identifier in a denylist until its natural expiry. Register a blocklist callback so every protected request checks that state:
Best Value
REVOKED = set() # Use durable, shared storage in production.
@jwt.token_in_blocklist_loader
def is_revoked(jwt_header, jwt_payload):
return jwt_payload["jti"] in REVOKED
@app.delete("/logout")
@jwt_required()
def logout():
REVOKED.add(get_jwt()["jti"])
return jsonify(message="token revoked"), 204
A process-local set disappears on restart and is not shared across workers; use a database or cache with an expiry matching the token.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Useful error handling and operational safeguards
- Return 401 when credentials are missing, malformed, expired or otherwise unauthenticated; return 403 after a valid identity fails an authorization check.
- Customize Flask-JWT-Extended error callbacks to return one consistent JSON shape, without echoing secrets or token contents.
- Keep secrets, password hashes and full Authorization headers out of logs. Redact them in reverse proxies, tracing and exception reporters.
- Use database constraints and transactions for permission changes, and re-check authorization on every mutation, not only on reads.
- Monitor repeated login failures, token-verification failures and unusual resource access, while avoiding sensitive payload capture.
Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Every protected route returns 401. | Missing Authorization header, wrong Bearer spelling, expired token, or a server with a different secret. |
Inspect the request shape and server configuration; obtain a fresh token and never paste it into logs. |
| Token works on one instance but not another. | Instances use different signing keys or incompatible JWT settings. | Load one managed secret and consistent algorithm/claim configuration everywhere. |
| Valid users receive 403. | Authentication succeeded but ownership, role or policy check failed. | Log a request ID and decision reason (not the token), then verify the resource authorization query. |
| Cookie authentication fails on POST or DELETE. | CSRF protection is enabled and the CSRF value is absent or mismatched. | Send the required double-submit CSRF value and keep HTTPS cookie settings; do not disable CSRF as a shortcut. |
| Logout does not stop an old token. | No revocation lookup is performed, so the token remains valid until expiry. | Persist its jti in a shared blocklist or shorten the access lifetime. |
| Claims appear correct but access is unsafe. | Application read claims before cryptographic verification or trusted a client-supplied role. | Use only claims returned by the verified extension context, then query current authorization state. |
Test the flow before deployment
- Log in with a valid account and confirm a token is returned over HTTPS.
- Call a protected route without a header and confirm a 401 response.
- Call it with the Bearer token and confirm the verified identity is used.
- Try another user’s resource and confirm the authorization check returns 403 (or your documented non-disclosure response).
- Test an expired token, a malformed token, a changed signing key and a revoked
jti. - Verify that logs, analytics, browser history and proxy access logs contain no bearer tokens.
Or skip the browser setup
If your goal is to capture your secured API’s documentation or status page rather than build a browser automation stack, ScreenshotNeo makes one request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for options such as full-page capture, device and viewport settings, custom headers and cookies, waiting rules, PDF output, signed links, asynchronous jobs and bulk capture. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can a JWT be encrypted so clients cannot read it?
A normal signed JWT is readable by its holder. Do not put secrets or sensitive personal data in its claims; use server-side data or an encryption design when confidentiality is required.
Should every endpoint use the same token lifetime?
No. Choose lifetimes by risk and client workflow, while keeping access tokens short enough to limit theft impact and defining refresh behavior explicitly.
What should a service do when a user’s role changes?
Check current authorization at request time and revoke relevant tokens when immediate invalidation is required; do not rely solely on a role claim created before the change.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




