Authentication
Snails gives you the HTTP handshake, not the user model: the controller reads a token or a username and password from the request, your app decides whether it belongs to a user, and the helper answers 401 when it does not. No session store, no cookie middleware, and no token table are built in.
Read a token#
authenticate_with_http_token yields the token and its options to a block. It sends no challenge of its own, and a request without a token leaves the block unrun and answers nil:
class Api::TokensController < Api::ApplicationController
def show
account = authenticate_with_http_token do |token, options|
Account.find_by({ api_token: token })
end
if account.nil?
head(:unauthorized, content_type: JSON_TYPE)
else
render(json: { "account" => account.name })
end
end
endThe client sends either scheme, with optional key=value pairs after the token:
Authorization: Token abc123
Authorization: Bearer abc123
Authorization: Token abc123, scope=read, region=usThe token is the first value; the pairs after it arrive as options, keyed by strings. Separate pairs with commas or semicolons — a space-separated scope=read is part of the token value, not an option.
Answer the 401 in one call#
The or_request form does the challenge for you. When the block answers nil or false, or when the scheme is missing, the controller sends a 401 with a WWW-Authenticate header and stops the filter chain:
class Api::TokensController < Api::ApplicationController
before_action :authenticate
def show
render(json: { "account" => @account.name })
end
private
def authenticate
@account = authenticate_or_request_with_http_token("Snails API") do |token, options|
Account.find_by({ api_token: token })
end
end
endHTTP/1.1 401 Unauthorized
WWW-Authenticate: Token realm="Snails API"
Content-Type: text/plain; charset=utf-8
HTTP Token: Access denied.realm names the protection space in the challenge; the default is "Application". Pass message: to replace the default body. The render marks the response performed, so neither the rest of the action nor any later before_action runs.
The two helpers are separate operations — a block that renders a body still needs a truthy return value. Use the non-challenge form and answer the 401 yourself when the block owns the response.
Basic authentication#
authenticate_with_http_basic yields the decoded username and password, split at the first colon:
def authenticate
@account = authenticate_or_request_with_http_basic("Admin") do |username, password|
Account.find_by({ email: username, password_digest: digest(password) })
end
endThe challenge header is Basic realm="Admin" and the default body is HTTP Basic: Access denied.. A password may contain colons; the username may not. Basic credentials travel in clear text inside the header, so use it over TLS or not at all.
Challenge without a helper#
The challenge renderers are public, so a custom flow can send them directly:
request_http_token_authentication("Snails API", "Token expired.\n")
request_http_basic_authentication("Admin")Each writes the WWW-Authenticate header, a 401 status, and a plain-text body. Call one from an action or a filter after your own lookup fails.
Where the boundary sits#
The runtime owns the header parsing and the 401 response shape. Your app owns the credential store and the comparison. A token lookup is a model query like any other:
def current_account
authenticate_with_http_token do |token, options|
Account.find_by({ api_token: token, revoked_at: nil })
end
endfind_by is enough when the token column is unique; a token table with an expiry or scope column is your schema. Nothing in the runtime stores sessions, rotates tokens, or compares secrets in constant time.
The signing key for tokens you issue yourself comes from Snails.app.secret_key_base; Configuration covers that setting.