Access Controls and Accounts
Modern web applications serve diverse users with different needs and permissions. A school portal might need to distinguish between students viewing their grades and instructors entering them. An online store must separate customers browsing products from administrators managing inventory. A content management system requires different capabilities for authors, editors, and administrators. This reading explores how applications implement these distinctions through access control systems.
By the end of this reading, you will understand the fundamental difference between authentication and authorization, explore several methods for implementing access control, and learn how role-based systems integrate with relational databases. This knowledge provides the foundation for building applications that serve different types of users while maintaining security and appropriate access boundaries.
Authentication vs Authorization
Before diving into access control systems, we must distinguish between two frequently confused concepts: authentication and authorization. Though often mentioned together, they serve fundamentally different purposes in application security.
Authentication: Proving Who You Are
Authentication answers the question "Who are you?" When you log into a website with your username and password, you authenticate yourself by proving your identity. The system verifies that you are who you claim to be by checking your credentials against stored information. Authentication establishes your identity but says nothing about what you can do once inside the system.
Think of authentication like showing your ID at a building entrance. The security guard verifies your identity, confirms you are who your ID says you are, and lets you enter. However, your ID alone does not determine which floors you can access or which rooms you can enter once inside.
Authorization: Determining What You Can Do
Authorization answers the question "What are you allowed to do?" After you have authenticated (proven your identity), the system must determine your permissions. Can you view this page? Can you edit this content? Can you delete these records? Authorization controls access to resources and features based on who you are.
Continuing the building analogy, authorization is like the access card you receive after showing your ID. Your card might grant access to certain floors but not others, certain rooms but not the server room, certain times but not after hours. Your identity has been verified (authentication), and now your permissions determine where you can go and what you can do (authorization).
Modern Separation: OAuth and Third-Party Authentication
Modern web applications often separate authentication from their core application logic entirely. You have likely seen "Sign in with Google" or "Continue with GitHub" buttons on websites. These use protocols like OAuth to delegate authentication to trusted third parties while keeping authorization within the application itself.
When you click "Sign in with Google," Google handles authentication (verifying you are who you claim to be), but the application you are signing into still controls authorization (determining what you can do). Google confirms your identity and provides basic information about you, but it does not tell the application whether you should be a regular user or an administrator. The application makes that determination based on its own authorization rules.
This separation demonstrates an important principle: authentication and authorization are distinct concerns that can be handled by different systems. In this course, we will build both authentication and authorization ourselves, but understanding this separation helps clarify the role each plays in application security.
Authentication verifies identity ("Are you really Bob?"), while authorization determines permissions ("What can Bob do?"). You can have one without the other, and they often involve different systems and techniques.
Methods of Access Control
Once users authenticate, applications need systematic ways to control what authenticated users can access and modify. Different applications require different levels of complexity in their access control systems. A simple blog might need only basic permission flags, while an enterprise system might require sophisticated role hierarchies and fine-grained permissions. Let us examine several common approaches, progressing from simple to more complex.
Simple Permission Flags
The simplest form of access control uses boolean flags stored directly on user accounts. A user record might include fields like isAdmin, canPost, or canModerate. When the application needs to check permissions, it simply looks at these flags.
// Simple permission flag approach
const user = {
id: 1,
username: 'jsmith',
email: 'jsmith@example.com',
isAdmin: false,
canEditPosts: true,
canDeleteComments: false
};
// Checking permissions
if (user.isAdmin) {
// Allow access to admin panel
}
if (user.canEditPosts) {
// Show edit button
}
This approach works well for simple applications with few permission types. It is easy to understand and implement, requires minimal database structure, and provides clear, direct permission checks. However, it scales poorly as applications grow more complex. Adding new permission types requires database schema changes. Managing users with many different permission combinations becomes cumbersome, and you might end up with dozens of boolean fields cluttering your user table.
Access Control Lists
Access Control Lists (ACLs) offer more flexibility by explicitly listing what each user can access. Rather than storing general permission flags, ACLs associate specific resources with specific users and their allowed actions. This approach is common in file systems and content management systems where permissions vary by resource.
// ACL approach - permissions tied to specific resources
const permissions = [
{ userId: 1, resourceType: 'post', resourceId: 42, action: 'read' },
{ userId: 1, resourceType: 'post', resourceId: 42, action: 'edit' },
{ userId: 1, resourceType: 'post', resourceId: 57, action: 'read' },
{ userId: 2, resourceType: 'post', resourceId: 42, action: 'read' }
];
// Checking if user can edit a specific post
function canUserEditPost(userId, postId) {
return permissions.some(p =>
p.userId === userId &&
p.resourceType === 'post' &&
p.resourceId === postId &&
p.action === 'edit'
);
}
ACLs provide fine-grained control over individual resources. User permissions can vary by specific item, making them ideal when different users need different access to the same types of resources. A document management system might use ACLs so that users can share specific documents with specific colleagues while keeping others private.
The tradeoff is complexity. ACL tables can grow very large, and checking permissions requires more database queries. Managing permissions becomes more involved, as you must track permissions for every user-resource combination. For many applications, this level of granularity is unnecessary.
Role-Based Access Control
Role-Based Access Control (RBAC) strikes a balance between simplicity and flexibility. Instead of granting permissions directly to users or tracking permissions per resource, RBAC groups users into roles. Each role has a defined set of permissions, and users inherit the permissions of their assigned role.
Consider a school portal application. Rather than marking individual users with flags like canViewGrades, canEditGrades, canManageCourses, and canCreateAccounts, you create roles: student, instructor, and admin. Each role comes with an understood set of capabilities. Students can view their own grades, instructors can enter grades for their courses, and administrators can manage the entire system.
// RBAC approach - permissions associated with roles
const user = {
id: 1,
username: 'jsmith',
email: 'jsmith@example.com',
roleId: 2 // References a role
};
const roles = {
1: { name: 'user', permissions: ['view_profile', 'edit_own_profile'] },
2: { name: 'admin', permissions: ['view_profile', 'edit_own_profile', 'edit_any_profile', 'manage_users'] }
};
// Checking permissions through roles
function hasPermission(user, permission) {
const role = roles[user.roleId];
return role.permissions.includes(permission);
}
RBAC offers several advantages that make it popular in many applications. Roles align naturally with organizational structure (employee, manager, administrator). Permission management becomes simpler because you manage role permissions rather than individual user permissions. When you need to change what managers can do, you update the manager role once rather than updating hundreds of individual user records. New users can be quickly assigned appropriate access by assigning them a role.
RBAC handles the common case elegantly: most users fit into predefined categories with standard permissions. The system remains flexible enough to add new roles as needs evolve, while staying simple enough to understand and maintain. This is the approach we will implement in this course.
Simple permission flags work well for applications with few permission types and minimal variation between users. ACLs excel when permissions must vary significantly by individual resource. RBAC provides the best balance for applications where users naturally fall into categories with similar permission needs. Most web applications benefit from RBAC's combination of simplicity and flexibility.
Role-Based Access Control and Relational Databases
Understanding how RBAC integrates with relational databases is crucial for implementation. The database structure directly impacts how efficiently you can check permissions and how easily you can modify role definitions. Let us examine the typical database design and then explore how application code uses that structure to enforce access control.
Database Structure for RBAC
A basic RBAC system typically involves at least two tables: a users table and a roles table. The users table contains account information (username, email, password hash) plus a foreign key reference to the roles table. The roles table defines available roles and can optionally store role-specific settings.
-- Roles table defines available roles
CREATE TABLE roles (
role_id SERIAL PRIMARY KEY,
role_name VARCHAR(50) UNIQUE NOT NULL,
role_description TEXT
);
-- Users table references roles
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role_id INTEGER REFERENCES roles(role_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Seed with basic roles
INSERT INTO roles (role_name, role_description) VALUES
('user', 'Standard user with basic access'),
('admin', 'Administrator with full system access');
This structure establishes a relationship: each user has exactly one role, and each role can be assigned to many users (a one-to-many relationship). When you query for a user, you can easily join to the roles table to retrieve their role information. When you need to update role definitions, you modify the roles table, and all users with that role immediately reflect the change.
More sophisticated systems might include additional tables. A permissions table could enumerate all possible actions in the system. A role_permissions join table would then associate roles with their specific permissions, allowing dynamic permission assignment without code changes. For this course, we will keep the structure simpler, with permission logic handled in application code rather than in the database.
Enforcing Access Control in Application Code
Database structure stores roles, but application code enforces them. In Express applications, middleware functions provide an elegant way to check permissions before allowing access to routes. These middleware functions examine the authenticated user's role and either allow the request to proceed or deny access.
// Middleware to require specific role
function requireRole(roleName) {
return (req, res, next) => {
// Assumes user object is attached to req by authentication middleware
if (!req.user) {
return res.status(401).send('Not authenticated');
}
if (req.user.role_name !== roleName) {
return res.status(403).send('Insufficient permissions');
}
// User has required role, allow request to proceed
next();
};
}
// Using the middleware on routes
app.get('/admin/dashboard', requireRole('admin'), (req, res) => {
res.render('admin/dashboard');
});
app.post('/admin/users/:id/edit', requireRole('admin'), (req, res) => {
// Only admins can reach this code
// Handle user editing logic
});
This pattern is common and effective. Middleware functions run before route handlers, providing a checkpoint where access can be verified. If the user lacks permission, the middleware sends an error response and stops the request. If the user has permission, the middleware calls next(), allowing the request to continue to the route handler.
Controllers can also check permissions for more nuanced access control. While middleware works well for route-level protection (entire routes require specific roles), controllers handle situation-specific logic (users can edit their own profile, but admins can edit any profile).
// Controller with role-specific logic
async function editUserProfile(req, res) {
const targetUserId = req.params.userId;
const currentUser = req.user;
// Admins can edit anyone, users can only edit themselves
const canEdit = currentUser.role_name === 'admin' ||
currentUser.user_id === parseInt(targetUserId);
if (!canEdit) {
return res.status(403).send('You cannot edit this profile');
}
// Proceed with editing logic
// ...
}
This combination of middleware for broad role requirements and controller logic for specific permission scenarios provides flexible, maintainable access control. Middleware keeps unauthorized users away from entire sections of the application, while controller logic handles the nuances of who can do what to which specific resources.
Hiding buttons or menu items based on user roles improves user experience, but it does not provide security. Users can manipulate their browser or make direct HTTP requests to bypass client-side restrictions. Always enforce access control on the server using middleware and controller logic. Client-side controls are for convenience, server-side controls are for security.
Security Considerations
Implementing access control involves more than just checking roles. Several security principles help ensure your authorization system remains robust and secure.
Principle of Least Privilege: Users should have the minimum permissions necessary to accomplish their tasks. When in doubt, restrict access rather than grant it. You can always add permissions later if users need them, but removing permissions after users expect them is more difficult and can introduce security vulnerabilities if overlooked.
Defense in Depth: Multiple layers of security provide better protection than any single mechanism. Check permissions at the route level with middleware, verify them again in controllers before sensitive operations, and validate user input throughout. If one check fails or is bypassed, other checks can still prevent unauthorized access.
Fail Securely: When permission checks encounter errors or unexpected conditions, they should deny access rather than allow it. If you cannot determine whether a user should have access (perhaps because the database is temporarily unavailable), the safe default is to deny the request. Failing securely prevents temporary errors from becoming security vulnerabilities.
Audit Important Actions: Log significant events, especially those involving privilege changes or access to sensitive data. If an administrator edits another user's account, log who made the change, when it happened, and what changed. Audit logs help detect unauthorized access and provide accountability.
Avoid common pitfalls: Do not expose role information in client-side JavaScript that users can manipulate. Do not rely solely on hiding UI elements to restrict access. Do not use predictable role identifiers that users might guess. Do not skip authorization checks in API endpoints or AJAX handlers. Every route that modifies data or displays sensitive information needs proper authorization checks.
Conclusion
Access control systems form a crucial part of application security, determining what authenticated users can see and do. Understanding the distinction between authentication (proving identity) and authorization (determining permissions) provides the foundation for building secure systems. Different access control methods serve different needs: simple permission flags for basic scenarios, Access Control Lists for fine-grained resource permissions, and Role-Based Access Control for the common case where users naturally fall into categories.
RBAC integrates elegantly with relational databases through foreign key relationships between users and roles. Express middleware provides a clean, maintainable way to enforce role requirements, checking permissions before routes execute. Controllers handle situation-specific logic where permission requirements vary based on context, such as users editing their own information versus administrators editing anyone's information.
In the upcoming assignment, you will implement a two-role system with standard users and administrators. This hands-on practice will solidify your understanding of how RBAC works in real applications. You will create database structures to support roles, write middleware to enforce role requirements, and build controllers that respect permission boundaries. These skills transfer directly to more complex systems and larger applications where access control becomes even more critical.