class CreateApiKeys < ActiveRecord::Migration[6.1]
def change
create_table :api_keys do |t|
t.references :user, null: false, foreign_key: true
t.string :name, null: false
t.string :key_digest, null: false
t.string :key_prefix, null: false
t.text :scopes, array: true, default: []
t.datetime :last_used_at
t.datetime :expires_at
t.timestamps
end
add_index :api_keys, :key_digest, unique: true
add_index :api_keys, :key_prefix
end
end
class ApiKey < ApplicationRecord
belongs_to :user
before_create :generate_key
validates :name, presence: true
def self.authenticate(key)
return nil if key.blank?
digest = Digest::SHA256.hexdigest(key)
api_key = find_by(key_digest: digest)
return nil unless api_key
return nil if api_key.expired?
api_key.touch(:last_used_at)
api_key
end
def expired?
expires_at.present? && expires_at < Time.current
end
def has_scope?(scope)
scopes.include?(scope.to_s)
end
private
def generate_key
raw_key = SecureRandom.hex(32)
self.key_prefix = raw_key[0..7]
self.key_digest = Digest::SHA256.hexdigest(raw_key)
# Return raw key to show user (only time it's visible)
@raw_key = raw_key
end
attr_reader :raw_key
end
module ApiKeyAuthentication
extend ActiveSupport::Concern
included do
before_action :authenticate_with_api_key
end
private
def authenticate_with_api_key
key = extract_api_key
unless key
render json: { error: 'MISSING_API_KEY' }, status: :unauthorized
return
end
@current_api_key = ApiKey.authenticate(key)
unless @current_api_key
render json: { error: 'INVALID_API_KEY' }, status: :unauthorized
return
end
@current_user = @current_api_key.user
end
def extract_api_key
# Try Authorization header first
header = request.headers['Authorization']
return header.split(' ').last if header&.start_with?('Bearer ')
# Fall back to X-API-Key header
request.headers['X-API-Key']
end
def require_scope(scope)
unless @current_api_key&.has_scope?(scope)
render json: { error: 'INSUFFICIENT_SCOPE' }, status: :forbidden
end
end
attr_reader :current_api_key, :current_user
end
While JWT works well for user authentication, service-to-service communication often uses simpler API key authentication. I generate cryptographically random API keys using SecureRandom.hex(32) and store them hashed in the database, similar to passwords. Clients send keys via Authorization: Bearer <key> or custom X-API-Key headers. Each key has associated scopes defining permissions, rate limits, and belongs to a specific account or service. I support key rotation by allowing multiple active keys per account. API keys are logged (partially, like key_abc...xyz) for audit purposes. For security, I require HTTPS for all API requests and implement rate limiting per key. Keys can be revoked immediately by deletion from the database.