CrackedRuby CrackedRuby

API Security Best Practices

Overview

API security encompasses the strategies, techniques, and controls that protect APIs from unauthorized access, data breaches, and malicious attacks. As APIs serve as the primary interface for modern applications to communicate with backend services and external systems, they represent critical attack surfaces that require defense at multiple layers.

The core challenge in API security stems from the tension between accessibility and protection. APIs must remain accessible to legitimate clients while blocking malicious actors. This balance requires implementing multiple security controls that work together: authentication verifies client identity, authorization controls resource access, encryption protects data in transit, rate limiting prevents abuse, and input validation blocks injection attacks.

API security differs from traditional web application security in several ways. APIs typically lack user interfaces, making CSRF tokens and same-origin policies less applicable. APIs often serve machine clients rather than human users, requiring different authentication mechanisms. APIs frequently expose more granular operations than web pages, increasing the attack surface. APIs may need to support multiple client types simultaneously, each with different security requirements.

The consequences of API security failures extend beyond data breaches. Compromised APIs can enable attackers to manipulate business logic, access unauthorized resources, perform privilege escalation, and launch attacks against other systems. The 2019 OWASP API Security Top 10 identified broken object level authorization, broken user authentication, and excessive data exposure as the most critical API security risks.

# Insecure API endpoint without authentication
get '/users/:id' do
  user = User.find(params[:id])
  user.to_json
end

# Secure API endpoint with authentication and authorization
get '/users/:id' do
  authenticate_request! # Verify valid token
  user = User.find(params[:id])
  authorize! :read, user # Check permission
  user.to_json(only: [:id, :name, :email]) # Limit exposed fields
end

Key Principles

Authentication and Identity Verification establishes who the client is before granting access. APIs should require authentication for all endpoints except those explicitly designed for public access. Token-based authentication using JWT or OAuth 2.0 provides stateless verification without session storage. API keys offer simpler authentication for service-to-service communication but require secure storage and rotation. Multi-factor authentication adds defense in depth for sensitive operations.

Authorization and Access Control determines what authenticated clients can access. Role-based access control (RBAC) assigns permissions based on user roles. Attribute-based access control (ABAC) makes decisions based on attributes of the user, resource, and environment. Object-level authorization verifies permission for each specific resource, preventing horizontal privilege escalation where users access data belonging to other users at the same privilege level. Function-level authorization prevents vertical privilege escalation by restricting administrative operations.

Data Protection and Encryption secures information throughout its lifecycle. Transport encryption using TLS 1.2 or higher protects data in transit from eavesdropping and tampering. APIs should enforce HTTPS exclusively and reject HTTP connections. Sensitive data in responses requires additional protection through field-level encryption or tokenization. Encryption at rest protects stored data from unauthorized access. APIs should minimize data exposure by returning only necessary fields and implementing field-level access control.

Input Validation and Sanitization defends against injection attacks and data corruption. APIs must validate all input parameters against expected types, formats, ranges, and patterns. Whitelist validation accepts only known-good values. String inputs require length limits and character set restrictions. Structured data needs schema validation. SQL injection, command injection, and XML external entity attacks all exploit insufficient input validation. Content-Type validation ensures the request body matches declared content type.

Rate Limiting and Throttling prevent abuse and ensure availability. Rate limits restrict the number of requests from a client within a time window. Throttling slows excessive requests without blocking them entirely. Different endpoints may require different rate limits based on computational cost and sensitivity. Rate limiting protects against brute force attacks, denial of service, and resource exhaustion. Implementation requires tracking request counts per client identifier across distributed systems.

Error Handling and Information Disclosure prevents leaking sensitive information through error messages. APIs should return generic error messages to clients while logging detailed errors internally. Stack traces, database errors, and filesystem paths expose system internals that aid attackers. HTTP status codes should match the actual error type. Rate limit errors should not reveal whether authentication succeeded. Timing attacks exploit variations in response times to infer system behavior.

Security Headers and CORS Policy configure browser security controls. The Content-Security-Policy header restricts resource loading. X-Content-Type-Options prevents MIME sniffing. Strict-Transport-Security enforces HTTPS. Cross-Origin Resource Sharing (CORS) headers control which domains can access the API from browsers. Overly permissive CORS policies allow attacks from malicious websites. The Access-Control-Allow-Origin header should specify exact allowed origins rather than using wildcards.

API Versioning and Deprecation maintains security as APIs evolve. Versioning allows removing insecure functionality without breaking existing clients. Deprecated endpoints should return warnings and eventual removal dates. Security patches may require forced migration to new versions. Version identifiers in URLs or headers indicate which API contract applies.

Ruby Implementation

Ruby APIs commonly use Rack-based frameworks like Rails, Sinatra, or Grape. Authentication typically relies on JSON Web Tokens with libraries like jwt or OAuth 2.0 with doorkeeper. Authorization uses pundit or cancancan for policy enforcement. Rate limiting employs rack-attack for request throttling.

Token-Based Authentication generates and verifies JWT tokens for stateless authentication. The token contains claims about the user and expires after a set duration. Tokens include signatures to prevent tampering.

require 'jwt'

class TokenAuth
  SECRET_KEY = ENV['JWT_SECRET_KEY']
  ALGORITHM = 'HS256'
  
  def self.encode(payload, expiration = 24.hours.from_now)
    payload[:exp] = expiration.to_i
    JWT.encode(payload, SECRET_KEY, ALGORITHM)
  end
  
  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM)
    HashWithIndifferentAccess.new(decoded[0])
  rescue JWT::ExpiredSignature
    raise AuthenticationError, 'Token expired'
  rescue JWT::DecodeError
    raise AuthenticationError, 'Invalid token'
  end
end

# Usage in API endpoint
class ApiController < ActionController::API
  before_action :authenticate_request
  
  private
  
  def authenticate_request
    token = request.headers['Authorization']&.split(' ')&.last
    raise AuthenticationError, 'Missing token' unless token
    
    @current_user_claims = TokenAuth.decode(token)
    @current_user = User.find(@current_user_claims[:user_id])
  rescue AuthenticationError => e
    render json: { error: 'Unauthorized' }, status: :unauthorized
  end
end

Authorization with Pundit separates authorization logic into policy objects. Each resource type has a policy class defining permitted operations. Policies receive the current user and resource as arguments.

# app/policies/article_policy.rb
class ArticlePolicy
  attr_reader :user, :article
  
  def initialize(user, article)
    @user = user
    @article = article
  end
  
  def show?
    article.published? || article.author == user || user.admin?
  end
  
  def update?
    article.author == user || user.admin?
  end
  
  def destroy?
    user.admin? || (article.author == user && article.created_at > 1.hour.ago)
  end
end

# app/controllers/articles_controller.rb
class ArticlesController < ApiController
  def show
    @article = Article.find(params[:id])
    authorize @article
    render json: @article
  end
  
  def update
    @article = Article.find(params[:id])
    authorize @article
    
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
end

Input Validation with Strong Parameters filters and validates request parameters. Rails strong parameters whitelist permitted attributes, preventing mass assignment vulnerabilities.

class UsersController < ApiController
  def create
    user_params = validate_user_params
    user = User.new(user_params)
    
    if user.save
      render json: user, status: :created
    else
      render json: { errors: user.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def validate_user_params
    params.require(:user).permit(:email, :name, :password, :password_confirmation)
  end
end

# Additional validation with custom constraints
class UserCreateParams
  include ActiveModel::Model
  include ActiveModel::Validations
  
  attr_accessor :email, :name, :password, :age
  
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :name, presence: true, length: { minimum: 2, maximum: 100 }
  validates :password, presence: true, length: { minimum: 12 }
  validates :age, numericality: { greater_than_or_equal_to: 13, less_than: 150 }
  
  def self.from_params(params)
    new(
      email: params[:email],
      name: params[:name],
      password: params[:password],
      age: params[:age]
    )
  end
end

Rate Limiting with Rack::Attack throttles requests based on client identifiers. Configuration specifies limits per endpoint and time window. Throttles apply to IP addresses or authenticated users.

# config/initializers/rack_attack.rb
class Rack::Attack
  # Throttle general API requests by IP
  throttle('api/ip', limit: 100, period: 1.minute) do |req|
    req.ip if req.path.start_with?('/api/')
  end
  
  # Throttle login attempts by email
  throttle('logins/email', limit: 5, period: 20.minutes) do |req|
    if req.path == '/api/login' && req.post?
      req.params['email'].to_s.downcase.presence
    end
  end
  
  # Throttle authenticated users by user ID
  throttle('api/user', limit: 300, period: 1.minute) do |req|
    if req.path.start_with?('/api/') && req.env['current_user']
      req.env['current_user'].id
    end
  end
  
  # Block requests from suspicious IPs
  blocklist('block suspicious ips') do |req|
    SuspiciousIp.where(ip: req.ip).exists?
  end
  
  # Custom response for throttled requests
  self.throttled_responder = lambda do |env|
    retry_after = env['rack.attack.match_data'][:period]
    [
      429,
      {'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s},
      [{error: 'Rate limit exceeded', retry_after: retry_after}.to_json]
    ]
  end
end

Secure Password Storage hashes passwords with bcrypt before storage. Bcrypt includes salt generation and cost factors to resist brute force attacks.

class User < ApplicationRecord
  has_secure_password
  
  validates :password, length: { minimum: 12 }, on: :create
  validates :password, format: { 
    with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
    message: 'must include uppercase, lowercase, number, and special character'
  }, on: :create
end

# Manual password hashing with bcrypt
require 'bcrypt'

class PasswordService
  COST = 12 # Higher cost increases computation time
  
  def self.hash_password(password)
    BCrypt::Password.create(password, cost: COST)
  end
  
  def self.verify_password(password, hash)
    BCrypt::Password.new(hash) == password
  end
end

HTTPS Enforcement redirects HTTP requests to HTTPS and sets security headers. Rails provides configuration options for forcing SSL connections.

# config/environments/production.rb
Rails.application.configure do
  config.force_ssl = true
  config.ssl_options = {
    redirect: {
      exclude: -> request { request.path.start_with?('/health') }
    },
    hsts: {
      expires: 1.year,
      subdomains: true,
      preload: true
    }
  }
end

# Custom middleware for additional security headers
class SecurityHeaders
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, response = @app.call(env)
    
    headers['X-Content-Type-Options'] = 'nosniff'
    headers['X-Frame-Options'] = 'DENY'
    headers['X-XSS-Protection'] = '1; mode=block'
    headers['Content-Security-Policy'] = "default-src 'self'"
    headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    
    [status, headers, response]
  end
end

Common Patterns

OAuth 2.0 Authorization separates authentication from resource access. Clients obtain access tokens from an authorization server, then present tokens to resource servers. The authorization code flow provides the most secure option for server-side applications.

# Using Doorkeeper gem for OAuth 2.0 provider
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  resource_owner_authenticator do
    User.find_by(id: session[:user_id]) || redirect_to(login_url)
  end
  
  admin_authenticator do
    User.find_by(id: session[:user_id])&.admin? || redirect_to(login_url)
  end
  
  access_token_expires_in 2.hours
  refresh_token_enabled true
  
  grant_flows %w[authorization_code client_credentials]
  
  # Require PKCE for authorization code grant
  force_ssl_in_redirect_uri true
end

# Protected resource endpoint
class ApiController < ApplicationController
  before_action :doorkeeper_authorize!
  
  def index
    current_user = User.find(doorkeeper_token.resource_owner_id)
    render json: { data: current_user.resources }
  end
  
  private
  
  def doorkeeper_unauthorized_render_options(error:)
    { json: { error: error.description } }
  end
end

API Key Authentication uses secret keys for service-to-service authentication. API keys should include sufficient entropy, rotate regularly, and scope to specific permissions.

class ApiKey < ApplicationRecord
  belongs_to :user
  
  before_create :generate_key
  
  validates :name, presence: true
  validates :key_hash, uniqueness: true
  
  scope :active, -> { where(revoked_at: nil) }
  
  def self.authenticate(key)
    key_hash = hash_key(key)
    api_key = active.find_by(key_hash: key_hash)
    api_key&.touch(:last_used_at)
    api_key
  end
  
  def revoke!
    update(revoked_at: Time.current)
  end
  
  private
  
  def generate_key
    key = SecureRandom.hex(32)
    self.key_hash = self.class.hash_key(key)
    self.key_prefix = key[0..7]
    key # Return key only at creation time
  end
  
  def self.hash_key(key)
    Digest::SHA256.hexdigest(key)
  end
end

# API key authentication middleware
class ApiKeyAuth
  def initialize(app)
    @app = app
  end
  
  def call(env)
    request = Rack::Request.new(env)
    api_key = extract_api_key(request)
    
    if api_key && (key_record = ApiKey.authenticate(api_key))
      env['current_api_key'] = key_record
      env['current_user'] = key_record.user
    end
    
    @app.call(env)
  end
  
  private
  
  def extract_api_key(request)
    request.env['HTTP_X_API_KEY'] || request.params['api_key']
  end
end

Request Signing verifies request integrity and authenticity. Clients sign requests using a shared secret or private key. Servers validate signatures before processing requests.

class RequestSigner
  def self.sign(method, path, body, timestamp, secret)
    string_to_sign = [method.upcase, path, body, timestamp].join("\n")
    OpenSSL::HMAC.hexdigest('SHA256', secret, string_to_sign)
  end
  
  def self.verify(request, secret, max_age = 5.minutes)
    signature = request.headers['X-Signature']
    timestamp = request.headers['X-Timestamp'].to_i
    
    raise AuthenticationError, 'Missing signature' unless signature
    raise AuthenticationError, 'Missing timestamp' if timestamp.zero?
    raise AuthenticationError, 'Request too old' if Time.at(timestamp) < max_age.ago
    
    expected_signature = sign(
      request.method,
      request.path,
      request.body.read,
      timestamp,
      secret
    )
    
    ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
  end
end

# Usage in controller
class WebhookController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def receive
    secret = ENV['WEBHOOK_SECRET']
    
    unless RequestSigner.verify(request, secret)
      render json: { error: 'Invalid signature' }, status: :unauthorized
      return
    end
    
    process_webhook(params)
    head :ok
  end
end

Scope-Based Authorization grants permissions at granular levels. Scopes define what operations a token can perform. Token introspection reveals granted scopes.

class TokenScope
  SCOPES = {
    'read:users' => 'Read user data',
    'write:users' => 'Modify user data',
    'read:articles' => 'Read articles',
    'write:articles' => 'Create and modify articles',
    'delete:articles' => 'Delete articles',
    'admin' => 'Administrative access'
  }.freeze
  
  def initialize(scope_string)
    @scopes = scope_string.to_s.split
  end
  
  def permits?(required_scope)
    @scopes.include?('admin') || @scopes.include?(required_scope)
  end
  
  def permits_all?(*required_scopes)
    required_scopes.all? { |scope| permits?(scope) }
  end
  
  def permits_any?(*required_scopes)
    required_scopes.any? { |scope| permits?(scope) }
  end
end

# Controller authorization with scopes
class ArticlesController < ApiController
  before_action :require_scopes
  
  def index
    render json: Article.all
  end
  
  def create
    article = Article.new(article_params)
    
    if article.save
      render json: article, status: :created
    else
      render json: { errors: article.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def require_scopes
    token_scopes = TokenScope.new(@current_user_claims[:scope])
    
    case action_name
    when 'index', 'show'
      unauthorized unless token_scopes.permits?('read:articles')
    when 'create', 'update'
      unauthorized unless token_scopes.permits?('write:articles')
    when 'destroy'
      unauthorized unless token_scopes.permits?('delete:articles')
    end
  end
  
  def unauthorized
    render json: { error: 'Insufficient permissions' }, status: :forbidden
  end
end

Practical Examples

Securing a User Profile API demonstrates authentication, authorization, and data filtering. Users retrieve their own profiles or public profiles of other users. Admin users access all fields.

class ProfilesController < ApiController
  before_action :authenticate_request
  before_action :load_profile
  before_action :authorize_access
  
  def show
    render json: filtered_profile_data
  end
  
  def update
    if @profile.update(profile_params)
      render json: filtered_profile_data
    else
      render json: { errors: @profile.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def load_profile
    @profile = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Profile not found' }, status: :not_found
  end
  
  def authorize_access
    case action_name
    when 'show'
      # Anyone can view public profiles
      return if @profile.public?
      # Users can view their own private profiles
      return if @current_user.id == @profile.id
      # Admins can view all profiles
      return if @current_user.admin?
      
      render json: { error: 'Access denied' }, status: :forbidden
    when 'update'
      unless @current_user.id == @profile.id || @current_user.admin?
        render json: { error: 'Access denied' }, status: :forbidden
      end
    end
  end
  
  def filtered_profile_data
    fields = if @current_user.admin?
      [:id, :email, :name, :created_at, :last_login, :status]
    elsif @current_user.id == @profile.id
      [:id, :email, :name, :bio, :created_at]
    else
      [:id, :name, :bio]
    end
    
    @profile.as_json(only: fields)
  end
  
  def profile_params
    params.require(:profile).permit(:name, :bio, :public)
  end
end

Implementing Secure File Upload validates file types, limits file sizes, and prevents path traversal attacks. Uploaded files store in secure locations with random filenames.

class FileUploadController < ApiController
  MAX_FILE_SIZE = 10.megabytes
  ALLOWED_CONTENT_TYPES = %w[image/jpeg image/png application/pdf].freeze
  
  def create
    file = params[:file]
    
    validate_file!(file)
    
    upload = Upload.create!(
      user: @current_user,
      filename: secure_filename(file.original_filename),
      content_type: file.content_type,
      size: file.size
    )
    
    storage_path = generate_storage_path(upload)
    File.open(storage_path, 'wb') do |f|
      f.write(file.read)
    end
    
    upload.update!(storage_path: storage_path)
    
    render json: upload, status: :created
  end
  
  private
  
  def validate_file!(file)
    raise ValidationError, 'No file provided' unless file
    raise ValidationError, 'File too large' if file.size > MAX_FILE_SIZE
    
    unless ALLOWED_CONTENT_TYPES.include?(file.content_type)
      raise ValidationError, 'Invalid file type'
    end
    
    # Verify content matches declared type
    detected_type = Marcel::MimeType.for(file.tempfile)
    unless detected_type == file.content_type
      raise ValidationError, 'File content does not match declared type'
    end
  end
  
  def secure_filename(original_filename)
    extension = File.extname(original_filename)
    "#{SecureRandom.uuid}#{extension}"
  end
  
  def generate_storage_path(upload)
    date_path = upload.created_at.strftime('%Y/%m/%d')
    Rails.root.join('storage', 'uploads', date_path, upload.filename)
  end
end

Building a Secure Payment API requires multiple security layers: HTTPS enforcement, request signing, idempotency keys, and audit logging.

class PaymentsController < ApiController
  before_action :authenticate_request
  before_action :verify_signature
  before_action :check_idempotency
  
  def create
    payment = Payment.new(payment_params)
    payment.user = @current_user
    payment.idempotency_key = request.headers['Idempotency-Key']
    
    if payment.save
      ProcessPaymentJob.perform_later(payment.id)
      log_payment_request(payment)
      render json: payment, status: :created
    else
      render json: { errors: payment.errors }, status: :unprocessable_entity
    end
  rescue PaymentError => e
    log_payment_error(e)
    render json: { error: e.message }, status: :unprocessable_entity
  end
  
  private
  
  def verify_signature
    unless RequestSigner.verify(request, @current_user.api_secret)
      render json: { error: 'Invalid signature' }, status: :unauthorized
    end
  end
  
  def check_idempotency
    idempotency_key = request.headers['Idempotency-Key']
    
    unless idempotency_key
      render json: { error: 'Idempotency-Key required' }, status: :bad_request
      return
    end
    
    existing = Payment.find_by(
      user: @current_user,
      idempotency_key: idempotency_key
    )
    
    if existing
      render json: existing, status: :ok
    end
  end
  
  def payment_params
    params.require(:payment).permit(:amount, :currency, :description)
  end
  
  def log_payment_request(payment)
    AuditLog.create!(
      user: @current_user,
      action: 'payment_created',
      resource_type: 'Payment',
      resource_id: payment.id,
      ip_address: request.remote_ip,
      user_agent: request.user_agent,
      details: { amount: payment.amount, currency: payment.currency }
    )
  end
  
  def log_payment_error(error)
    ErrorLog.create!(
      user: @current_user,
      error_type: error.class.name,
      message: error.message,
      backtrace: error.backtrace.first(10),
      ip_address: request.remote_ip
    )
  end
end

Common Pitfalls

Broken Object Level Authorization occurs when APIs fail to verify that the authenticated user has permission to access a specific resource. Attackers modify object identifiers in requests to access unauthorized data.

# VULNERABLE: No authorization check
class ArticlesController < ApiController
  def show
    article = Article.find(params[:id])
    render json: article
  end
end

# SECURE: Verify ownership or permission
class ArticlesController < ApiController
  def show
    article = @current_user.articles.find(params[:id])
    render json: article
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Not found' }, status: :not_found
  end
end

Excessive Data Exposure happens when APIs return complete objects without filtering sensitive fields. APIs should return only necessary data based on the client's permission level.

# VULNERABLE: Returns all user fields including sensitive data
def show
  user = User.find(params[:id])
  render json: user
end

# SECURE: Filter fields based on context
def show
  user = User.find(params[:id])
  
  if @current_user.admin?
    render json: user.as_json(except: [:password_digest])
  elsif @current_user.id == user.id
    render json: user.as_json(only: [:id, :email, :name, :created_at])
  else
    render json: user.as_json(only: [:id, :name, :public_bio])
  end
end

Mass Assignment Vulnerabilities allow attackers to modify fields they should not access by including unexpected parameters in requests. Strong parameters prevent this attack.

# VULNERABLE: Accepts all parameters
def update
  user = User.find(params[:id])
  user.update(params[:user])
  render json: user
end

# SECURE: Whitelist allowed parameters
def update
  user = User.find(params[:id])
  user.update(user_params)
  render json: user
end

private

def user_params
  allowed = [:name, :bio, :email]
  allowed << :role if @current_user.admin?
  params.require(:user).permit(allowed)
end

Insecure Direct Object References expose internal object identifiers that allow enumeration attacks. Using UUIDs or signed tokens prevents predictable identifier guessing.

# VULNERABLE: Sequential integer IDs
class User < ApplicationRecord
  # id: 1, 2, 3, 4... (easily enumerable)
end

# SECURE: Use UUIDs as primary keys
class User < ApplicationRecord
  self.primary_key = :uuid
  before_create :generate_uuid
  
  private
  
  def generate_uuid
    self.uuid = SecureRandom.uuid
  end
end

# SECURE: Use signed tokens for temporary access
class ShareLink
  def self.generate(resource)
    payload = {
      resource_type: resource.class.name,
      resource_id: resource.id,
      exp: 24.hours.from_now.to_i
    }
    TokenAuth.encode(payload)
  end
  
  def self.resolve(token)
    claims = TokenAuth.decode(token)
    claims[:resource_type].constantize.find(claims[:resource_id])
  end
end

Missing Rate Limiting enables brute force attacks, credential stuffing, and denial of service. Rate limits should apply per endpoint based on sensitivity.

# Without rate limiting, attackers can make unlimited login attempts
def login
  user = User.find_by(email: params[:email])
  
  if user&.authenticate(params[:password])
    render json: { token: TokenAuth.encode(user_id: user.id) }
  else
    render json: { error: 'Invalid credentials' }, status: :unauthorized
  end
end

# With rate limiting (requires Rack::Attack configuration)
throttle('logins/email', limit: 5, period: 20.minutes) do |req|
  if req.path == '/api/login' && req.post?
    req.params['email'].to_s.downcase.presence
  end
end

Insufficient Logging and Monitoring prevents detection of security incidents. APIs should log authentication attempts, authorization failures, and suspicious patterns.

class SecurityLogger
  def self.log_authentication(success:, user:, ip:, user_agent:)
    SecurityEvent.create!(
      event_type: success ? 'auth_success' : 'auth_failure',
      user_id: user&.id,
      ip_address: ip,
      user_agent: user_agent,
      timestamp: Time.current
    )
    
    # Alert on multiple failures
    if !success && recent_failures(ip) > 5
      AlertService.notify_suspicious_activity(ip)
    end
  end
  
  def self.recent_failures(ip)
    SecurityEvent.where(
      event_type: 'auth_failure',
      ip_address: ip,
      timestamp: 10.minutes.ago..Time.current
    ).count
  end
end

Testing Approaches

Authentication Testing verifies that endpoints reject unauthenticated requests and accept valid tokens. Tests should cover expired tokens, invalid signatures, and missing tokens.

RSpec.describe 'Articles API', type: :request do
  describe 'GET /api/articles' do
    it 'rejects requests without authentication' do
      get '/api/articles'
      expect(response).to have_http_status(:unauthorized)
    end
    
    it 'accepts valid authentication tokens' do
      user = create(:user)
      token = TokenAuth.encode(user_id: user.id)
      
      get '/api/articles', headers: { 'Authorization' => "Bearer #{token}" }
      expect(response).to have_http_status(:ok)
    end
    
    it 'rejects expired tokens' do
      user = create(:user)
      token = TokenAuth.encode({ user_id: user.id }, 1.hour.ago)
      
      get '/api/articles', headers: { 'Authorization' => "Bearer #{token}" }
      expect(response).to have_http_status(:unauthorized)
      expect(json_response['error']).to match(/expired/i)
    end
    
    it 'rejects tampered tokens' do
      user = create(:user)
      token = TokenAuth.encode(user_id: user.id)
      tampered_token = token[0..-5] + 'XXXX'
      
      get '/api/articles', headers: { 'Authorization' => "Bearer #{tampered_token}" }
      expect(response).to have_http_status(:unauthorized)
    end
  end
end

Authorization Testing ensures that users can only access permitted resources. Tests verify both horizontal and vertical privilege escalation prevention.

RSpec.describe 'User Profiles API', type: :request do
  let(:user) { create(:user) }
  let(:other_user) { create(:user) }
  let(:admin) { create(:user, :admin) }
  
  describe 'GET /api/users/:id' do
    it 'allows users to view their own profile' do
      get "/api/users/#{user.id}", headers: auth_headers(user)
      expect(response).to have_http_status(:ok)
    end
    
    it 'prevents users from viewing other private profiles' do
      other_user.update(public: false)
      get "/api/users/#{other_user.id}", headers: auth_headers(user)
      expect(response).to have_http_status(:forbidden)
    end
    
    it 'allows viewing public profiles' do
      other_user.update(public: true)
      get "/api/users/#{other_user.id}", headers: auth_headers(user)
      expect(response).to have_http_status(:ok)
    end
    
    it 'allows admins to view all profiles' do
      other_user.update(public: false)
      get "/api/users/#{other_user.id}", headers: auth_headers(admin)
      expect(response).to have_http_status(:ok)
    end
  end
  
  describe 'PATCH /api/users/:id' do
    it 'prevents users from updating other profiles' do
      patch "/api/users/#{other_user.id}", 
            params: { user: { name: 'Hacked' } },
            headers: auth_headers(user)
      
      expect(response).to have_http_status(:forbidden)
      expect(other_user.reload.name).not_to eq('Hacked')
    end
    
    it 'prevents users from elevating their own privileges' do
      patch "/api/users/#{user.id}",
            params: { user: { role: 'admin' } },
            headers: auth_headers(user)
      
      expect(user.reload.role).not_to eq('admin')
    end
  end
end

Input Validation Testing checks that invalid inputs are rejected and do not cause errors or security issues. Tests cover SQL injection, XSS, and command injection attempts.

RSpec.describe 'Search API', type: :request do
  let(:user) { create(:user) }
  
  describe 'GET /api/search' do
    it 'rejects SQL injection attempts' do
      malicious_queries = [
        "'; DROP TABLE users; --",
        "1' OR '1'='1",
        "admin'--"
      ]
      
      malicious_queries.each do |query|
        get '/api/search', 
            params: { q: query },
            headers: auth_headers(user)
        
        expect(response).to have_http_status(:ok)
        expect(User.count).to be > 0 # Users table still exists
      end
    end
    
    it 'rejects overly long search queries' do
      long_query = 'a' * 1001
      get '/api/search',
          params: { q: long_query },
          headers: auth_headers(user)
      
      expect(response).to have_http_status(:bad_request)
    end
    
    it 'handles special characters safely' do
      special_chars = ['<script>', '${code}', '../../../etc/passwd']
      
      special_chars.each do |chars|
        get '/api/search',
            params: { q: chars },
            headers: auth_headers(user)
        
        expect(response).to have_http_status(:ok)
      end
    end
  end
end

Rate Limiting Testing verifies that excessive requests are throttled. Tests should cover different rate limit tiers and ensure counters reset correctly.

RSpec.describe 'Rate Limiting', type: :request do
  let(:user) { create(:user) }
  
  describe 'API request throttling' do
    it 'allows requests under the rate limit' do
      5.times do
        get '/api/articles', headers: auth_headers(user)
        expect(response).to have_http_status(:ok)
      end
    end
    
    it 'blocks requests exceeding the rate limit' do
      # Assume limit is 10 requests per minute
      11.times do |i|
        get '/api/articles', headers: auth_headers(user)
        
        if i < 10
          expect(response).to have_http_status(:ok)
        else
          expect(response).to have_http_status(:too_many_requests)
          expect(response.headers['Retry-After']).to be_present
        end
      end
    end
    
    it 'applies different limits to different endpoints' do
      # Login endpoint has stricter limits
      6.times do
        post '/api/login', params: { email: 'test@example.com', password: 'wrong' }
      end
      
      expect(response).to have_http_status(:too_many_requests)
      
      # But general API still works
      get '/api/articles', headers: auth_headers(user)
      expect(response).to have_http_status(:ok)
    end
  end
end

Reference

Authentication Methods

Method Use Case Security Level Implementation Complexity
JWT Stateless authentication, microservices High with proper secret management Medium
OAuth 2.0 Third-party access, delegated authorization High High
API Keys Service-to-service authentication Medium Low
Session Cookies Traditional web apps, same-domain Medium Low
mTLS High-security service communication Very High High

Common HTTP Status Codes for Security

Status Code Meaning When to Use
401 Unauthorized Missing or invalid authentication Authentication failed or missing
403 Forbidden Authenticated but insufficient permissions Authorization check failed
429 Too Many Requests Rate limit exceeded Request throttling activated
400 Bad Request Invalid input parameters Validation failed
422 Unprocessable Entity Semantically invalid request Business logic validation failed

Security Headers

Header Purpose Example Value
Strict-Transport-Security Enforce HTTPS max-age=31536000; includeSubDomains
Content-Security-Policy Restrict resource loading default-src 'self'
X-Content-Type-Options Prevent MIME sniffing nosniff
X-Frame-Options Prevent clickjacking DENY
X-XSS-Protection Enable XSS filtering 1; mode=block

Input Validation Checklist

Input Type Validation Requirements
Email Format validation, length limit, normalization
Password Minimum length, complexity requirements, no common passwords
Integer Range validation, type coercion, overflow protection
String Length limit, character whitelist, encoding validation
File Upload Size limit, type validation, content verification, filename sanitization
URL Protocol whitelist, domain validation, SSRF protection
JSON Schema validation, depth limit, size limit

Rate Limiting Strategies

Strategy Description Use Case
Fixed Window Count resets at fixed intervals Simple implementations
Sliding Window Rolling time window More accurate throttling
Token Bucket Allows bursts up to bucket size APIs with variable load
Leaky Bucket Smooth rate limiting Prevent sudden spikes
Per-User Different limits per user tier Tiered service plans

JWT Claims

Claim Type Purpose
iss Standard Token issuer
sub Standard Subject identifier
aud Standard Intended audience
exp Standard Expiration time
iat Standard Issued at time
scope Custom Permission scopes
user_id Custom User identifier

Common Vulnerabilities

Vulnerability Impact Mitigation
SQL Injection Database compromise Parameterized queries, ORM usage
XSS Session hijacking Output encoding, CSP headers
CSRF Unauthorized actions Token verification, SameSite cookies
IDOR Unauthorized data access Object-level authorization checks
Mass Assignment Privilege escalation Strong parameters, field whitelisting
XXE File disclosure, SSRF Disable external entities, input validation