CodingNic

Authorization & Roles

Ownership-Based Authorization

Authorization & Roles 15 min read

Ownership-Based Authorization

Objectives

By the end of this lesson, you should be able to:

  • Explain why role checks alone don’t cover every authorization need
  • Implement an ownership check, allowing a user to modify their own resource
  • Combine ownership and role checks so an admin can bypass an ownership restriction

💡 Why this matters: Role checks (Lesson 2) answer “is this user an admin?” A huge number of real authorization rules are actually a different, more specific question: “does this user own this exact resource?” This lesson covers that pattern directly.

⚠️ A note on verification: every snippet and every output in this lesson was actually run, with real HTTP requests against a real Express app.

The Problem Role Checks Alone Don’t Solve

A blog’s DELETE /api/v1/posts/:id shouldn’t require an 'admin' role, most users should be able to delete their own posts. But it also shouldn’t allow any logged-in 'member' to delete anyone’s post. Neither requireAuth alone nor requireRole('member') alone expresses this rule correctly, the actual rule depends on the specific post being requested, not just the requester’s role.

Data for the Example

javascript
const users = [
  { id: 1, email: 'erin@example.com', role: 'member' },
  { id: 2, email: 'jordan@example.com', role: 'member' },
  { id: 3, email: 'maya@example.com', role: 'admin' }
];

const posts = [
  { id: 1, title: 'Erin post', authorId: 1 },
  { id: 2, title: 'Jordan post', authorId: 2 }
];

The Ownership Check

javascript
app.delete('/api/v1/posts/:id', requireAuth, (req, res) => {
  const post = posts.find(p => p.id === Number(req.params.id));
  if (!post) return res.status(404).json({ error: 'NotFound', message: 'Post not found' });

  const isOwner = post.authorId === req.user.userId;
  const isAdmin = req.user.role === 'admin';
  if (!isOwner && !isAdmin) {
    return res.status(403).json({ error: 'Forbidden', message: 'You can only delete your own posts' });
  }

  posts.splice(posts.indexOf(post), 1);
  res.status(204).send();
});

Unlike Lesson 2’s requireRole, this check can’t run as generic middleware before the route knows which post is being requested, it needs the post’s authorId, only available after looking the post up. isOwner || isAdmin is the actual rule: the post’s owner, or anyone with the admin role, can delete it, everyone else gets 403.

Testing All Three Outcomes

javascript
const wrongOwner = await request(app).delete('/api/v1/posts/2').set('Authorization', `Bearer ${erinToken}`);
console.log(wrongOwner.status, wrongOwner.body);

const ownPost = await request(app).delete('/api/v1/posts/1').set('Authorization', `Bearer ${erinToken}`);
console.log(ownPost.status);

const adminDeletesOther = await request(app).delete('/api/v1/posts/2').set('Authorization', `Bearer ${mayaAdminToken}`);
console.log(adminDeletesOther.status);
text
403 { error: 'Forbidden', message: 'You can only delete your own posts' }
204
204

Erin, trying to delete Jordan’s post, is correctly rejected. Erin, deleting her own post, succeeds. Maya, an admin with no ownership relationship to Jordan’s post at all, succeeds anyway, purely because of her role, exactly the isOwner || isAdmin rule.

Where This Pattern Shows Up

This exact shape, “the owner or an admin, nobody else”, covers most CRUD authorization in real applications: editing a profile, deleting a comment, updating an order. It’s worth recognizing as its own pattern, distinct from a pure role check, since it needs the specific resource loaded first, not just the requester’s identity.

Try It

  1. Build the DELETE /api/v1/posts/:id route above, and confirm all three outcomes shown.
  2. Add an equivalent check to a PUT /api/v1/posts/:id (edit) route, following the same isOwner || isAdmin pattern.
  3. Try deleting a post that doesn’t exist, and confirm it returns 404, not 403, explain, in your own words, why checking existence before checking ownership is the correct order.
  4. Explain, in your own words, why this check can’t be written as reusable middleware the same way requireRole was in Lesson 2.

Recap

  • Ownership-based authorization checks whether a specific user owns a specific resource, a different, more granular question than a role check alone.
  • isOwner || isAdmin is a common, reusable shape: the resource’s owner, or an admin, can act on it, everyone else is forbidden.
  • This check needs the resource loaded first, so it typically lives inside the route handler itself, not as separate, reusable middleware.

This is the final lesson of this module before exercises. Next module: input validation, making sure the data reaching these routes is valid in the first place.