Authorization best practices
Security guidelines and best practices for implementing robust authorization systems with Scalekit
Implementing secure and maintainable authorization requires careful planning and adherence to security best practices. This guide consolidates proven patterns and recommendations for building robust access control systems with Scalekit.
Permission design principles
Section titled “Permission design principles”Use consistent naming patterns
Section titled “Use consistent naming patterns”Follow the resource:action format consistently
- Group related permissions under common resource names
- Use descriptive action names (
create,read,update,delete,manage) - Maintain consistency across your entire application
// Project management permissions"projects:create" // Create new projects"projects:read" // View project details"projects:update" // Modify existing projects"projects:delete" // Remove projects"projects:manage" // Full project administration
// User management permissions"users:invite" // Send user invitations"users:read" // View user profiles"users:update" // Modify user information"users:suspend" // Temporarily disable users
// Billing permissions"billing:read" // View billing information"billing:manage" // Modify payment methods and plansKeep permissions granular
Section titled “Keep permissions granular”Create specific permissions for distinct actions
- Avoid overly broad permissions that grant too much access
- Consider breaking down complex actions into smaller, specific permissions
- Allow for precise control over individual capabilities
// ❌ Too broad - grants excessive access"admin:all" // Dangerous - gives unlimited access
// ✅ Granular - precise control"users:create""users:read""users:update""users:delete""billing:read""billing:update""settings:read""settings:update"Plan for inheritance
Section titled “Plan for inheritance”Design permissions that work well when inherited through roles
- Consider permission hierarchies (e.g.,
manageimpliescreate,read,update,delete) - Group related permissions that are commonly assigned together
- Create logical permission families that make sense for role composition
// Base permissions"tasks:read" // View tasks"tasks:create" // Create new tasks"tasks:update" // Modify existing tasks"tasks:delete" // Remove tasks
// Composite permission"tasks:manage" // Implies all above permissions
// Role compositionconst viewerRole = ["tasks:read"];const editorRole = ["tasks:read", "tasks:create", "tasks:update"];const managerRole = ["tasks:manage"]; // Includes all task permissionsDocument permission purposes
Section titled “Document permission purposes”Use clear, descriptive display names and descriptions
- Provide meaningful descriptions explaining what each permission allows
- Maintain documentation of how permissions relate to your application features
- Include use cases and security implications in your documentation
Runtime access control security
Section titled “Runtime access control security”Fail securely by default
Section titled “Fail securely by default”Deny access when permissions are unclear or missing
- Always default to denying access when in doubt
- Log access attempts for security auditing and compliance
- Use explicit allow-lists rather than deny-lists
// ❌ Insecure - fails openfunction hasPermission(user, permission) { if (!user || !user.permissions) { return true; // Dangerous - grants access when uncertain } return user.permissions.includes(permission);}
// ✅ Secure - fails closedfunction hasPermission(user, permission) { if (!user || !user.permissions || !permission) { console.warn('Access denied: Missing user, permissions, or permission check'); return false; // Safe default } return user.permissions.includes(permission);}
// ✅ Secure with audit loggingfunction hasPermission(user, permission, resource = null) { const granted = user?.permissions?.includes(permission) || false;
// Log all access attempts for security auditing auditLog({ userId: user?.id, permission, resource, granted, timestamp: new Date().toISOString(), ipAddress: getCurrentRequestIP() });
return granted;}Centralize authorization logic
Section titled “Centralize authorization logic”Create reusable functions for common permission checks
- Keep authorization rules in dedicated modules or services
- Avoid duplicating authorization logic across your application
- Make authorization logic easy to test and maintain
// ✅ Centralized authorization serviceclass AuthorizationService { static hasPermission(user, permission) { return user?.permissions?.includes(permission) || false; }
static hasRole(user, role) { return user?.roles?.includes(role) || false; }
static canManageProject(user, project) { // Centralized business logic for project access return ( this.hasRole(user, 'admin') || project.ownerId === user.id || (project.managers.includes(user.id) && this.hasPermission(user, 'projects:manage')) ); }
static requirePermission(permission) { return (req, res, next) => { if (!this.hasPermission(req.user, permission)) { return res.status(403).json({ error: `Access denied. Required permission: ${permission}` }); } next(); }; }}
// Usage across your applicationapp.get('/api/projects/:id', AuthorizationService.requirePermission('projects:read'), getProject);app.post('/api/projects', AuthorizationService.requirePermission('projects:create'), createProject);Validate at multiple layers
Section titled “Validate at multiple layers”Implement defense in depth
- Check permissions at the API layer for all requests
- Implement additional checks in your business logic
- Use database-level permissions where appropriate
// Layer 1: API middlewareapp.use('/api/admin/*', requireRole('admin'));
// Layer 2: Route-level checksapp.get('/api/projects/:id', requirePermission('projects:read'), (req, res) => { // Layer 3: Business logic validation const project = getProject(req.params.id);
if (!canAccessProject(req.user, project)) { return res.status(403).json({ error: 'Access denied to this project' }); }
res.json(project);});
// Layer 4: Database-level security (where possible)async function getProjectsForUser(userId, organizationId) { return await db.query(` SELECT p.* FROM projects p JOIN project_members pm ON p.id = pm.project_id WHERE pm.user_id = ? AND p.organization_id = ? `, [userId, organizationId]);}Handle token expiration gracefully
Section titled “Handle token expiration gracefully”Provide seamless user experience during token refresh
- Refresh tokens automatically when possible
- Provide clear error messages for expired tokens
- Redirect users to re-authenticate when refresh fails
// Token validation with automatic refreshasync function validateAndRefreshToken(req, res, next) { try { const accessToken = getTokenFromRequest(req);
// Try to validate current token if (await scalekit.validateAccessToken(accessToken)) { req.user = await decodeAccessToken(accessToken); return next(); }
// Token expired - attempt refresh const refreshToken = getRefreshTokenFromRequest(req); if (refreshToken) { try { const newTokens = await scalekit.refreshAccessToken(refreshToken);
// Update tokens in response setTokensInResponse(res, newTokens); req.user = await decodeAccessToken(newTokens.accessToken); return next();
} catch (refreshError) { // Refresh failed - clear tokens and require re-authentication clearTokensFromResponse(res); return res.status(401).json({ error: 'Session expired. Please log in again.', redirectToLogin: true }); } }
// No valid tokens available return res.status(401).json({ error: 'Authentication required', redirectToLogin: true });
} catch (error) { console.error('Token validation error:', error); return res.status(401).json({ error: 'Authentication failed' }); }}Security considerations
Section titled “Security considerations”Token security
Section titled “Token security”Always validate tokens on the server side, never trust client-side token validation
- Store access tokens securely and use HTTPS in production
- Regularly audit your permission assignments and access patterns
- Implement proper token rotation and expiration policies
// ✅ Secure token storagefunction storeTokensSecurely(tokens, res) { // Encrypt tokens before storing const encryptedAccessToken = encrypt(tokens.accessToken); const encryptedRefreshToken = encrypt(tokens.refreshToken);
// Store with secure cookie settings res.cookie('accessToken', encryptedAccessToken, { httpOnly: true, // Prevents JavaScript access secure: true, // HTTPS only sameSite: 'strict', // CSRF protection maxAge: tokens.expiresIn * 1000 });
res.cookie('refreshToken', encryptedRefreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days });}Audit and monitoring
Section titled “Audit and monitoring”Track authorization decisions for security and compliance
- Log all access attempts, both successful and failed
- Monitor for unusual permission usage patterns
- Regularly audit user permissions and role assignments
- Implement alerts for privileged access usage
function auditAuthorizationDecision(user, action, resource, granted, context = {}) { const auditEntry = { timestamp: new Date().toISOString(), userId: user?.id, userEmail: user?.email, organizationId: user?.organizationId, action, resource, granted, userAgent: context.userAgent, ipAddress: context.ipAddress, sessionId: context.sessionId, // Include relevant permissions and roles for analysis userPermissions: user?.permissions || [], userRoles: user?.roles || [] };
// Send to your security monitoring system securityLogger.log('authorization_decision', auditEntry);
// Alert on suspicious patterns if (!granted && isPrivilegedAction(action)) { securityAlerting.checkForSuspiciousActivity(auditEntry); }}Performance optimization
Section titled “Performance optimization”Design authorization checks to be fast and efficient
- Cache user permissions in memory or fast storage
- Avoid database lookups during authorization checks
- Use Scalekit’s token-based approach to eliminate runtime permission queries
// ✅ Fast authorization using token datafunction hasPermission(user, permission) { // Permissions are already in the decoded token - no DB lookup needed return user.permissions?.includes(permission) || false;}
// ✅ Cache role hierarchies for complex checksconst roleHierarchyCache = new Map();
function getUserEffectivePermissions(user) { const cacheKey = `${user.organizationId}:${user.roles.join(',')}`;
if (roleHierarchyCache.has(cacheKey)) { return roleHierarchyCache.get(cacheKey); }
// Calculate effective permissions from roles const effectivePermissions = calculateEffectivePermissions(user.roles); roleHierarchyCache.set(cacheKey, effectivePermissions);
return effectivePermissions;}