Configuration
Server
Section titled “Server”Nothing in the repository names a host, a domain, or a secret — those are facts about a deployment. Everything arrives through the environment.
POSTGRES_HOST POSTGRES_PORT |
where the database is |
POSTGRES_USER POSTGRES_PASSWORD |
must not be a superuser — RLS is bypassed by one |
POSTGRES_DATABASE |
production only |
SECRET_KEY_BASE |
required in production |
ENCRYPTION_PRIMARY_KEY |
ActiveRecord encryption |
ENCRYPTION_DETERMINISTIC_KEY |
|
ENCRYPTION_KEY_DERIVATION_SALT |
|
MASKS_HOST_SUFFIX |
the suffix tenant subdomains hang off, development only |
MASKS_PUBLIC_ORIGIN_TEMPLATE |
overrides the derived issuer origin; takes %{subdomain} |
PORT |
Generate real encryption keys with bin/rails db:encryption:init. The defaults in .env.example
are development values and are labelled as such.
Tenants and first run
Section titled “Tenants and first run”MASKS_TENANTS |
comma or space separated; ensured on boot | |
MASKS_SETUP_TOKEN |
required to answer the setup prompt, when set |
Declaring MASKS_TENANTS turns claiming off. Leave it unset and the first request to a hostname
nothing serves claims that subdomain — once, and only while no tenant exists at all, so it cannot be
used to mint tenants against a wildcard. See connecting an app.
Rate limits
Section titled “Rate limits”Each is a count per tenant; the windows are fixed. Hardening explains why sign-in carries two of them.
MASKS_ATTEMPT_LIMIT |
sign-in and second factor, per address — default 10 |
MASKS_ACCOUNT_ATTEMPT_LIMIT |
sign-in, per identifier — default 5 |
MASKS_REGISTRATION_LIMIT |
dynamic client registration — default 10 |
Development only
Section titled “Development only”PG_BIN_PATH |
prepended to PATH, when the linked pg_dump is older than the server |
The schema is dumped as SQL rather than Ruby, because schema.rb cannot represent a row-level
security policy — so pg_dump has to match the server it dumps, and refuses outright when it does
not. bin/setup checks for one before it does anything else.
MASKS_PUBLIC_ORIGIN_TEMPLATE exists for the case where something in front of the app rewrites the
host — a tunnel, or a proxy that does not set forwarding headers. It must be stable, because OAuth
redirect URIs and token audiences are registered against it. Leave it blank otherwise.
The gem
Section titled “The gem”One gem, masks, holds both halves of the consumer side. require "masks" loads the client always
and the Rails engine only when Rails is already loaded, so there is nothing to configure to keep the
framework out of a service that does not want it.
The generator writes this file. Everything in it has a default except issuer.
Masks::Rails.configure do |config| config.issuer = "https://jons.auth.example" config.credentials = -> { { client_id: ENV["MASKS_CLIENT_ID"], client_secret: ENV["MASKS_CLIENT_SECRET"] } } config.store = ->(request, registration) { Credentials.store!(registration) } config.name = "things" config.resource = nil config.resource_scopes = [] config.scope = %w[openid profile email]endConnecting
issuer |
required — the masks issuer this app signs in against | |
name |
what the approval screen calls this app; defaults to the application’s module name | |
credentials |
client_id and client_secret, or nil when the app has not been connected |
|
store |
given (request, registration) after a handshake — write them down |
|
forget |
given (request) to drop them again; an app that sets store and not this is never offered a disconnect |
|
credentials_path |
config/masks.json |
where the default store writes, when you set neither |
Asking
scope |
openid profile email |
what to request, and what the handshake asks to be approved for |
resource |
nil |
the API the access token is for — sets its aud, and what the handshake asks about |
resource_scopes |
[] |
scope to description, published as RFC 9728 metadata |
redirect_uri |
this host’s /auth/callback |
must match a URI the client was approved for |
Behaviour
after_sign_in |
/ |
where to land, unless a return path was remembered |
after_sign_out |
/ |
where to land after signing out |
sign_out_of_issuer |
false |
make every sign-out an RP-initiated logout at the issuer |
session_key |
masks |
the session key tokens are stored under |
parent_controller |
ActionController::Base |
what the engine’s own pages inherit, for your layout |
authenticate_everything |
false |
include Authentication on every controller rather than where you say |
Omit the secret for a public client. An app with no credentials yet is not misconfigured — it is unconnected, and every refusal says so.
Every one of these accepts a callable taking the request, which is how one application serves many tenants:
config.issuer = ->(request) { "https://#{request.host.split(".").first}.auth.example" }config.resource = ->(request) { "https://#{request.host}/mcp" }Any Ruby app
Section titled “Any Ruby app”No global configuration. Construct what you need:
Masks::Client.issuer(url) # discovery and JWKS, cachedMasks::Client.verifier(url, audience:) # for a resource serverMasks::Client::Session.new(issuer:, client_id:, redirect_uri:, client_secret:, scope:)Masks::Client::Registration.create(issuer, token: nil, **metadata)Masks::Client::Handshake.new(issuer, name:, resource:, redirect_uris:, return_to:, scope:)A Registration carries client_id, client_secret, access_token and uri, reads itself back
with read, changes itself with update, archives itself with delete, and builds a Session
with session(redirect_uri:).
