Practical guide
How to secure an API with tokens
Validate the token reliably first, then decide separately what its holder is allowed to do.
First, the short version
A token is not the whole security model
Authentication verifies who sent a request. Authorization then decides whether that user or system may call a particular endpoint and access the requested object.
JWT is one token format, not an automatic security guarantee. The API must verify its signature, issuer, audience, and time validity, while HTTPS/TLS protects the entire transfer.
Get ready
What you need
Start by defining the trusted token issuer, intended audience, and required lifetime.
- A list of API clients, protected operations, and risks if a token is stolen.
- A trusted identity provider or an established library for issuing and validating tokens. Do not create your own cryptography.
- HTTPS in every environment that transfers real credentials or tokens.
- Secure storage for keys and secrets, plus a plan for rotation, revocation, and incident response.
Steps 1 to 3
Build protection in three layers
Token format, identity verification, and permission checks are three separate decisions.
1. Choose the token format and lifecycle
- A random opaque token stored on the server can suit an internal API. Use JWT when services need to validate signed claims locally without calling the issuer.
- Give access tokens a short expiry, usually measured in minutes. A long-lived session belongs in a separate, more strictly protected refresh mechanism.
- Put only necessary identifiers and permissions in a JWT, never passwords or secrets. Define at least issuer, audience, subject, expiry, and a unique token identifier.
- Use a proven library or identity provider. With asymmetric keys, publish only public keys, identify each key, and plan an overlapping rotation window.
composer require symfony/security-bundle Official Symfony access token documentation 2. Validate the token before application logic runs
- Accept access tokens in the standard Authorization: Bearer header. Do not put them in URLs, where they can enter history, analytics, or logs.
- For JWT, allow only expected algorithms and verify the signature, issuer, audience, exp, and nbf where applicable. Do not take an algorithm or key URL from an untrusted token.
- After validation, map the stable subject to an internal identity. A missing, blocked, or deleted account must not gain access merely because its token has not expired.
- Return 401 for a missing or invalid token. Log the reason and correlation ID, but never the complete token or sensitive claims.
3. Authorize every protected operation
- After authentication, decide using permissions, roles, and the specific object. In Symfony, put simple rules in access_control and domain decisions in voters.
- Deny by default. A new endpoint must not become public merely because nobody has added a rule yet.
- Check ownership and tenant boundaries too. The EDITOR role alone must not permit editing another customer's order or another organisation's data.
- Return 403 for a valid identity without the required permission. The client can then distinguish an invalid identity from insufficient access.
php bin/console debug:firewall Official Symfony voter documentation Step 4
Test the access boundaries
A successful request is not enough. Every rule needs a negative test as well.
-
Call the endpoint without a token
A protected endpoint should return 401 and must not run the application operation.
curl -i https://api.example.test/api/orders/42 -
Send a damaged or expired token
The API must reject it regardless of the roles listed inside the unverified token.
curl -i https://api.example.test/api/orders/42 -H 'Authorization: Bearer invalid-token' -
Check insufficient permission
A valid token without permission for the object should receive 403. Repeat the same test across tenant boundaries.
curl -i -X DELETE https://api.example.test/api/orders/42 -H 'Authorization: Bearer ACCESS_TOKEN'
If something goes wrong
Common problems
A valid token returns 401
Check the issuer, audience, selected key, and server clock. Configure a small clock tolerance explicitly, but do not use it to extend token lifetime unnecessarily.
date -u The user is signed in but the operation returns 403
Authentication succeeded. Look for a missing permission, access_control rule, voter, or object ownership check.
php bin/console debug:firewall A signed-out user can still use a JWT
A signed JWT remains statelessly valid until expiry. Use a short lifetime and, when immediate removal is required, check a revocation list by jti or the account state.
A token appeared in a log or client storage
Revoke it immediately, rotate affected secrets, and stop logging the Authorization header. In browsers, consider an HttpOnly, Secure, and SameSite cookie with suitable CSRF protection; use a secret manager on servers and operating-system secure storage on mobile devices.
Done
The API distinguishes identity from permission.
Authorization now follows verified identity and protects each operation separately. Regularly test expiry, key rotation, revocation, and access to objects owned by someone else.