Step 1 — Confirm It's Actually 403
Open DevTools → Network → find the failing request → check the status code. A 403 response body often contains a hint:
{"error": "forbidden", "message": "Admin access required"} Step 2 — Is It Authentication or Authorization?
| Symptom | Likely cause | Fix |
|---|---|---|
| No token sent | Missing auth header | Add Authorization: Bearer <token> |
| Token expired | JWT/session expired | Refresh token or re-login |
| Wrong token scope | Token lacks required permission | Request token with correct scopes |
| Correct token, still 403 | Authorization failure | User lacks the required role/permission |
Quick test: Try the same request with an admin token. If it works, it's an authorization (role/permission) issue, not authentication.
Step 3 — Common Causes
Missing or insufficient role
The most common cause. The user is authenticated but their role doesn't include the required permission.
// Express.js — check role before proceeding app.delete('/api/posts/:id', requireAuth, (req, res) => { if (req.user.role !== 'admin' && req.user.id !== post.authorId) { return res.status(403).json({ error: 'Forbidden' }) } // proceed with deletion })CSRF token missing or invalid
Form submissions and state-changing requests often require a CSRF token. If it's missing or expired, the server returns 403. Ensure your form includes the CSRF token and that it matches the server-side session.
IP allowlist / rate limiting
Some APIs restrict access by IP address. If your IP is not in the allowlist, you get 403. Check with the API provider.
File system permissions (server-side)
On Linux servers, if the web server process (nginx, Apache) doesn't have read permission on a file, it returns 403.
# Check file permissions ls -la /var/www/html/file.txt # Fix: give web server read access chmod 644 /var/www/html/file.txt chown www-data:www-data /var/www/html/file.txtAPI key missing required scopes
OAuth tokens and API keys often have scopes. A token with
read:usersscope cannot performdelete:users.function requireScope(scope) { return (req, res, next) => { if (!req.token.scopes.includes(scope)) { return res.status(403).json({ error: 'insufficient_scope', required: scope }) } next() } }Step 4 — Checklist
- Is the user authenticated? (If not, it should be 401, not 403)
- Does the user have the required role or permission?
- Is a CSRF token required and present?
- Is the request coming from an allowed IP?
- Does the API token have the required scopes?
- Are file/directory permissions correct on the server?
- Is the resource behind a feature flag that's disabled for this user?