Overview
User management encompasses the processes and systems that control who can access an application and what actions they can perform. This includes creating user accounts, authenticating identity, authorizing access to resources, and maintaining user data throughout the account lifecycle. Permission systems determine access rights based on policies that map users or groups to actions and resources.
The distinction between authentication and authorization defines the foundation of access control. Authentication verifies identity—confirming that users are who they claim to be through credentials like passwords, tokens, or biometric data. Authorization determines permissions—deciding what authenticated users can access or modify based on their roles, attributes, or explicit grants.
Modern applications require user management systems that handle millions of users, enforce fine-grained permissions, audit access patterns, and integrate with external identity providers. A banking application might authenticate users through multi-factor authentication, then authorize specific account access based on ownership relationships and transaction types. An enterprise system might authenticate against corporate LDAP, then apply role-based rules to determine which modules each user can access.
# Authentication verifies identity
user = User.authenticate(email, password)
# Authorization checks permissions
if user.can?(:edit, document)
document.update(params)
end
The complexity of permission systems scales with application requirements. A blog might need only admin and user roles. An enterprise resource planning system might require hierarchical roles, department-based access, data-level security rules, and temporal permissions that change based on business processes. Healthcare systems must enforce HIPAA compliance through strict access controls and comprehensive audit logs. Multi-tenant SaaS applications need isolation between customer data while supporting shared administrative hierarchies.
Key Principles
Identity Management establishes and maintains user accounts. Each user receives a unique identifier that persists across sessions. Account data includes credentials for authentication, profile information, contact details, and metadata like creation timestamps and login history. Password storage requires cryptographic hashing with salt values—never store passwords in plain text or reversibly encrypted form. Modern systems use algorithms like bcrypt, scrypt, or Argon2 that include computational cost factors to resist brute-force attacks.
Authentication Mechanisms verify user identity through multiple approaches. Password-based authentication remains common but requires protection against attacks like credential stuffing, where attackers try leaked credentials from other breaches. Token-based authentication issues signed tokens after password verification, allowing subsequent requests to proceed without re-entering credentials. OAuth 2.0 and OpenID Connect enable federated authentication where users authenticate through third-party providers like Google or GitHub. Multi-factor authentication adds layers beyond passwords—typically something the user knows (password), something they have (phone or hardware token), and something they are (biometric).
# Password hashing with bcrypt
class User < ApplicationRecord
has_secure_password
def self.authenticate(email, password)
user = find_by(email: email)
user&.authenticate(password)
end
end
# Token-based session
class SessionsController < ApplicationController
def create
user = User.authenticate(params[:email], params[:password])
if user
token = JWT.encode({ user_id: user.id }, secret_key)
render json: { token: token }
else
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
end
Authorization Models determine access rights through different approaches. Discretionary Access Control (DAC) allows resource owners to grant permissions—a user who creates a document can decide who else can read or edit it. Mandatory Access Control (MAC) enforces system-level policies that users cannot override, common in military and government systems with security clearances. Role-Based Access Control (RBAC) assigns permissions to roles, then assigns roles to users—a user with the "editor" role inherits all permissions granted to that role. Attribute-Based Access Control (ABAC) evaluates policies against user attributes, resource properties, and environmental conditions.
Permission Granularity ranges from coarse to fine. Application-level permissions control access to entire features—can this user access the admin panel? Resource-level permissions control specific objects—can this user edit this particular document? Field-level permissions restrict individual attributes—can this user see salary information in employee records? Action-level permissions distinguish between operations—a user might read documents but not delete them. Temporal permissions add time constraints—a user might have elevated access during their on-call shift.
Permission Inheritance reduces redundancy through hierarchies. Role inheritance allows senior roles to include permissions from junior roles—a manager role might inherit all employee role permissions plus additional management capabilities. Group inheritance propagates permissions through organizational structures—users in the engineering department might automatically receive permissions assigned to that department. Resource inheritance applies parent permissions to children—permissions on a folder can propagate to contained files.
Access Control Lists (ACLs) explicitly map principals to permissions for each resource. An ACL entry specifies a subject (user or group), an action (read, write, delete), and a permission (allow or deny). ACLs provide maximum flexibility but become difficult to manage at scale—a system with thousands of users and millions of resources generates enormous ACL tables.
# ACL example
class Document < ApplicationRecord
has_many :access_grants
def grant_access(user, permission_level)
access_grants.create(
user: user,
can_read: [:read, :write, :admin].include?(permission_level),
can_write: [:write, :admin].include?(permission_level),
can_delete: permission_level == :admin
)
end
def accessible_by?(user, action)
grant = access_grants.find_by(user: user)
case action
when :read then grant&.can_read
when :write then grant&.can_write
when :delete then grant&.can_delete
end
end
end
Common Patterns
Role-Based Access Control (RBAC) organizes permissions into roles that group related capabilities. A typical web application defines roles like guest, user, moderator, and admin. Each role receives a set of permissions—moderators might delete inappropriate comments and ban users, while regular users can only create and edit their own content. Users receive role assignments that determine their effective permissions.
RBAC implementations vary in complexity. Flat RBAC assigns roles directly to users without hierarchies. Hierarchical RBAC organizes roles in trees where senior roles inherit junior role permissions. Constrained RBAC adds rules like separation of duties—a user cannot simultaneously hold roles that would create conflicts of interest, like both initiating and approving purchase orders.
class User < ApplicationRecord
has_and_belongs_to_many :roles
def has_role?(role_name)
roles.exists?(name: role_name)
end
def can?(action, resource)
roles.any? { |role| role.can?(action, resource) }
end
end
class Role < ApplicationRecord
has_and_belongs_to_many :permissions
has_and_belongs_to_many :users
def can?(action, resource)
permissions.exists?(
action: action,
resource_type: resource.class.name
)
end
end
# Usage
user.has_role?('admin') # Check role membership
user.can?(:delete, post) # Check permission
Attribute-Based Access Control (ABAC) evaluates policies against attributes from multiple sources. Subject attributes describe the user—department, clearance level, employment status. Resource attributes describe the object being accessed—classification, owner, creation date. Environmental attributes capture context—time of day, IP address, device type. Action attributes specify the operation—read, write, approve.
ABAC policies express rules in formats like XACML (eXtensible Access Control Markup Language). A policy might state: "Users in the finance department can read financial reports if the report classification matches or is below their clearance level and the access occurs during business hours from a corporate IP address."
class AccessPolicy
def self.evaluate(user, action, resource, context)
# Attribute-based policy evaluation
return false unless user.active?
return false unless business_hours?(context[:time])
return false unless corporate_network?(context[:ip])
case action
when :read
user.clearance_level >= resource.classification &&
user.department == resource.department
when :write
user.clearance_level >= resource.classification &&
resource.owner_id == user.id
when :approve
user.has_role?('manager') &&
user.department == resource.department &&
resource.amount <= user.approval_limit
end
end
def self.business_hours?(time)
time.hour.between?(9, 17) && time.wday.between?(1, 5)
end
def self.corporate_network?(ip)
corporate_ranges = ['10.0.0.0/8', '192.168.0.0/16']
corporate_ranges.any? { |range| IPAddr.new(range).include?(ip) }
end
end
Policy-Based Access Control separates authorization logic from application code. Policies exist as external rules that administrators can modify without code changes. This pattern works well with ABAC, where complex rules benefit from centralized management. Policy engines evaluate requests against stored policies and return access decisions.
Capability-Based Security issues unforgeable tokens that grant specific permissions. Rather than checking whether a user can access a resource, the system checks whether the request includes a valid capability token for that operation. This inverts the typical security model—possession of the capability proves authorization. Capability tokens typically include the granted permission, the resource identifier, an expiration time, and a cryptographic signature.
class Capability
def self.generate(user, resource, action, expires_at)
payload = {
user_id: user.id,
resource_type: resource.class.name,
resource_id: resource.id,
action: action,
exp: expires_at.to_i
}
JWT.encode(payload, Rails.application.secret_key_base)
end
def self.verify(token, resource, action)
payload = JWT.decode(token, Rails.application.secret_key_base)[0]
payload['resource_type'] == resource.class.name &&
payload['resource_id'] == resource.id &&
payload['action'] == action.to_s &&
Time.at(payload['exp']) > Time.now
rescue JWT::DecodeError
false
end
end
# Usage: generate a temporary access token
token = Capability.generate(user, document, :read, 1.hour.from_now)
# Later: verify the capability
if Capability.verify(token, document, :read)
# Proceed with read operation
end
Row-Level Security restricts database query results based on user permissions. Instead of loading all records and filtering in application code, the database applies security predicates to queries. PostgreSQL row-level security defines policies directly in the database that automatically filter results based on current user context. This prevents security bypasses from query optimization or caching issues.
Design Considerations
Centralized vs Decentralized Authorization presents architectural tradeoffs. Centralized authorization maintains all permission logic in dedicated services or modules. Applications query the authorization service to check permissions, which evaluates policies against a central permission store. This approach ensures consistency—all applications enforce the same rules—and simplifies policy updates. However, it creates a single point of failure and adds network latency to every authorization check.
Decentralized authorization embeds permission checks throughout the application. Each module or service maintains its own permission logic and data. This reduces latency and eliminates dependencies on authorization services, but creates consistency challenges. Updates to permission policies require coordinating changes across multiple systems.
Hybrid approaches balance these tradeoffs. Core authorization logic centralizes in shared libraries or services, while applications cache permission results to reduce latency. Permission caches require invalidation strategies when policies change—typically through publish-subscribe patterns or time-based expiration.
Static vs Dynamic Roles affects system flexibility. Static roles remain constant—defined at deployment time and rarely change. An accounting system might define roles like accountant, auditor, and controller with fixed permissions. Static roles simplify reasoning about security and perform efficiently, but resist change. Adding a new role requires code deployment.
Dynamic roles allow runtime definition and modification. Administrators create roles through user interfaces and assign permissions without code changes. This flexibility accommodates evolving requirements but complicates security analysis—developers cannot enumerate all possible roles at development time. Dynamic roles require careful access control on the role management interface itself.
Permission Checking Strategies determine when to evaluate authorization. Pre-authorization checks permissions before executing operations. The application verifies access rights, then proceeds if authorized. This approach fails fast—unauthorized requests never reach business logic. However, pre-authorization cannot always determine permissions without executing partial operations. Approving a purchase requisition might require checking budget availability, which involves calculations that occur during processing.
Post-authorization validates results after execution. The system performs operations, then filters results based on permissions before returning data. Database queries using row-level security follow this pattern—the query executes, then security predicates filter rows. Post-authorization complicates error handling—partial failures might leave the system in inconsistent states.
Permission Denial Semantics defines how to handle missing permissions. Explicit denial requires explicit permission grants—absence of a grant means denial. This conservative approach defaults to denying access, improving security at the cost of initial setup overhead. Administrators must grant every permission users need.
Permissive defaults allow access unless explicitly denied. New resources default to accessible until administrators restrict them. This reduces initial configuration but creates security risks—new features might inadvertently expose sensitive data.
Multi-Tenancy Patterns isolate customer data in shared systems. Database-per-tenant provides strongest isolation—each customer receives a separate database. This simplifies permissions since database-level access control provides tenant boundaries, but increases infrastructure costs and complicates cross-tenant analytics. Schema-per-tenant shares databases but isolates data in separate schemas. Row-level tenant discrimination stores all data in shared tables with tenant identifier columns. Applications must filter all queries by tenant ID and verify users cannot access other tenants' data.
Ruby Implementation
Ruby applications typically use authentication gems like Devise or authorization gems like Pundit and CanCanCan. These libraries integrate with Rails applications through conventions and modules.
Devise handles user authentication, registration, password reset, account confirmation, and session management. It generates User model code, database migrations, and controller actions for authentication workflows.
# Gemfile
gem 'devise'
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :timeoutable, :trackable
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate_user!
def current_user
@current_user ||= super
end
end
# Usage in controllers
class DocumentsController < ApplicationController
def show
# current_user automatically available
@document = Document.find(params[:id])
end
end
Pundit provides policy-based authorization through policy objects that encapsulate permission logic. Each model receives a corresponding policy class defining authorization rules.
# Gemfile
gem 'pundit'
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit::Authorization
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "Access denied"
redirect_to(request.referrer || root_path)
end
end
# app/policies/document_policy.rb
class DocumentPolicy < ApplicationPolicy
def show?
record.public? || record.user_id == user.id || user.admin?
end
def update?
user.present? && (record.user_id == user.id || user.admin?)
end
def destroy?
user.admin? || (record.user_id == user.id && record.created_at > 1.hour.ago)
end
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(user_id: user.id).or(scope.where(public: true))
end
end
end
end
# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
def index
@documents = policy_scope(Document)
end
def show
@document = Document.find(params[:id])
authorize @document
end
def update
@document = Document.find(params[:id])
authorize @document
@document.update(document_params)
end
end
Pundit policies receive the current user and record being authorized. The policy defines methods corresponding to controller actions—show?, update?, destroy?—that return boolean values. The Scope inner class defines query scopes that filter collections based on user permissions.
CanCanCan uses ability classes that define permissions in a DSL. The ability class centralizes authorization rules for the entire application.
# Gemfile
gem 'cancancan'
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
elsif user.moderator?
can :read, :all
can :manage, Comment
can :update, Document, user_id: user.id
else
can :read, Document, public: true
can :manage, Document, user_id: user.id
can :create, Comment
can :manage, Comment, user_id: user.id
end
cannot :destroy, Document do |document|
document.created_at < 1.day.ago
end
end
end
# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
load_and_authorize_resource
def index
# @documents automatically loaded and filtered
end
def show
# @document automatically loaded and authorized
end
def update
# Authorization checked automatically
@document.update(document_params)
end
end
CanCanCan's load_and_authorize_resource automatically loads model instances and checks permissions before actions execute. The ability class uses can and cannot declarations to specify permissions, with optional conditions as hashes or blocks.
Custom Permission Systems offer full control at the cost of implementation effort. A custom system might store permissions in a database with polymorphic associations.
# app/models/permission.rb
class Permission < ApplicationRecord
belongs_to :subject, polymorphic: true # User or Role
belongs_to :resource, polymorphic: true # Any model
enum action: { read: 0, write: 1, delete: 2, admin: 3 }
def self.grant(subject, action, resource)
find_or_create_by(
subject: subject,
action: action,
resource: resource
)
end
def self.revoke(subject, action, resource)
where(
subject: subject,
action: action,
resource: resource
).destroy_all
end
def self.authorized?(user, action, resource)
exists?(
subject: user,
action: action,
resource: resource
) || exists?(
subject: user.roles,
action: action,
resource: resource
)
end
end
# app/models/concerns/authorizable.rb
module Authorizable
extend ActiveSupport::Concern
included do
has_many :permissions, as: :resource, dependent: :destroy
end
def grant_permission(subject, action)
Permission.grant(subject, action, self)
end
def revoke_permission(subject, action)
Permission.revoke(subject, action, self)
end
def accessible_by?(user, action)
Permission.authorized?(user, action, self)
end
end
# Usage
class Document < ApplicationRecord
include Authorizable
end
document = Document.create(title: "Confidential Report")
document.grant_permission(user, :read)
document.grant_permission(admin_role, :admin)
if document.accessible_by?(current_user, :read)
# Proceed with access
end
Security Implications
Password Security requires multiple defensive layers. Passwords must hash with algorithms designed for credential storage—bcrypt, scrypt, or Argon2. These algorithms include computational cost factors that slow brute-force attacks. Never use fast cryptographic hashes like SHA-256 for passwords, as attackers can test billions of passwords per second with specialized hardware.
Salt values prevent rainbow table attacks where pre-computed hash tables accelerate password cracking. Each password receives a unique random salt before hashing. Even if two users have identical passwords, their hashes differ due to different salts. Modern password hashing algorithms include salts automatically.
Password policies balance security and usability. Minimum length matters more than complexity requirements—a 16-character passphrase of common words resists attacks better than an 8-character string with mixed case and special characters. Forcing frequent password changes often reduces security by encouraging users to choose weaker passwords or reuse passwords with minor variations. Password strength meters educate users about weak choices without arbitrary rules.
# Secure password hashing with bcrypt
require 'bcrypt'
class User < ApplicationRecord
validates :password, length: { minimum: 12 }, if: :password_required?
def password=(new_password)
@password = new_password
self.password_digest = BCrypt::Password.create(new_password, cost: 12)
end
def authenticate(attempt)
BCrypt::Password.new(password_digest).is_password?(attempt) && self
end
end
# Checking password strength
require 'zxcvbn'
def check_password_strength(password)
result = Zxcvbn.test(password)
if result.score < 3
errors.add(:password, "is too weak: #{result.feedback.warning}")
end
end
Session Management controls authenticated user state. Session tokens authenticate subsequent requests after login. Session fixation attacks provide victims with attacker-controlled session IDs, then wait for victims to authenticate using those IDs. Regenerating session IDs after authentication prevents this attack—the authentication process creates a new session ID that attackers do not know.
Session tokens require secure generation using cryptographic random number generators. Predictable session IDs allow attackers to guess valid tokens. Sessions need timeout mechanisms—absolute timeouts expire sessions after fixed durations regardless of activity, while idle timeouts expire sessions after inactivity periods. Different security contexts require different timeout values—banking applications might use 10-minute idle timeouts, while content sites might allow hours.
Token storage location affects security. Server-side sessions store session data on the server with only a session ID sent to clients. This prevents tampering but requires server state and session storage. Client-side tokens contain signed data that clients cannot modify. JSON Web Tokens (JWT) follow this pattern—tokens contain claims encoded as JSON, signed with server secrets. Clients include tokens in request headers, and servers verify signatures before trusting token contents.
# Secure session configuration in Rails
Rails.application.config.session_store :cookie_store,
key: '_app_session',
secure: Rails.env.production?, # HTTPS only in production
httponly: true, # Not accessible to JavaScript
same_site: :strict # CSRF protection
# JWT token management
class JsonWebToken
SECRET_KEY = Rails.application.secrets.secret_key_base
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET_KEY)
end
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY)[0]
HashWithIndifferentAccess.new(decoded)
rescue JWT::DecodeError, JWT::ExpiredSignature
nil
end
end
# Regenerate session after authentication
class SessionsController < ApplicationController
def create
user = User.authenticate(params[:email], params[:password])
if user
reset_session # Prevent session fixation
session[:user_id] = user.id
redirect_to dashboard_path
end
end
end
Cross-Site Request Forgery (CSRF) tricks authenticated users into executing unwanted actions. An attacker embeds requests to target applications in malicious sites. When victims visit attacker sites while authenticated to target applications, browsers automatically include authentication cookies, causing unwanted operations to execute with victim credentials.
CSRF tokens prevent these attacks by requiring additional proof that requests originate from legitimate application pages. The server generates unpredictable tokens associated with user sessions and embeds them in forms. Submitted requests must include valid tokens that match session values. Attackers cannot access tokens due to same-origin policies.
Rails includes CSRF protection by default through the protect_from_forgery method. APIs using token authentication rather than cookies require different CSRF considerations—bearer tokens in authorization headers do not automatically attach to requests, providing inherent CSRF protection.
Privilege Escalation vulnerabilities allow users to gain permissions beyond their authorization. Vertical privilege escalation elevates users from low privileges to high privileges—a regular user becomes an administrator. Horizontal privilege escalation allows users to access resources belonging to other users at the same privilege level—one user accesses another user's documents.
Parameter tampering enables privilege escalation when applications trust client-supplied data. A user modifies a role parameter in a profile update request, changing their role to admin. Applications must validate all permission-related parameters and verify users have authority to make requested changes.
Insecure direct object references expose internal implementation details that enable privilege escalation. Sequential database IDs in URLs allow users to iterate through other users' resources by modifying ID parameters. Applications must verify authorization for every requested resource, not just check authentication.
# Vulnerable code - missing authorization check
def show
@document = Document.find(params[:id]) # No permission check
end
# Secure code - verify authorization
def show
@document = Document.find(params[:id])
unless @document.accessible_by?(current_user, :read)
raise Pundit::NotAuthorizedError
end
end
# Prevent parameter tampering
def update
@user = User.find(params[:id])
authorize @user
# Only allow safe parameters
safe_params = params.require(:user).permit(:email, :name)
# Never permit role or admin flags from user input
@user.update(safe_params)
end
Audit Logging records access attempts and permission changes for security monitoring and compliance. Logs capture who accessed what resources, when access occurred, what actions executed, and whether the system allowed or denied access. Audit logs must be tamper-evident—unauthorized modification detection prevents attackers from hiding traces.
Audit logs require careful design to balance detail and performance. Logging every database read might generate excessive data. Focus on security-relevant events—authentication attempts, permission changes, access to sensitive resources, and privileged operations.
Common Pitfalls
Confusing Authentication and Authorization creates security gaps. Checking that users are authenticated confirms identity but not permissions. An application might verify login, then assume authenticated users can access all resources. Every operation needs both authentication and authorization—verify who the user is, then verify what they can do.
Client-Side Authorization relies on untrusted code. JavaScript that hides UI elements based on roles does not prevent unauthorized access—attackers bypass client-side checks by directly accessing APIs. The server must validate every request regardless of what the client displays.
# Bad: Only hiding UI elements
# In view template
<% if current_user.admin? %>
<%= link_to 'Delete', document_path(@document), method: :delete %>
<% end %>
# Without server-side authorization, users can still send DELETE requests
# Good: Server-side authorization
def destroy
@document = Document.find(params[:id])
authorize @document # Verify permission on server
@document.destroy
end
Mass Assignment Vulnerabilities allow users to modify unauthorized attributes. When controllers pass all parameters to model update methods, users can inject additional parameters. A user adds an admin: true parameter to profile update requests, elevating privileges. Always use strong parameters to explicitly allow only safe attributes.
Default Deny Failures occur when new features default to accessible. A new endpoint lacks authorization checks, inadvertently exposing data. Establish authorization as the default—require explicit permission grants rather than explicit denials. Use controller filters like before_action :authenticate_user! to enforce authentication on all actions unless explicitly skipped.
Insecure Permission Checks trust user input or inadequate validation. Checking if a user owns a resource by comparing parameters to user IDs fails if attackers modify parameters. Always verify ownership through trusted data sources, not client-supplied values.
# Vulnerable: Trusting user_id parameter
def show
if params[:user_id] == current_user.id # Attacker controls params
@document = Document.find(params[:id])
end
end
# Secure: Verify through trusted relationship
def show
@document = current_user.documents.find(params[:id]) # Implicit ownership
end
Race Conditions in Permission Checks cause time-of-check-time-of-use vulnerabilities. An application checks permissions, then performs an operation. Between these steps, permissions change—a user's role is revoked before the operation executes. Database transactions help but do not fully solve race conditions. Permission checks should occur as close as possible to protected operations, ideally atomically.
Excessive Permissions grant overly broad access to simplify development. Making all users administrators during testing leaves debug permissions in production. Applying least privilege—granting only minimum required permissions—reduces damage from compromised accounts.
Missing Authorization on API Endpoints occurs when developers focus on web interface security but forget API protections. REST APIs and GraphQL endpoints need the same authorization checks as web controllers. API authentication often uses different mechanisms like API keys or OAuth tokens rather than session cookies.
Cached Permission Decisions become stale when permissions change. An application caches that a user can access a resource, then revokes their permission. The cache returns the old decision until expiration. Cache invalidation strategies must account for permission changes, either through event-based invalidation or short TTLs.
Ignoring Indirect Access Paths focuses authorization on primary interfaces but misses alternate routes. File upload features might check permissions for upload actions but not for public URL access to uploaded files. Webhooks might trigger operations without permission checks. Every access path needs authorization.
Reference
Authentication Methods
| Method | Description | Security Level | Use Case |
|---|---|---|---|
| Password | User credentials verified against stored hash | Medium | General applications |
| Multi-Factor | Password plus secondary factor | High | Financial, enterprise |
| Token | Signed token from authentication service | Medium-High | APIs, mobile apps |
| OAuth/OpenID | Federated identity through third-party | Varies | Social login, SSO |
| Certificate | X.509 certificate verification | High | Enterprise, device auth |
| Biometric | Fingerprint, facial recognition | High | Mobile, physical access |
Authorization Models
| Model | Permissions Based On | Advantages | Limitations |
|---|---|---|---|
| RBAC | User roles | Simple, clear role mapping | Rigid, role explosion |
| ABAC | User/resource attributes | Flexible, context-aware | Complex policies |
| ACL | Per-resource permissions | Fine-grained control | Difficult at scale |
| PBAC | Centralized policies | Consistent, manageable | Requires policy engine |
| MAC | System security labels | Enforced compliance | Inflexible |
Common Permissions
| Permission | Description | Typical Roles |
|---|---|---|
| Read | View resource contents | All authenticated users |
| Write | Modify existing resources | Content creators, editors |
| Create | Generate new resources | Authenticated users |
| Delete | Remove resources | Owners, administrators |
| Admin | Full control, manage permissions | System administrators |
| Approve | Authorize pending actions | Managers, approvers |
| Audit | View logs, access history | Auditors, compliance |
Session Security Checklist
| Requirement | Implementation | Purpose |
|---|---|---|
| Secure flag | Set on production cookies | HTTPS-only transmission |
| HttpOnly flag | Enable on session cookies | Prevent JavaScript access |
| SameSite | Strict or Lax mode | CSRF protection |
| Regeneration | New ID after authentication | Session fixation prevention |
| Timeout | Idle and absolute limits | Limit exposure window |
| Secure storage | Server-side or signed tokens | Prevent tampering |
Rails Authorization Gems
| Gem | Approach | Best For |
|---|---|---|
| Pundit | Policy objects per model | Explicit, testable policies |
| CanCanCan | Centralized ability class | Simple RBAC |
| ActionPolicy | Performance-focused policies | High-traffic applications |
| Rolify | Dynamic role management | User-managed roles |
Permission Query Patterns
# User capability check
user.can?(:read, document)
# Policy authorization
authorize document, :update?
# Scope filtering
policy_scope(Document)
# Explicit permission grant
document.grant_permission(user, :read)
# Role membership
user.has_role?(:admin)
# Attribute-based check
policy.evaluate(user, :approve, expense, context)
Security Audit Events
| Event | Log Fields | Retention |
|---|---|---|
| Login attempt | User ID, IP, timestamp, success | 90 days |
| Permission change | Subject, resource, action, modifier | 1 year |
| Failed authorization | User ID, resource, attempted action | 90 days |
| Password change | User ID, timestamp, method | 1 year |
| Role assignment | User ID, role, granter, timestamp | 2 years |
| Sensitive access | User ID, resource type, timestamp | 7 years |