Full Stack Web Development OWASP Top 10: Broken Access Control RBAC & Resource Ownership RFC 9110 HTTP Semantics

Full Stack Authorization: Real-World Access Control

Master web authorization from a practical full-stack perspective. Differentiate Authentication (“Who are you?”) from Authorization (“What are you permitted to do?”). Implement Role-Based Access Control (RBAC), enforce resource-level ownership guards (user.id === resource.ownerId), master HTTP status codes (401 vs 403 vs 404), and understand why hiding frontend buttons provides zero security against direct API requests.

Standards: IETF RFC 9110 & OWASP Access Control
Architecture: Server-Side Middleware Pipeline
Est. Time: 50–65 Minutes

01. What is Authorization?

Identity Verification vs. Permission Enforcement

In modern web systems, Authorization is the security mechanism that determines whether an authenticated user (or API client principal) has permission to perform a specific action on a specific resource.

Real-World Scenario: A user clicks “Delete Course” on an online platform:
  • Authentication: “Who is this user?” → (Server validates session/JWT → User is Maya, ID #202).
  • Authorization: “Is Maya allowed to delete Course #42?” → (Server checks if Maya owns Course #42 or has admin role).
Authentication = Identity

Validates that the client really is who they claim to be via credentials (passwords, Passkeys, TOTP 2FA, OAuth tokens).

Authorization = Permissions

Evaluates access policies and resource ownership to permit or deny specific operations (e.g. read, update, delete).

02. Authentication vs Authorization

Side-by-Side Comparison & The Two-Stage Security Gate

A common mistake for beginners is conflating these two concepts or treating “logged in” as equivalent to “allowed to do everything”. In full-stack architecture, they are two sequential gates:

AUTHENTICATION (AuthN)

“Who are you?”

  • User login with username/password
  • Session cookie verification
  • JWT Bearer token signature validation
  • Multi-Factor Authentication (SMS/TOTP)
  • Failed check returns: 401 Unauthorized

AUTHORIZATION (AuthZ)

“What are you allowed to do?”

  • Can this user view another user's invoice?
  • Can this instructor delete this specific course?
  • Can an unverified user publish public articles?
  • Can an admin promote another user to superuser?
  • Failed check returns: 403 Forbidden
1
LOGIN
User supplies credentials
2
AUTHENTICATED
Identity verified, token issued
3
REQUEST RESOURCE
Client asks to delete Course #42
4
AUTHZ CHECK
Server inspects role & ownership
5
ALLOW / DENY
200 OK or 403 Forbidden

03. Roles: Role-Based Access Control (RBAC)

Grouping Permissions into Manageable Persona Profiles

In Role-Based Access Control (RBAC), permissions are not assigned directly to individual users one by one. Instead, permissions are grouped into Roles (e.g. student, instructor, admin), and users are assigned one or more roles.

Student Role

Assigned to learners. Allows viewing enrolled courses and submitting assignments. Cannot edit course content or see administrative reports.

Instructor Role

Assigned to educators. Allows creating courses, uploading lessons, and grading student submissions for courses they personally own.

Admin Role

Assigned to platform operators. Allows managing all user accounts, moderating all courses, modifying site settings, and auditing security logs.

Important Architecture Note: A Role is simply a container or label for a set of permissions. Roles alone are not the only authorization model. Complex systems often layer Resource Ownership andAttribute-Based policies (ABAC) on top of RBAC.

04. Granular Permissions

Atomic Action Strings: resource:action

While roles represent who a user is within an organization, Permissions define the atomic capabilities permitted in the software. Best practice in modern web development uses colon-separated string tokens: <resource>:<action>.

// Examples of granular permissions: course:read // Ability to view course details and syllabus course:create // Ability to create a brand-new course draft course:update // Ability to edit lessons and metadata course:delete // Ability to purge a course from the platform user:read // View profile information user:update // Change name, email, or settings user:delete // Deactivate or delete user records

By decoupling roles from code logic, your backend middlewares can check:if (!user.hasPermission('course:delete')) return 403; rather than hardcoding role names everywhere.

05. Resource-Level Authorization & Ownership

“Can this user perform this action on THIS specific resource?”

This is where most security vulnerabilities occur in real-world applications. Being logged in and holding the instructorrole does NOT mean an instructor can edit every course in the database!

User A (Maya - ID #101)

Requests: PATCH /api/courses/42
Course #42 has ownerId: 101.
Check: req.user.id === course.ownerId → ALLOW (200 OK)

User B (John - ID #205)

Requests: PATCH /api/courses/42
John is also an instructor, but he does NOT own Course #42.
Check: req.user.id !== course.ownerId → DENY (403 Forbidden)

Critical Rule: Never rely solely on req.user.role === 'instructor'. Always check resource ownership unless the user possesses explicit global administrative bypass privileges.

06. Frontend vs Backend Authorization

UI Button Hiding Improves UX — Server Enforcement Is Security

Many frontend developers believe that wrapping a button in a conditional check provides security:

// ⚠️ FRONTEND COMPONENT (e.g. React): {user.role === "admin" && ( <button onClick={handleDeleteCourse}>Delete Course</button> )}

Hiding this button is wonderful for User Experience (UX) — regular students won't be confused by buttons they cannot use. However, this provides ZERO security.

The Gold Standard Rule: Anyone can open terminal, Postman, or the browser DevTools Network tab and dispatch:
curl -X DELETE https://api.yoursite.com/api/courses/42 -H "Authorization: Bearer ..."

If the backend server endpoint does not check authorization on every incoming request, an attacker can delete records regardless of whether the frontend button was rendered or hidden.

07. HTTP Status Codes (RFC 9110)

401 Unauthorized vs. 403 Forbidden vs. 404 Privacy Masking

Using the wrong HTTP status code leads to subtle client bugs, infinite auth redirect loops, and security enumeration flaws. Here are the authoritative definitions according to IETF RFC 9110:

401 Unauthorized

Meaning: Authentication is required and has either failed or has not yet been provided. The client must provide valid credentials (e.g. log in or refresh expired JWT).

403 Forbidden

Meaning: The server understood the request and knows who the client is, but refuses to authorize it. Re-authenticating with the same credentials will make no difference.

404 Not Found

Meaning: Resource does not exist. In sensitive designs (e.g. confidential repositories or admin portals), servers intentionally return 404 instead of 403 to avoid revealing whether a private resource exists (resource enumeration defense).

08. Authorization Request Flow

End-to-End Execution Trace of a Protected API Call

When a client fires an HTTP mutation like DELETE /api/courses/42 with header Authorization: Bearer <token>, the server passes the request through an orchestrated sequence:

1
HTTP REQUEST
Headers + Payload arrive at server
2
AUTHENTICATE
Verify token; extract user ID. Fail → 401
3
FETCH RESOURCE
Load Course #42 from DB. Missing → 404
4
AUTHORIZE
Check permissions & ownerId. Fail → 403
5
BUSINESS LOGIC
Check state (e.g. students enrolled)
6
DATABASE & RESP
Execute delete → return 204 No Content

🔥 09. Role-Based Access Control — Live Simulator

Test Access Decisions Across Roles, Resources, and Actions

Select a user persona, a target resource, and an attempted action. Watch the live decision engine evaluate authentication, role capabilities, and resource ownership in real time:

Decision Output: ALLOW — 200 OK

Why: Maya is an instructor AND she is the verified owner of Course #42 (course.ownerId === Maya.id).

🔥 10. Interactive Permission Matrix

Click Table Cells to Inspect Specific Authorization Rules

Modern full-stack security maps roles to an access matrix. Notice the asterisk (✓*): Instructors can only edit or delete courses they personally own:

RoleView (Read)CreateEdit (Update)Delete
Student
Instructor
Admin

Policy Rule: Instructor → Edit Action

✓* Subject to Resource Ownership: An instructor can ONLY edit a course if course.instructorId === req.user.id. Attempting to edit another educator's course returns 403 Forbidden.

🔥 11. Resource Ownership Check Scenario

Testing: user.id === resource.ownerId

Consider Course #42 in your database with property ownerId = 101. Both User #101 and User #205 possess the instructor role in their profiles. What happens when each tries to send PATCH /api/courses/42?

// Server-side middleware check: const course = await db.courses.findById(req.params.id); // Course #42 (ownerId: 101) if (req.user.role === 'admin' || course.ownerId === req.user.id) { // Current user (101) === course.ownerId (101) -> ALLOW }

🔥 12. API Authorization Playground

Generate Simulated HTTP Requests & Inspect Server Evaluations

Construct an API call, fire it against the simulated server, and inspect the internal 3-step authorization decision pipeline:

PATCH /api/courses/42 HTTP/1.1 Host: api.learnplatform.io Authorization: Bearer jwt_maya_token Content-Type: application/json

13. The #1 Frontend-Only Security Mistake

“I hid the button in React, so the API is safe” (The Fatal Illusion)

Let's prove in real time why hiding UI buttons without backend validation fails immediately. Toggle the switch below to see what happens when an attacker bypasses the UI and sends a direct HTTP request:

Client React View:
(Button hidden in React DOM because role !== 'admin')

14. Insecure Direct Object References (IDOR / BOLA)

OWASP Top 10 #1 Risk: Trusting User-Supplied Database Keys

Insecure Direct Object Reference (IDOR), also classified as Broken Object Level Authorization (BOLA), occurs when an application takes an ID from the user (such as a URL path parameter /api/orders/500) and fetches the record directly from the database without verifying ownership.

❌ Vulnerable Implementation

// Flawed: Fetches order solely by ID! app.get('/api/orders/:id', verifyToken, async (req, res) => { const order = await db.query( 'SELECT * FROM orders WHERE id = $1', [req.params.id] ); // Any logged-in user can view ANY order! res.json(order); });

✅ Secure Implementation

// Secure: Scoped to authenticated user ID! app.get('/api/orders/:id', verifyToken, async (req, res) => { const order = await db.query( 'SELECT * FROM orders WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id] ); if (!order) return res.status(404).send(); res.json(order); });
Core Lesson: Never assume that because a user knows or guesses an object's ID, they are authorized to access it. All database lookups must be scoped to the authenticated principal or validated against permissions.

15. Authorization vs. Business Logic

Permissions (“Can I?”) vs Domain State (“Is it currently valid?”)

A common architectural confusion is mixing up Authorization checks with Business Logic rules. They answer two completely different questions:

Authorization Question

“Does this customer have permission to cancel an order?”
→ Yes, customer owns the order (order:cancel permission).

Business Logic Question

“Under the application's business rules, can this order be cancelled?”
→ If the package already shipped, cancellation is physically impossible.

Interactive State Test: Customer Cancels Order #500

16. Practical Backend Architecture Pattern

Framework-Agnostic 5-Stage Middleware Pipeline

Regardless of whether you use Express, Fastify, Next.js Route Handlers, NestJS, Go, or Python FastAPI, robust full-stack applications structure their controller handlers using this universal 5-stage pipeline:

// Universal Full-Stack Access Control Handler async function handleUpdateCourse(request, response) { // 1. AUTHENTICATION GATE const user = await authenticate(request); if (!user) { return response.status(401).json({ error: 'Authentication required' }); } // 2. RESOURCE LOOKUP const course = await getCourseById(request.params.id); if (!course) { return response.status(404).json({ error: 'Course not found' }); } // 3. AUTHORIZATION & OWNERSHIP GATE const isOwner = course.ownerId === user.id; const isAdmin = user.role === 'admin'; const hasEditPermission = user.permissions.includes('course:update'); if (!isAdmin && (!hasEditPermission || !isOwner)) { return response.status(403).json({ error: 'Access forbidden: Insufficient permissions' }); } // 4. BUSINESS LOGIC VALIDATION if (course.isArchived) { return response.status(409).json({ error: 'Cannot modify archived course' }); } // 5. DATABASE EXECUTION & RESPONSE const updatedCourse = await updateCourse(course.id, request.body); return response.status(200).json(updatedCourse); }

Notice the clean separation: 401 on unauthenticated, 404 on missing records,403 on authorization failure, and 409 on business conflicts.

🔥 17. Authorization Debugging Lab

Diagnose Real-World Security & Middleware Vulnerabilities

Read each production scenario, inspect the flawed snippet, and select the correct full-stack remediation:

1. Unauthenticated Request Yields 200 with Empty Data
Security Bug

Symptom: A client calls GET /api/profile with no Authorization header. Instead of blocking the request, the backend returns 200 OK with {"user": null}, causing frontend rendering crashes.

app.get('/api/profile', (req, res) => { const user = getUserFromToken(req.headers.authorization); // Bug: Missing authentication guard! res.status(200).json({ user }); });
2. Student Receives 401 Instead of 403 When Deleting Course
Security Bug

Symptom: An authenticated student (Alex) sends DELETE /api/courses/42 with a valid JWT token. The server replies with 401 Unauthorized, prompting the frontend to redirect Alex to the login page.

app.delete('/api/courses/:id', verifyJwt, (req, res) => { if (req.user.role !== 'admin') { return res.status(401).json({ error: 'Not permitted' }); // Bug! } db.deleteCourse(req.params.id); });
3. Instructor Maya Can Edit John's Course (Missing Ownership Check)
Security Bug

Symptom: Instructor Maya (ID: 202) sends PATCH /api/courses/99 (created by Instructor John, ID: 303). The server accepts the change because it only checked req.user.role === "instructor".

app.patch('/api/courses/:id', verifyJwt, async (req, res) => { if (req.user.role !== 'instructor') return res.status(403).send(); // Bug: Checks role, but never verifies resource ownership! await db.updateCourse(req.params.id, req.body); res.send({ updated: true }); });
4. System Admin Blocked From Editing Outdated Course
Security Bug

Symptom: An administrator attempts to fix a typo in Course #42, but receives 403 Forbidden because their user ID does not match the course ownerId.

if (course.ownerId !== req.user.id) { return res.status(403).json({ error: 'Only owner can edit' }); }
5. UI Button Hidden, but Direct API Request Deletes Records
Security Bug

Symptom: The frontend developers hid the "Delete User" button for non-admins. However, an intern fired DELETE /api/users/10 directly using Postman and deleted an account.

// Frontend: {user.role === 'admin' && <button onClick={handleDelete}>Delete</button>} // Backend: app.delete('/api/users/:id', async (req, res) => { // Bug: No authentication or authorization middleware at all! await db.users.delete(req.params.id); res.status(204).send(); });

18. Principle of Least Privilege

Minimizing Attack Surfaces & Blast Radius

The Principle of Least Privilege (PoLP) dictates that every user, service, and application process must only be granted the minimum permissions strictly necessary to perform its intended job.

Student Accounts

Granted course:read and submission:create. They have zero need for course creation, user enumeration, or database introspection.

Instructor Accounts

Granted course:create and course:update scoped strictly to their own course IDs. They are never granted global administrative rights.

Admin Accounts

Admin rights are limited to tightly audited operators with multi-factor authentication (MFA). Wildcard permissions (*.*) are avoided wherever possible.

Why This Matters: If a student's laptop is compromised, the attacker only gains access to that student's view of courses. If all authenticated users were granted wildcard privileges, a single compromised student account would expose the entire institution's database.

🔥 19. Final Authorization Challenge

Predict the Server Decision for 6 Complex Platform Scenarios

Test your full-stack authorization mastery across 6 realistic platform scenarios. Predict whether each request will be ALLOW, 401, or 403:

Scenario 1: Student Edits Another User's CoursePrincipal: Alex (ID: 101) (student)

Request Context: Alex sends PATCH /api/courses/42 with valid student session token.

Scenario 2: Instructor Edits Their Own CoursePrincipal: Maya (ID: 202) (instructor)

Request Context: Maya sends PATCH /api/courses/42 to update the syllabus of her own course.

Scenario 3: Instructor Edits Another Instructor's CoursePrincipal: Maya (ID: 202) (instructor)

Request Context: Maya sends PATCH /api/courses/99 attempting to modify John's lesson plan.

Scenario 4: Admin Deletes a CoursePrincipal: Sarah (ID: 999) (admin)

Request Context: Sarah sends DELETE /api/courses/42 to purge terms-violating content.

Scenario 5: Unauthenticated User Accesses Admin MetricsPrincipal: Anonymous Visitor (unauthenticated)

Request Context: Direct GET request with no Authorization header or session cookies.

Scenario 6: Student Views a Published CoursePrincipal: Alex (ID: 101) (student)

Request Context: Alex sends GET /api/courses/42 to read course syllabus.

20. Common Authorization Mistakes to Avoid

8 Critical Anti-Patterns Seen in Real Production Apps

1. Confusing Authentication with Authorization

Assuming that because a user successfully logged in with valid credentials, they are authorized to access all resources on the server.

2. Treating Frontend UI Restrictions as Security

Believing that hiding buttons or disabling navigation links in React/Vue prevents malicious requests. The backend MUST check authorization.

3. Checking Only Roles and Ignoring Resource Ownership

Validating that a user has the “instructor” role without confirming whether they own the specific course record being edited or deleted.

4. Returning 401 When Authenticated Users Are Forbidden

Returning 401 instead of 403. This triggers frontend interceptors to clear user sessions and inappropriately loop the user back to the login screen.

5. Assuming All Logged-In Users Can Access Every Resource

Treating endpoints as public once a user has any valid JWT, creating massive cross-tenant data leaks.

6. Trusting User-Supplied IDs Without Ownership Checks (IDOR)

Accepting /api/orders/:id directly into SQL without scoping the query to the authenticated req.user.id.

7. Over-Permissioning and Violating Least Privilege

Granting broad wildcard permissions (e.g. *.*) to regular users or service accounts, drastically expanding the blast radius of any credential breach.

8. Conflating Authorization with Business Logic Failures

Returning 403 Forbidden when an action is denied because of domain state (e.g. order already shipped) instead of using 409 Conflict or 422 Unprocessable.

🎯 21. Knowledge Check Quiz

Verify Your Full Stack Authorization Mastery

TEST YOUR KNOWLEDGE

Full Stack Authorization & Access Control Certification Quiz

Test your understanding of RBAC, granular permissions, HTTP 401 vs 403 semantics, IDOR mitigation, and server-side authorization enforcement.

Question 1 of 6Current Score: 0 / 0
Q1

What is the fundamental difference between Authentication and Authorization?

22. Key Takeaways Checklist

The Full Stack Developer's Mental Model

Authentication = Identity: Answers “Who are you?” via credentials, sessions, or tokens.
Authorization = Permissions: Answers “What are you permitted to do?” on a specific resource.
Role = Permission Grouping: Roles (student, admin) simplify management by bundling granular permission strings.
Resource-Level Ownership: Always verify user.id === resource.ownerId for non-admin mutations.
Backend Enforcement Is Essential: Hiding UI buttons improves UX, but only server-side guards prevent direct API exploitation.
Accurate HTTP Statuses: Return 401 when unauthenticated, 403 when forbidden, and 404 to prevent resource enumeration.
THE FULL STACK AUTHORIZATION MENTAL MODEL: REQUEST ➔ AUTHENTICATION ("Who are you?") ➔ RESOURCE ("What item?") ➔ AUTHORIZATION ("Are you allowed?") ➔ BUSINESS LOGIC ➔ DATABASE ➔ RESPONSE