affaan-m/ECC · error
forbidden
forbidden
Error message
forbidden
What it means
Ownership branch of the api-design skill's authorization example: the order lookup succeeded, but order.userId !== req.user.id, so the handler returns 403 { code: 'forbidden' }. It means the caller is authenticated, the resource exists, but the authenticated identity is not its owner — as opposed to the 404 returned when the order does not exist at all.
Source
Thrown at skills/api-design/SKILL.md:317
```
# Bearer token in Authorization header
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# API key (for server-to-server)
GET /api/v1/data
X-API-Key: sk_live_abc123
```
### Authorization Patterns
```typescript
// Resource-level: check ownership
app.get("/api/v1/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: { code: "not_found" } });
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
return res.json({ data: order });
});
// Role-based: check permissions
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
await User.delete(req.params.id);
return res.status(204).send();
});
```
## Rate Limiting
### Headers
```
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95View on GitHub (pinned to d8409a4b08)
Solutions
- Confirm which identity the request is authenticated as (decode the token / log req.user.id) and that it owns the resource
- If cross-user access is legitimately required, route through an admin-authorized endpoint (the requireRole('admin') pattern) instead of the owner-scoped one
- Fix type/format mismatches in the ownership comparison (compare both sides as strings, or normalize UUIDs)
- If resource existence should be hidden from non-owners, return 404 instead of 403 for strangers
Example fix
// before - type mismatch makes every request 'forbidden'
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: 'forbidden' } })
// order.userId is 42 (number), req.user.id is '42' (string) -> always 403
// after - normalize before comparing
if (String(order.userId) !== String(req.user.id)) {
return res.status(403).json({ error: { code: 'forbidden' } })
} Defensive patterns
Strategy: type-guard
Type guard
// Normalize then compare ownership — guards against the classic number-vs-string mismatch const isOwner = (resourceUserId: string | number, authenticatedUserId: string | number): boolean => String(resourceUserId).trim().toLowerCase() === String(authenticatedUserId).trim().toLowerCase()
Try / catch
// Handler: keep 404 before 403 so non-existence is checked first, ownership second
const order = await Order.findById(req.params.id)
if (!order) return res.status(404).json({ error: { code: 'not_found' } })
if (!isOwner(order.userId, req.user.id)) {
return res.status(403).json({ error: { code: 'forbidden' } })
} Prevention
- Normalize both sides of ownership comparisons to one type/case before comparing
- Confirm which identity the token represents (decode it in a request log) before assuming an ownership bug
- Route legitimately cross-user access through role-guarded endpoints rather than loosening the owner check
- Make seed/test data create resources under the same user the tests authenticate as
When it happens
Trigger: User A requests user B's order id; the bearer token belongs to a different account than the one that created the resource (e.g. after account switching or a stale token); ownership comparison fails on type mismatch (numeric userId from DB vs string id in the token); seeded test data created rows under a different user id than the test token.
Common situations: Sharing resource URLs between accounts; frontend still sending a token from the previous login after re-auth; inconsistent id types across services; test fixtures where the seeding user and the authenticated test user differ.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of affaan-m/ECC@d8409a4b08 (2026-08-26).
Data as JSON: /api/errors/7b90d66f0540496c.
Report an issue: GitHub.