Documentation

Security Features

New
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

1.4.1
Unavailable in this version
1.5
Unavailable in this version

Security Features

Supported web path: Amber CLI 2.0.6 includes Grant in every generated web application and pins the reviewed V2 commit. Preserve that pin while following this beta.

Where the examples go

Encrypted attributes, secure-token declarations, signed-ID methods, normalization, and enums belong inside the matching Grant model under src/models/. Configure encryption in config/application.cr; the generated application entry point loads top-level config/* before application source. Token generation and lookup expressions run from the controller, job, service, or spec that owns the security flow. Never put key values in source code or committed environment YAML.

Grant provides built-in security features for protecting sensitive data, generating secure tokens, and creating tamper-proof URLs.

Encrypted Attributes

Store sensitive data encrypted at rest.

Basic Encryption

Crystal
class User < Grant::Base
  column id : Int64, primary: true
  column email : String
  column ssn : String?
  column credit_card_number : String?

  # Encrypt these fields
  encrypts :ssn, :credit_card_number
end

# Usage is transparent
user = User.create!(
  email: "[email protected]",
  ssn: "123-45-6789"
)

user.ssn  # => "123-45-6789" (decrypted)
# In database: encrypted blob

Deterministic Encryption

Use deterministic encryption when you need to search encrypted fields.

Crystal
class User < Grant::Base
  # Non-deterministic (more secure, cannot search)
  encrypts :ssn

  # Deterministic (searchable)
  encrypts :phone_number, deterministic: true
end

# Can search deterministic fields
User.where(phone_number: "+1-555-1234")  # Works

# Cannot search non-deterministic fields
User.where(ssn: "123-45-6789")  # Won't work

Configuration

File: config/application.cr — append this configuration after the Grant dependency is required.

Crystal
Grant::Encryption.configure do |config|
  config.primary_key = ENV["ENCRYPTION_PRIMARY_KEY"]
  config.key_derivation_salt = ENV["ENCRYPTION_KEY_DERIVATION_SALT"]
  config.deterministic_key = ENV["ENCRYPTION_DETERMINISTIC_KEY"]
end

# Generate keys
# crystal eval 'require "random"; puts Random::Secure.hex(32)'

Secure Tokens

Generate cryptographically secure tokens for authentication.

Basic Token Generation

Crystal
class User < Grant::Base
  column id : Int64, primary: true
  column email : String
  column auth_token : String?

  has_secure_token :auth_token
end

user = User.create!(email: "[email protected]")
user.auth_token  # => "pX27zsMN2ViQKta1bGfLmVJE"

# Regenerate token
user.regenerate_auth_token

Token Options

Crystal
class ApiKey < Grant::Base
  column id : Int64, primary: true
  column user_id : Int64
  column key : String?
  column secret : String?

  # Default: 24 characters, URL-safe base64
  has_secure_token :key

  # Custom length
  has_secure_token :secret, length: 32

  # Hex format
  has_secure_token :hex_key, length: 16, alphabet: :hex
end

Token Authentication

Crystal
class ApplicationController < Amber::Controller::Base
  def authenticate_api_key
    token = request.headers["Authorization"]?
      .try(&.gsub("Bearer ", ""))

    unless token && ApiKey.find_by(key: token)
      halt!(401, "Invalid API key")
    end
  end
end

Signed IDs

Create tamper-proof, expiring identifiers for URLs.

Basic Signed IDs

Crystal
class User < Grant::Base
  include Grant::SignedId

  column id : Int64, primary: true
  column email : String
end

user = User.find!(1)

# Generate signed ID
signed_id = user.signed_id
# => "eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1RPT0iL..."

# Find by signed ID
found = User.find_signed(signed_id)
# => User(id: 1, email: "[email protected]")

# Invalid/tampered ID returns nil
User.find_signed("tampered_id")  # => nil

Expiring Signed IDs

Crystal
# Expires in 15 minutes
signed_id = user.signed_id(expires_in: 15.minutes)

# Expires at specific time
signed_id = user.signed_id(expires_at: 1.hour.from_now)

# Expired ID returns nil
User.find_signed(expired_signed_id)  # => nil

Scoped Signed IDs

Crystal
# Scope to specific purpose
signed_id = user.signed_id(purpose: :password_reset)

# Must use same purpose to verify
User.find_signed(signed_id, purpose: :password_reset)  # Works
User.find_signed(signed_id, purpose: :email_confirm)   # => nil

Use Cases

Crystal
class PasswordResetController < ApplicationController
  def create
    user = User.find_by!(email: params["email"])
    token = user.signed_id(
      expires_in: 15.minutes,
      purpose: :password_reset
    )

    PasswordResetMailer.send(user.email, token)
    redirect_to "/login", notice: "Check your email"
  end

  def update
    user = User.find_signed!(
      params["token"],
      purpose: :password_reset
    )

    user.update!(password: params["password"])
    redirect_to "/login", notice: "Password updated"
  rescue Grant::InvalidSignedId
    redirect_to "/forgot-password", alert: "Invalid or expired link"
  end
end

Token Generation (token_for)

Generate purpose-specific tokens that can include record state.

Crystal
class User < Grant::Base
  include Grant::TokenFor

  column id : Int64, primary: true
  column email : String
  column password_salt : String

  # Token invalidates when password_salt changes
  generates_token_for :password_reset, expires_in: 15.minutes do
    password_salt
  end

  generates_token_for :email_confirmation, expires_in: 24.hours do
    email
  end
end

# Generate token
user = User.find!(1)
token = user.generate_token_for(:password_reset)

# Find by token
found = User.find_by_token_for(:password_reset, token)

# Token invalidates if password changes
user.update!(password_salt: SecureRandom.hex)
User.find_by_token_for(:password_reset, token)  # => nil

Data Normalization

Automatically normalize data before saving.

Crystal
class User < Grant::Base
  column email : String
  column phone : String?
  column name : String

  # Normalize email
  normalizes :email, &.downcase.strip

  # Normalize name
  normalizes :name, &.strip.titleize

  # Normalize phone (remove non-digits)
  normalizes :phone do |phone|
    phone.gsub(/\D/, "")
  end
end

user = User.new(
  email: "  [email protected]  ",
  name: "alice smith",
  phone: "(555) 123-4567"
)

user.email  # => "[email protected]"
user.name   # => "Alice Smith"
user.phone  # => "5551234567"

Enum Attributes

Type-safe enumerated values.

Crystal
class User < Grant::Base
  column id : Int64, primary: true
  column role : String

  enum Role
    Guest
    Member
    Admin
    SuperAdmin
  end

  enum_attribute role : Role = :member
end

user = User.new
user.role        # => Role::Member
user.member?     # => true
user.admin?      # => false

user.admin!      # Sets role to Admin
user.role        # => Role::Admin

# Scopes generated automatically
User.admin       # Users with admin role
User.member      # Users with member role

Best Practices

1. Protect Sensitive Data

Crystal
class User < Grant::Base
  # Always encrypt PII
  encrypts :ssn, :tax_id, :bank_account

  # Deterministic only when searchable needed
  encrypts :phone_number, deterministic: true

  # Never log sensitive data
  @[JSON::Field(ignore: true)]
  column ssn : String?
end

2. Use Scoped Tokens

Crystal
# Always scope tokens to purpose
signed_id = user.signed_id(purpose: :password_reset)

# Never use generic signed IDs for sensitive operations

3. Set Appropriate Expiration

Crystal
# Short expiration for sensitive operations
password_reset_token = user.signed_id(
  expires_in: 15.minutes,
  purpose: :password_reset
)

# Longer for less sensitive
email_unsubscribe = user.signed_id(
  expires_in: 30.days,
  purpose: :unsubscribe
)

4. Rotate Encryption Keys

Crystal
# Support key rotation
Grant::Encryption.configure do |config|
  config.primary_key = ENV["NEW_ENCRYPTION_KEY"]
  config.previous_keys = [ENV["OLD_ENCRYPTION_KEY"]]
end