Skip to content

Implement access control

Verify permissions and roles in your application code to control user access

After configuring permissions and roles, the next critical step is implementing access control directly within your application code. This is achieved by carefully examining the roles and permissions embedded in the user’s access token to make authorization decisions.

Scalekit conveniently packages these authorization details during the authentication process, providing you with a comprehensive set of data to make precise access control decisions without requiring additional API calls.

Review the authorization flow UserYour App Request protected resource Decode and validate access token Extract and verify required permission/role Return resource or error

This section focuses on implementing access control, which naturally follows user authentication. We recommend completing the authentication quickstart before diving into these access control implementation details.

When you exchange the code for a user profile, Scalekit also adds additional information that help your app determine the access control decisions.

{
user: {
email: "john.doe@example.com",
emailVerified: true,
givenName: "John",
name: "John Doe",
id: "usr_74599896446906854"
},
idToken: "eyJhbGciO..", // Decode for full user details
accessToken: "eyJhbGciOi..",
refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..",
expiresIn: 299 // in seconds
}

Let’s closely look at the access token:

Decoded access token
{
"aud": ["skc_987654321098765432"],
"client_id": "skc_987654321098765432",
"exp": 1750850145,
"iat": 1750849845,
"iss": "http://example.localhost:8889",
"jti": "tkn_987654321098765432",
"nbf": 1750849845,
"roles": ["project_manager", "member"],
"oid": "org_69615647365005430",
"permissions": ["projects:create", "projects:read", "projects:update", "tasks:assign"],
"sid": "ses_987654321098765432",
"sub": "usr_987654321098765432"
}

The roles and permissions values provide runtime insights into the user’s access constraints directly within the access token, eliminating the need for additional API requests. Crucially, always validate the token’s integrity before relying on the embedded authorization details.

Validate and decode access token in middleware
// Middleware to validate tokens and extract authorization data
const validateAndExtractAuth = async (req, res, next) => {
try {
// Extract access token from cookie (decrypt if needed)
const accessToken = decrypt(req.cookies.accessToken);
// Validate the token using Scalekit SDK
const isValid = await scalekit.validateAccessToken(accessToken);
if (!isValid) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
// Decode token to get roles and permissions using any JWT decode library
const tokenData = await decodeAccessToken(accessToken);
// Make authorization data available to route handlers
req.user = {
id: tokenData.sub,
organizationId: tokenData.oid,
roles: tokenData.roles || [],
permissions: tokenData.permissions || []
};
next();
} catch (error) {
return res.status(401).json({ error: 'Authentication failed' });
}
};

This approach makes user roles and permissions available throughout different routes of your application, enabling consistent and secure access control across all endpoints.

Verify user’s role to allow access to protected resources

Section titled “Verify user’s role to allow access to protected resources”

Role-based access control (RBAC) provides a straightforward way to manage permissions by grouping them into logical roles. Instead of checking individual permissions for every action, your application can simply verify if the user has the required role, making access control decisions more efficient and easier to maintain.

Role-based access control
17 collapsed lines
// Helper function to check roles
function hasRole(user, requiredRole) {
return user.roles && user.roles.includes(requiredRole);
}
// Middleware to require specific roles
function requireRole(role) {
return (req, res, next) => {
if (!hasRole(req.user, role)) {
return res.status(403).json({
error: `Access denied. Required role: ${role}`
});
}
next();
};
}
// Admin-only routes
app.get('/api/admin/users', validateAndExtractAuth, requireRole('admin'), (req, res) => {
// Only admin users can access this endpoint
res.json(getAllUsers(req.user.organizationId));
});
// Multiple role check
app.post('/api/admin/invite-user', validateAndExtractAuth, (req, res) => {
const user = req.user;
// Allow admins or managers to invite users
if (!hasRole(user, 'admin') && !hasRole(user, 'manager')) {
return res.status(403).json({ error: 'Only admins and managers can invite users' });
}
const invitation = createUserInvitation(req.body, user.organizationId);
res.json(invitation);
});

Verify user’s permissions to allow specific actions

Section titled “Verify user’s permissions to allow specific actions”

Permission-based access control provides granular control over specific actions and resources within your application. While roles offer broad access patterns, permissions allow you to define exactly what operations users can perform, enabling precise security controls and the principle of least privilege.

Permission-based access control
17 collapsed lines
// Helper function to check permissions
function hasPermission(user, requiredPermission) {
return user.permissions && user.permissions.includes(requiredPermission);
}
// Middleware to require specific permissions
function requirePermission(permission) {
return (req, res, next) => {
if (!hasPermission(req.user, permission)) {
return res.status(403).json({
error: `Access denied. Required permission: ${permission}`
});
}
next();
};
}
// Protected routes with permission checks
app.get('/api/projects', validateAndExtractAuth, requirePermission('projects:read'), (req, res) => {
// User has projects:read permission - allow access
res.json(getProjects(req.user.organizationId));
});
app.post('/api/projects', validateAndExtractAuth, requirePermission('projects:create'), (req, res) => {
// User has projects:create permission - allow creation
const newProject = createProject(req.body, req.user.organizationId);
res.json(newProject);
});
// Multiple permission check
app.delete('/api/projects/:id', validateAndExtractAuth, (req, res) => {
const user = req.user;
// Check if user has either admin role or specific delete permission
if (!hasPermission(user, 'projects:delete') && !user.roles.includes('admin')) {
return res.status(403).json({ error: 'Cannot delete projects' });
}
deleteProject(req.params.id, user.organizationId);
res.json({ success: true });
});

By implementing both role-based and permission-based access control, your application now has a comprehensive security framework that protects different routes and endpoints. You can combine both approaches to create fine-grained access control that matches your application’s specific requirements.

Admin bypass pattern: Allow users with admin role to bypass certain permission checks while maintaining granular control for other users

Resource ownership pattern: Combine role/permission checks with resource ownership verification (e.g., users can only edit their own projects unless they have admin role)

Time-based access pattern: Consider implementing time-based restrictions for sensitive operations, especially for roles with elevated permissions