{"record":{"id":"7b90d66f0540496c","repo":"affaan-m/ECC","slug":"forbidden-7b90d6","errorCode":"forbidden","errorMessage":"forbidden","messagePattern":"forbidden","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"skills/api-design/SKILL.md","lineNumber":317,"sourceCode":"\n```\n# Bearer token in Authorization header\nGET /api/v1/users\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIs...\n\n# API key (for server-to-server)\nGET /api/v1/data\nX-API-Key: sk_live_abc123\n```\n\n### Authorization Patterns\n\n```typescript\n// Resource-level: check ownership\napp.get(\"/api/v1/orders/:id\", async (req, res) => {\n  const order = await Order.findById(req.params.id);\n  if (!order) return res.status(404).json({ error: { code: \"not_found\" } });\n  if (order.userId !== req.user.id) return res.status(403).json({ error: { code: \"forbidden\" } });\n  return res.json({ data: order });\n});\n\n// Role-based: check permissions\napp.delete(\"/api/v1/users/:id\", requireRole(\"admin\"), async (req, res) => {\n  await User.delete(req.params.id);\n  return res.status(204).send();\n});\n```\n\n## Rate Limiting\n\n### Headers\n\n```\nHTTP/1.1 200 OK\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 95","sourceCodeStart":299,"sourceCodeEnd":335,"githubUrl":"https://github.com/affaan-m/ECC/blob/d8409a4b0813771235555e32e3d8046a73988bfa/skills/api-design/SKILL.md#L299-L335","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before - type mismatch makes every request 'forbidden'\nif (order.userId !== req.user.id) return res.status(403).json({ error: { code: 'forbidden' } })\n// order.userId is 42 (number), req.user.id is '42' (string) -> always 403\n\n// after - normalize before comparing\nif (String(order.userId) !== String(req.user.id)) {\n  return res.status(403).json({ error: { code: 'forbidden' } })\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Normalize then compare ownership — guards against the classic number-vs-string mismatch\nconst isOwner = (resourceUserId: string | number, authenticatedUserId: string | number): boolean =>\n  String(resourceUserId).trim().toLowerCase() === String(authenticatedUserId).trim().toLowerCase()","tryCatchPattern":"// Handler: keep 404 before 403 so non-existence is checked first, ownership second\nconst order = await Order.findById(req.params.id)\nif (!order) return res.status(404).json({ error: { code: 'not_found' } })\nif (!isOwner(order.userId, req.user.id)) {\n  return res.status(403).json({ error: { code: 'forbidden' } })\n}","preventionTips":["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"],"tags":["rest","http-403","authorization","ownership"],"backgroundTag":"authorization-forbidden","analyzedSha":"d8409a4b0813771235555e32e3d8046a73988bfa","analyzedAt":"2026-08-26T12:15:34.022Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}