{"title":"Security Features","description":"Encrypted attributes, secure tokens, and signed IDs in Grant ORM","section":"guides/models/grant","version":"v2","path":"guides/models/grant/security","canonical_url":"https://amberframework.org/docs/v2/guides/models/grant/security","markdown_url":"https://amberframework.org/docs/v2/guides/models/grant/security.md","inherited":false,"content_markdown":"# Security Features\n\n> **Supported web path:** Amber CLI `2.0.6` includes Grant in every generated\n> web application and pins the reviewed V2 commit. Preserve that pin while\n> following this beta.\n\n## Where the examples go\n\nEncrypted attributes, secure-token declarations, signed-ID methods,\nnormalization, and enums belong inside the matching Grant model under\n`src/models/`. Configure encryption in `config/application.cr`; the generated\napplication entry point loads top-level `config/*` before application source.\nToken generation and lookup expressions run from the controller, job, service,\nor spec that owns the security flow.\nNever put key values in source code or committed environment YAML.\n\nGrant provides built-in security features for protecting sensitive data, generating secure tokens, and creating tamper-proof URLs.\n\n## Encrypted Attributes\n\nStore sensitive data encrypted at rest.\n\n### Basic Encryption\n\n```crystal\nclass User < Grant::Base\n  column id : Int64, primary: true\n  column email : String\n  column ssn : String?\n  column credit_card_number : String?\n\n  # Encrypt these fields\n  encrypts :ssn, :credit_card_number\nend\n\n# Usage is transparent\nuser = User.create!(\n  email: \"alice@example.com\",\n  ssn: \"123-45-6789\"\n)\n\nuser.ssn  # => \"123-45-6789\" (decrypted)\n# In database: encrypted blob\n```\n\n### Deterministic Encryption\n\nUse deterministic encryption when you need to search encrypted fields.\n\n```crystal\nclass User < Grant::Base\n  # Non-deterministic (more secure, cannot search)\n  encrypts :ssn\n\n  # Deterministic (searchable)\n  encrypts :phone_number, deterministic: true\nend\n\n# Can search deterministic fields\nUser.where(phone_number: \"+1-555-1234\")  # Works\n\n# Cannot search non-deterministic fields\nUser.where(ssn: \"123-45-6789\")  # Won't work\n```\n\n### Configuration\n\n**File: `config/application.cr` — append this configuration after the Grant\ndependency is required.**\n\n```crystal\nGrant::Encryption.configure do |config|\n  config.primary_key = ENV[\"ENCRYPTION_PRIMARY_KEY\"]\n  config.key_derivation_salt = ENV[\"ENCRYPTION_KEY_DERIVATION_SALT\"]\n  config.deterministic_key = ENV[\"ENCRYPTION_DETERMINISTIC_KEY\"]\nend\n\n# Generate keys\n# crystal eval 'require \"random\"; puts Random::Secure.hex(32)'\n```\n\n## Secure Tokens\n\nGenerate cryptographically secure tokens for authentication.\n\n### Basic Token Generation\n\n```crystal\nclass User < Grant::Base\n  column id : Int64, primary: true\n  column email : String\n  column auth_token : String?\n\n  has_secure_token :auth_token\nend\n\nuser = User.create!(email: \"alice@example.com\")\nuser.auth_token  # => \"pX27zsMN2ViQKta1bGfLmVJE\"\n\n# Regenerate token\nuser.regenerate_auth_token\n```\n\n### Token Options\n\n```crystal\nclass ApiKey < Grant::Base\n  column id : Int64, primary: true\n  column user_id : Int64\n  column key : String?\n  column secret : String?\n\n  # Default: 24 characters, URL-safe base64\n  has_secure_token :key\n\n  # Custom length\n  has_secure_token :secret, length: 32\n\n  # Hex format\n  has_secure_token :hex_key, length: 16, alphabet: :hex\nend\n```\n\n### Token Authentication\n\n```crystal\nclass ApplicationController < Amber::Controller::Base\n  def authenticate_api_key\n    token = request.headers[\"Authorization\"]?\n      .try(&.gsub(\"Bearer \", \"\"))\n\n    unless token && ApiKey.find_by(key: token)\n      halt!(401, \"Invalid API key\")\n    end\n  end\nend\n```\n\n## Signed IDs\n\nCreate tamper-proof, expiring identifiers for URLs.\n\n### Basic Signed IDs\n\n```crystal\nclass User < Grant::Base\n  include Grant::SignedId\n\n  column id : Int64, primary: true\n  column email : String\nend\n\nuser = User.find!(1)\n\n# Generate signed ID\nsigned_id = user.signed_id\n# => \"eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1RPT0iL...\"\n\n# Find by signed ID\nfound = User.find_signed(signed_id)\n# => User(id: 1, email: \"alice@example.com\")\n\n# Invalid/tampered ID returns nil\nUser.find_signed(\"tampered_id\")  # => nil\n```\n\n### Expiring Signed IDs\n\n```crystal\n# Expires in 15 minutes\nsigned_id = user.signed_id(expires_in: 15.minutes)\n\n# Expires at specific time\nsigned_id = user.signed_id(expires_at: 1.hour.from_now)\n\n# Expired ID returns nil\nUser.find_signed(expired_signed_id)  # => nil\n```\n\n### Scoped Signed IDs\n\n```crystal\n# Scope to specific purpose\nsigned_id = user.signed_id(purpose: :password_reset)\n\n# Must use same purpose to verify\nUser.find_signed(signed_id, purpose: :password_reset)  # Works\nUser.find_signed(signed_id, purpose: :email_confirm)   # => nil\n```\n\n### Use Cases\n\n```crystal\nclass PasswordResetController < ApplicationController\n  def create\n    user = User.find_by!(email: params[\"email\"])\n    token = user.signed_id(\n      expires_in: 15.minutes,\n      purpose: :password_reset\n    )\n\n    PasswordResetMailer.send(user.email, token)\n    redirect_to \"/login\", notice: \"Check your email\"\n  end\n\n  def update\n    user = User.find_signed!(\n      params[\"token\"],\n      purpose: :password_reset\n    )\n\n    user.update!(password: params[\"password\"])\n    redirect_to \"/login\", notice: \"Password updated\"\n  rescue Grant::InvalidSignedId\n    redirect_to \"/forgot-password\", alert: \"Invalid or expired link\"\n  end\nend\n```\n\n## Token Generation (token_for)\n\nGenerate purpose-specific tokens that can include record state.\n\n```crystal\nclass User < Grant::Base\n  include Grant::TokenFor\n\n  column id : Int64, primary: true\n  column email : String\n  column password_salt : String\n\n  # Token invalidates when password_salt changes\n  generates_token_for :password_reset, expires_in: 15.minutes do\n    password_salt\n  end\n\n  generates_token_for :email_confirmation, expires_in: 24.hours do\n    email\n  end\nend\n\n# Generate token\nuser = User.find!(1)\ntoken = user.generate_token_for(:password_reset)\n\n# Find by token\nfound = User.find_by_token_for(:password_reset, token)\n\n# Token invalidates if password changes\nuser.update!(password_salt: SecureRandom.hex)\nUser.find_by_token_for(:password_reset, token)  # => nil\n```\n\n## Data Normalization\n\nAutomatically normalize data before saving.\n\n```crystal\nclass User < Grant::Base\n  column email : String\n  column phone : String?\n  column name : String\n\n  # Normalize email\n  normalizes :email, &.downcase.strip\n\n  # Normalize name\n  normalizes :name, &.strip.titleize\n\n  # Normalize phone (remove non-digits)\n  normalizes :phone do |phone|\n    phone.gsub(/\\D/, \"\")\n  end\nend\n\nuser = User.new(\n  email: \"  ALICE@Example.COM  \",\n  name: \"alice smith\",\n  phone: \"(555) 123-4567\"\n)\n\nuser.email  # => \"alice@example.com\"\nuser.name   # => \"Alice Smith\"\nuser.phone  # => \"5551234567\"\n```\n\n## Enum Attributes\n\nType-safe enumerated values.\n\n```crystal\nclass User < Grant::Base\n  column id : Int64, primary: true\n  column role : String\n\n  enum Role\n    Guest\n    Member\n    Admin\n    SuperAdmin\n  end\n\n  enum_attribute role : Role = :member\nend\n\nuser = User.new\nuser.role        # => Role::Member\nuser.member?     # => true\nuser.admin?      # => false\n\nuser.admin!      # Sets role to Admin\nuser.role        # => Role::Admin\n\n# Scopes generated automatically\nUser.admin       # Users with admin role\nUser.member      # Users with member role\n```\n\n## Best Practices\n\n### 1. Protect Sensitive Data\n\n```crystal\nclass User < Grant::Base\n  # Always encrypt PII\n  encrypts :ssn, :tax_id, :bank_account\n\n  # Deterministic only when searchable needed\n  encrypts :phone_number, deterministic: true\n\n  # Never log sensitive data\n  @[JSON::Field(ignore: true)]\n  column ssn : String?\nend\n```\n\n### 2. Use Scoped Tokens\n\n```crystal\n# Always scope tokens to purpose\nsigned_id = user.signed_id(purpose: :password_reset)\n\n# Never use generic signed IDs for sensitive operations\n```\n\n### 3. Set Appropriate Expiration\n\n```crystal\n# Short expiration for sensitive operations\npassword_reset_token = user.signed_id(\n  expires_in: 15.minutes,\n  purpose: :password_reset\n)\n\n# Longer for less sensitive\nemail_unsubscribe = user.signed_id(\n  expires_in: 30.days,\n  purpose: :unsubscribe\n)\n```\n\n### 4. Rotate Encryption Keys\n\n```crystal\n# Support key rotation\nGrant::Encryption.configure do |config|\n  config.primary_key = ENV[\"NEW_ENCRYPTION_KEY\"]\n  config.previous_keys = [ENV[\"OLD_ENCRYPTION_KEY\"]]\nend\n```"}